diff --git a/.gitignore b/.gitignore
index d2bf98a016f588760241f9dc7f90f6197c458404..7ee4ff1903e902c4715c6e2b0c3e784ed5755aaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,3 @@
-<<<<<<< HEAD
-=======
-
->>>>>>> develop
 # IDE specific files
 .project
 .cproject
@@ -19,3 +15,6 @@ cmake-build-release/
 *.exe
 *.ilk
 *.pdb
+
+# GUI configuration files
+imgui.ini
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 318f3e931e557421a5e9275735174cdee7947453..33b70018e368ecc3ad019ea33e57485814eb233a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -14,7 +14,7 @@ build_ubuntu_gcc:
       - $RUN =~ /\bubuntu.*/i || $RUN =~ /\ball.*/i
   stage: build
   tags: 
-    - ubuntu-gcc
+    - ubuntu-gcc-cached
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
   timeout: 10m
@@ -37,11 +37,11 @@ build_win10_msvc:
       - $RUN =~ /\bwin.*/i || $RUN =~ /\ball.*/i
   stage: build
   tags: 
-    - win10-msvc
+    - win10-msvc-cached
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
   timeout: 10m
-  retry: 1
+  retry: 0
   script:
     - cd 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\'
     - .\Launch-VsDevShell.ps1
diff --git a/.gitmodules b/.gitmodules
index 983b753744e8767da0ec3c959c32a3766ee346f6..e0aaf2d17c340f98ae875f7e0f1238bfe04f7e5d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -15,4 +15,10 @@
 	url = https://github.com/nothings/stb.git
 [submodule "modules/camera/lib/glm"]
 	path = modules/camera/lib/glm
-	url = https://github.com/g-truc/glm.git
\ No newline at end of file
+	url = https://github.com/g-truc/glm.git
+[submodule "modules/shader_compiler/lib/glslang"]
+	path = modules/shader_compiler/lib/glslang
+	url = https://github.com/KhronosGroup/glslang.git
+[submodule "modules/gui/lib/imgui"]
+	path = modules/gui/lib/imgui
+	url = https://github.com/ocornut/imgui.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0a7f978f81313ecc3a657dd670d2a16db3cd4e8d..2ae078a428a8e5e640ed8dc7bcc2f4e58e159c6b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -41,24 +41,24 @@ if (vkcv_build_debug)
 	endif()
 endif()
 
+# configure everything to use the required dependencies
+include(${vkcv_config}/Libraries.cmake)
+
+# set the compile definitions aka preprocessor variables
+add_compile_definitions(${vkcv_definitions})
+
 # add modules as targets
 add_subdirectory(modules)
 
 # add source files for compilation
 include(${vkcv_config}/Sources.cmake)
 
-# configure everything to use the required dependencies
-include(${vkcv_config}/Libraries.cmake)
-
 message("-- Libraries: [ ${vkcv_libraries} ]")
 message("-- Flags: [ ${vkcv_flags} ]")
 
 # set the compiler flags for the framework
 set(CMAKE_CXX_FLAGS ${vkcv_flags})
 
-# set the compile definitions aka preprocessor variables
-add_compile_definitions(${vkcv_definitions})
-
 # create VkCV framework as static library using all source files
 add_library(vkcv STATIC ${vkcv_sources})
 
diff --git a/Doxyfile b/Doxyfile
index be7d96f6cce1ed0ec753dafeaccb6314b9f6e04f..a657ede81c43b3cae1fc5c17f071aa7dc65add04 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -865,7 +865,8 @@ WARN_LOGFILE           =
 # Note: If this tag is empty the current directory is searched.
 
 INPUT                  = src \
-                         include
+                         include \
+                         modules
 
 # This tag can be used to specify the character encoding of the source files
 # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
@@ -953,7 +954,7 @@ RECURSIVE              = YES
 # Note that relative paths are relative to the directory from which doxygen is
 # run.
 
-EXCLUDE                =
+EXCLUDE                = lib
 
 # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
 # directories that are symbolic links (a Unix file system feature) are excluded
@@ -969,7 +970,7 @@ EXCLUDE_SYMLINKS       = NO
 # Note that the wildcards are matched against the file with absolute path, so to
 # exclude all test directories for example use the pattern */test/*
 
-EXCLUDE_PATTERNS       =
+EXCLUDE_PATTERNS       = */lib/*
 
 # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
 # (namespaces, classes, functions, etc.) that should be excluded from the
diff --git a/config/Libraries.cmake b/config/Libraries.cmake
index e04aa3575a34632eb75c929bf4640305cd93e298..ec014f84c820abf4988b070d5b733be08c377319 100644
--- a/config/Libraries.cmake
+++ b/config/Libraries.cmake
@@ -10,6 +10,8 @@ if(NOT WIN32)
 	list(APPEND vkcv_flags -fopenmp)
 endif()
 
+list(APPEND vkcv_definitions _USE_MATH_DEFINES)
+
 # some formatted printing
 set(vkcv_config_msg " - Library: ")
 
diff --git a/config/Sources.cmake b/config/Sources.cmake
index 4f673e00d1e42e733534480d6085affd651a8c04..54bb3485ed975669668d987787975f019aa6358b 100644
--- a/config/Sources.cmake
+++ b/config/Sources.cmake
@@ -32,8 +32,8 @@ set(vkcv_sources
 		
 		${vkcv_include}/vkcv/Logger.hpp
 
-		${vkcv_include}/vkcv/SwapChain.hpp
-		${vkcv_source}/vkcv/SwapChain.cpp
+		${vkcv_include}/vkcv/Swapchain.hpp
+		${vkcv_source}/vkcv/Swapchain.cpp
 		
 		${vkcv_include}/vkcv/ShaderStage.hpp
 		
@@ -41,7 +41,6 @@ set(vkcv_sources
 		${vkcv_source}/vkcv/ShaderProgram.cpp
 
 		${vkcv_include}/vkcv/PipelineConfig.hpp
-		${vkcv_source}/vkcv/PipelineConfig.cpp
 
 		${vkcv_source}/vkcv/PipelineManager.hpp
 		${vkcv_source}/vkcv/PipelineManager.cpp
@@ -81,4 +80,7 @@ set(vkcv_sources
         ${vkcv_source}/vkcv/CommandStreamManager.cpp
         
         ${vkcv_include}/vkcv/CommandRecordingFunctionTypes.hpp
+        
+        ${vkcv_include}/vkcv/ImageConfig.hpp
+        ${vkcv_source}/vkcv/ImageConfig.cpp
 )
diff --git a/include/vkcv/BufferManager.hpp b/include/vkcv/BufferManager.hpp
index a390a2ff3be7f84b0c12f065398f9e40a42017e0..9eb80d70862a79a01593e6fe4c3aabe98d253ac8 100644
--- a/include/vkcv/BufferManager.hpp
+++ b/include/vkcv/BufferManager.hpp
@@ -132,6 +132,9 @@ namespace vkcv
 		 */
 		void unmapBuffer(const BufferHandle& handle);
 		
+		void recordBufferMemoryBarrier(
+			const BufferHandle& handle,
+			vk::CommandBuffer cmdBuffer);
 	};
 	
 }
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 4a51b24f5c978daebc5116e20b527252c8063d61..cbbe1e908cdb74891ab9bfe4416c03e487e76b26 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -8,7 +8,7 @@
 #include <vulkan/vulkan.hpp>
 
 #include "vkcv/Context.hpp"
-#include "vkcv/SwapChain.hpp"
+#include "vkcv/Swapchain.hpp"
 #include "vkcv/Window.hpp"
 #include "vkcv/PassConfig.hpp"
 #include "vkcv/Handles.hpp"
@@ -52,7 +52,7 @@ namespace vkcv
          *
          * @param context encapsulates various Vulkan objects
          */
-        Core(Context &&context, Window &window, const SwapChain& swapChain,  std::vector<vk::ImageView> imageViews,
+        Core(Context &&context, Window &window, const Swapchain& swapChain,  std::vector<vk::ImageView> imageViews,
 			const CommandResources& commandResources, const SyncResources& syncResources) noexcept;
         // explicit destruction of default constructor
         Core() = delete;
@@ -61,11 +61,8 @@ namespace vkcv
 
         Context m_Context;
 
-        SwapChain                       m_swapchain;
-        std::vector<vk::ImageView>      m_swapchainImageViews;
-        std::vector<vk::Image>          m_swapchainImages;
-		std::vector<vk::ImageLayout>    m_swapchainImageLayouts;
-        const Window&                   m_window;
+        Swapchain                       m_swapchain;
+        Window&                   		m_window;
 
         std::unique_ptr<PassManager>            m_PassManager;
         std::unique_ptr<PipelineManager>        m_PipelineManager;
@@ -79,11 +76,7 @@ namespace vkcv
 		SyncResources       m_SyncResources;
 		uint32_t            m_currentSwapchainImageIndex;
 
-        std::function<void(int, int)> e_resizeHandle;
-
-        static std::vector<vk::ImageView> createImageViews( Context &context, SwapChain& swapChain);
-
-		void recordSwapchainImageLayoutTransition(vk::CommandBuffer cmdBuffer, vk::ImageLayout newLayout);
+		event_handle<int,int> e_resizeHandle;
 
     public:
         /**
@@ -123,6 +116,9 @@ namespace vkcv
 
         [[nodiscard]]
         const Context &getContext() const;
+        
+        [[nodiscard]]
+        const Swapchain& getSwapchain() const;
 
         /**
              * Creates a #Core with given @p applicationName and @p applicationVersion for your application.
@@ -216,7 +212,20 @@ namespace vkcv
          * @return Image-Object
          */
         [[nodiscard]]
-        Image createImage(vk::Format format, uint32_t width, uint32_t height, uint32_t depth = 1);
+        Image createImage(
+			vk::Format      format,
+			uint32_t        width,
+			uint32_t        height,
+			uint32_t        depth = 1,
+			bool            createMipChain = false,
+			bool            supportStorage = false,
+			bool            supportColorAttachment = false,
+			Multisampling   multisampling = Multisampling::None);
+
+        [[nodiscard]]
+        const uint32_t getImageWidth(ImageHandle imageHandle);
+        [[nodiscard]]
+        const uint32_t getImageHeight(ImageHandle imageHandle);
 
         /** TODO:
          *   @param setDescriptions
@@ -253,8 +262,6 @@ namespace vkcv
 		*/
 		void endFrame();
 
-		vk::Format getSwapchainImageFormat();
-
 		/**
 		 * Submit a command buffer to any queue of selected type. The recording can be customized by a
 		 * custom record-command-function. If the command submission has finished, an optional finish-function
@@ -264,7 +271,7 @@ namespace vkcv
 		 * @param record Record-command-function
 		 * @param finish Finish-command-function or nullptr
 		 */
-		void recordAndSubmitCommands(
+		void recordAndSubmitCommandsImmediate(
 			const SubmitInfo            &submitInfo, 
 			const RecordCommandFunction &record, 
 			const FinishCommandFunction &finish);
@@ -279,5 +286,12 @@ namespace vkcv
 		void submitCommandStream(const CommandStreamHandle handle);
 		void prepareSwapchainImageForPresent(const CommandStreamHandle handle);
 		void prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image);
+		void prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image);
+		void recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image);
+		void recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer);
+		void resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst);
+
+		vk::ImageView getSwapchainImageView() const;
+		
     };
 }
diff --git a/include/vkcv/DescriptorWrites.hpp b/include/vkcv/DescriptorWrites.hpp
index d67e8e3233e184b207d109e652adeca43407d7e0..f28a6c91e189b13413ffefec0f05e5a0a358ee26 100644
--- a/include/vkcv/DescriptorWrites.hpp
+++ b/include/vkcv/DescriptorWrites.hpp
@@ -4,15 +4,20 @@
 
 namespace vkcv {
 	struct SampledImageDescriptorWrite {
-		inline SampledImageDescriptorWrite(uint32_t binding, ImageHandle image) : binding(binding), image(image) {};
+		inline SampledImageDescriptorWrite(uint32_t binding, ImageHandle image, uint32_t mipLevel = 0, bool useGeneralLayout = false)
+		    : binding(binding), image(image), mipLevel(mipLevel), useGeneralLayout(useGeneralLayout) {};
 		uint32_t	binding;
 		ImageHandle	image;
+		uint32_t    mipLevel;
+		bool        useGeneralLayout;
 	};
 
 	struct StorageImageDescriptorWrite {
-		inline StorageImageDescriptorWrite(uint32_t binding, ImageHandle image) : binding(binding), image(image) {};
+		inline StorageImageDescriptorWrite(uint32_t binding, ImageHandle image, uint32_t mipLevel = 0) 
+			: binding(binding), image(image), mipLevel(mipLevel) {};
 		uint32_t	binding;
 		ImageHandle	image;
+		uint32_t	mipLevel;
 	};
 
 	struct UniformBufferDescriptorWrite {
@@ -36,8 +41,8 @@ namespace vkcv {
 	struct DescriptorWrites {
 		std::vector<SampledImageDescriptorWrite>		sampledImageWrites;
 		std::vector<StorageImageDescriptorWrite>		storageImageWrites;
-		std::vector<UniformBufferDescriptorWrite>	uniformBufferWrites;
-		std::vector<StorageBufferDescriptorWrite>	storageBufferWrites;
-		std::vector<SamplerDescriptorWrite>			samplerWrites;
+		std::vector<UniformBufferDescriptorWrite>	    uniformBufferWrites;
+		std::vector<StorageBufferDescriptorWrite>	    storageBufferWrites;
+		std::vector<SamplerDescriptorWrite>			    samplerWrites;
 	};
 }
\ No newline at end of file
diff --git a/include/vkcv/DrawcallRecording.hpp b/include/vkcv/DrawcallRecording.hpp
index 0929ad038fb95ec1573e7c76e5ce13adb84ab760..9f162a499a38d5633703f70eec8a8682e3328d72 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -37,11 +37,12 @@ namespace vkcv {
     };
 
     struct DrawcallInfo {
-        inline DrawcallInfo(const Mesh& mesh, const std::vector<DescriptorSetUsage>& descriptorSets)
-            : mesh(mesh), descriptorSets(descriptorSets) {}
+        inline DrawcallInfo(const Mesh& mesh, const std::vector<DescriptorSetUsage>& descriptorSets, const uint32_t instanceCount = 1)
+            : mesh(mesh), descriptorSets(descriptorSets), instanceCount(instanceCount){}
 
         Mesh                            mesh;
         std::vector<DescriptorSetUsage> descriptorSets;
+        uint32_t                        instanceCount;
     };
 
     void recordDrawcall(
diff --git a/include/vkcv/Event.hpp b/include/vkcv/Event.hpp
index dfa1325a0d06be309e24712d2115279dad171825..da5cbc72fbb3eee3a71a35c1da6fe32dff06b057 100644
--- a/include/vkcv/Event.hpp
+++ b/include/vkcv/Event.hpp
@@ -4,10 +4,18 @@
 #include <mutex>
 
 namespace vkcv {
+	
+	template<typename... T>
+	struct event_handle {
+		uint32_t id;
+	};
 
     template<typename... T>
     struct event_function {
         typedef std::function<void(T...)> type;
+	
+		event_handle<T...> handle;
+        type callback;
     };
 
     /**
@@ -17,8 +25,9 @@ namespace vkcv {
     template<typename... T>
     struct event {
     private:
-        std::vector<typename event_function<T...>::type> m_handles;
-        std::mutex m_mutex;
+        std::vector< event_function<T...> > m_functions;
+        uint32_t m_id_counter;
+		std::mutex m_mutex;
 
     public:
 
@@ -28,31 +37,37 @@ namespace vkcv {
          */
         void operator()(T... arguments) {
 			lock();
-        	
-            for (auto &handle : this->m_handles) {
-                handle(arguments...);
-            }
+
+            for (auto &function : this->m_functions) {
+				function.callback(arguments...);
+			}
             
             unlock();
         }
 
         /**
          * adds a function handle to the event to be called
-         * @param handle of the function
+         * @param callback of the function
+         * @return handle of the function
          */
-        typename event_function<T...>::type add(typename event_function<T...>::type handle) {
-            this->m_handles.push_back(handle);
-            return handle;
+		event_handle<T...> add(typename event_function<T...>::type callback) {
+			event_function<T...> function;
+			function.handle = { m_id_counter++ };
+			function.callback = callback;
+            this->m_functions.push_back(function);
+            return function.handle;
         }
 
         /**
          * removes a function handle of the event
          * @param handle of the function
          */
-        void remove(typename event_function<T...>::type handle) {
-            this->m_handles.erase(
-                    remove(this->m_handles.begin(), this->m_handles.end(), handle),
-                    this->m_handles.end()
+        void remove(event_handle<T...> handle) {
+            this->m_functions.erase(
+					std::remove_if(this->m_functions.begin(), this->m_functions.end(), [&handle](auto function){
+						return (handle.id == function.handle.id);
+					}),
+                    this->m_functions.end()
             );
         }
         
@@ -60,17 +75,21 @@ namespace vkcv {
          * locks the event so its function handles won't be called
          */
         void lock() {
-        	m_mutex.lock();
+			m_mutex.lock();
         }
 	
 		/**
 		* unlocks the event so its function handles can be called after locking
 		*/
         void unlock() {
-        	m_mutex.unlock();
+			m_mutex.unlock();
         }
 
-        event() = default;
+        explicit event(bool locked = false) {
+        	if (locked) {
+        		lock();
+        	}
+        }
 
         event(const event &other) = delete;
 
diff --git a/include/vkcv/Image.hpp b/include/vkcv/Image.hpp
index a1219ce4147ebbb8ae0650da8a87766f8967874b..85ab2b81e2718b3890ba361c988d5db0e40e84c7 100644
--- a/include/vkcv/Image.hpp
+++ b/include/vkcv/Image.hpp
@@ -7,11 +7,11 @@
 #include "vulkan/vulkan.hpp"
 
 #include "Handles.hpp"
+#include "vkcv/ImageConfig.hpp"
 
 namespace vkcv {
-
-    // forward declares
-    class ImageManager;
+	
+	class ImageManager;
 
 	bool isDepthFormat(const vk::Format format);
 
@@ -29,24 +29,37 @@ namespace vkcv {
 		
 		[[nodiscard]]
 		uint32_t getDepth() const;
-		
-		[[nodiscard]]
-		vk::ImageLayout getLayout() const;
 
 		[[nodiscard]]
 		vkcv::ImageHandle getHandle() const;
-		
+
+		[[nodiscard]]
+		uint32_t getMipCount() const;
+
 		void switchLayout(vk::ImageLayout newLayout);
 		
 		void fill(void* data, size_t size = SIZE_MAX);
+		void generateMipChainImmediate();
+		void recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream);
 	private:
-		ImageManager* const m_manager;
-		const ImageHandle   m_handle;
+	    // TODO: const qualifier removed, very hacky!!!
+	    //  Else you cannot recreate an image. Pls fix.
+		ImageManager*       m_manager;
+		ImageHandle   m_handle;
 
 		Image(ImageManager* manager, const ImageHandle& handle);
 		
-		static Image create(ImageManager* manager, vk::Format format, uint32_t width, uint32_t height, uint32_t depth);
-		
+		static Image create(
+			ImageManager*   manager,
+			vk::Format      format,
+			uint32_t        width,
+			uint32_t        height,
+			uint32_t        depth,
+			uint32_t        mipCount,
+			bool            supportStorage,
+			bool            supportColorAttachment,
+			Multisampling   msaa);
+
 	};
 	
 }
diff --git a/include/vkcv/ImageConfig.hpp b/include/vkcv/ImageConfig.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2e413b97be92ae771ef85342981ea0163a93ab52
--- /dev/null
+++ b/include/vkcv/ImageConfig.hpp
@@ -0,0 +1,9 @@
+#pragma once
+#include <vulkan/vulkan.hpp>
+
+namespace vkcv {
+	enum class Multisampling { None, MSAA2X, MSAA4X, MSAA8X };
+
+	vk::SampleCountFlagBits msaaToVkSampleCountFlag(Multisampling msaa);
+	uint32_t                msaaToSampleCount(Multisampling msaa);
+}
diff --git a/include/vkcv/Logger.hpp b/include/vkcv/Logger.hpp
index 251b6b528c45ea509dbfcd0cfb7135b77031f1ac..d484711f642506926b1281a830fb2c9caf8240a2 100644
--- a/include/vkcv/Logger.hpp
+++ b/include/vkcv/Logger.hpp
@@ -1,6 +1,6 @@
 #pragma once
 
-#include <iostream>
+#include <stdio.h>
 
 namespace vkcv {
 	
@@ -45,12 +45,12 @@ namespace vkcv {
   char output_message [             \
     VKCV_DEBUG_MESSAGE_LEN          \
   ];                                \
-  std::snprintf(                    \
+  snprintf(                         \
     output_message,                 \
     VKCV_DEBUG_MESSAGE_LEN,         \
     __VA_ARGS__                     \
   );                                \
-  std::fprintf(                     \
+  fprintf(                          \
     getLogOutput(level),            \
     "[%s]: %s [%s, line %d: %s]\n", \
   	vkcv::getLogName(level),        \
diff --git a/include/vkcv/PassConfig.hpp b/include/vkcv/PassConfig.hpp
index 8f3b516d4b4451c513366fbd8469908bccde6a5f..f3b2b802d062a441dfb0c810154205effb7053a2 100644
--- a/include/vkcv/PassConfig.hpp
+++ b/include/vkcv/PassConfig.hpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 #include <vulkan/vulkan.hpp>
+#include "ImageConfig.hpp"
 
 namespace vkcv
 {
@@ -45,7 +46,8 @@ namespace vkcv
 
     struct PassConfig
     {
-        explicit PassConfig(std::vector<AttachmentDescription> attachments) noexcept;
+        explicit PassConfig(std::vector<AttachmentDescription> attachments, Multisampling msaa = Multisampling::None) noexcept;
         std::vector<AttachmentDescription> attachments{};
+        Multisampling msaa;
     };
 }
\ No newline at end of file
diff --git a/include/vkcv/PipelineConfig.hpp b/include/vkcv/PipelineConfig.hpp
index 729330fcaf7eeac2acfdd1816b86ac29c7d9e30b..5e6dbaa3306f8d2aa6fc44d7dd1fadd9b79be3b4 100644
--- a/include/vkcv/PipelineConfig.hpp
+++ b/include/vkcv/PipelineConfig.hpp
@@ -10,37 +10,35 @@
 #include "Handles.hpp"
 #include "ShaderProgram.hpp"
 #include "VertexLayout.hpp"
+#include "ImageConfig.hpp"
 
 namespace vkcv {
 
-    struct PipelineConfig {
-        /**
-         *  Constructor for the pipeline. Creates a pipeline using @p vertexCode, @p fragmentCode as well as the
-         *  dimensions of the application window @p width and @p height. A handle for the Render Pass is also needed, @p passHandle.
-         *
-         * @param shaderProgram shaders of the pipeline
-         * @param height height of the application window
-         * @param width width of the application window
-         * @param passHandle handle for render pass
-         * @param vertexLayout layout of vertex buffer, comprised of its bindings and the bindings' attachments
-         */
-        PipelineConfig(
-            const ShaderProgram&                        shaderProgram,
-            uint32_t                                    width,
-            uint32_t                                    height,
-            const PassHandle                            &passHandle,
-            const VertexLayout                          &vertexLayouts,
-            const std::vector<vk::DescriptorSetLayout>  &descriptorLayouts,
-            bool                                        useDynamicViewport);
+    enum class PrimitiveTopology{PointList, LineList, TriangleList };
+	enum class CullMode{ None, Front, Back };
+    enum class DepthTest { None, Less, LessEqual, Greater, GreatherEqual, Equal };
 
-        ShaderProgram                         m_ShaderProgram;
-        uint32_t                              m_Height;
-        uint32_t                              m_Width;
-        PassHandle                            m_PassHandle;
-        VertexLayout                          m_VertexLayout;
-        std::vector<vk::DescriptorSetLayout>  m_DescriptorLayouts;
-        bool                                  m_UseDynamicViewport;
+    // add more as needed
+    // alternatively we could expose the blend factors directly
+    enum class BlendMode{ None, Additive };
 
+    struct PipelineConfig {
+        ShaderProgram                         	m_ShaderProgram;
+        uint32_t                              	m_Width;
+		uint32_t                              	m_Height;
+        PassHandle                            	m_PassHandle;
+        VertexLayout                          	m_VertexLayout;
+        std::vector<vk::DescriptorSetLayout>  	m_DescriptorLayouts;
+        bool                                  	m_UseDynamicViewport;
+        bool                                  	m_UseConservativeRasterization 	= false;
+        PrimitiveTopology                     	m_PrimitiveTopology 			= PrimitiveTopology::TriangleList;
+		BlendMode                             	m_blendMode 					= BlendMode::None;
+        bool                                    m_EnableDepthClamping           = false;
+        Multisampling                           m_multisampling                 = Multisampling::None;
+        CullMode                                m_culling                       = CullMode::None;
+        DepthTest                               m_depthTest                     = DepthTest::LessEqual;
+        bool                                    m_depthWrite                    = true;
+        bool                                    m_alphaToCoverage               = false;
     };
 
 }
\ No newline at end of file
diff --git a/include/vkcv/ShaderProgram.hpp b/include/vkcv/ShaderProgram.hpp
index 28f746d78477062eae9b0ad88f8c5de71e11efd0..78b1f02169fe630427b9f66150e32078d42b7b3f 100644
--- a/include/vkcv/ShaderProgram.hpp
+++ b/include/vkcv/ShaderProgram.hpp
@@ -51,7 +51,7 @@ namespace vkcv {
         const std::vector<VertexAttachment> &getVertexAttachments() const;
 		size_t getPushConstantSize() const;
 
-        const std::vector<std::vector<DescriptorBinding>> &getReflectedDescriptors() const;
+        const std::vector<std::vector<DescriptorBinding>>& getReflectedDescriptors() const;
 
 	private:
 	    /**
diff --git a/include/vkcv/SwapChain.hpp b/include/vkcv/Swapchain.hpp
similarity index 82%
rename from include/vkcv/SwapChain.hpp
rename to include/vkcv/Swapchain.hpp
index 089205d1633551b4ad9f11d0bdd5540b2bb61bbb..5e9bc7d0593a3b2e1f1f8e7b5ef7ea69e9711fb5 100644
--- a/include/vkcv/SwapChain.hpp
+++ b/include/vkcv/Swapchain.hpp
@@ -7,8 +7,12 @@
 
 namespace vkcv
 {
-    class SwapChain final {
+	
+	const uint32_t MIN_SWAPCHAIN_SIZE = 2;
+	
+    class Swapchain final {
     private:
+    	friend class Core;
 
         struct Surface
         {
@@ -21,10 +25,10 @@ namespace vkcv
         Surface m_Surface;
 
         vk::SwapchainKHR m_Swapchain;
-        vk::Format m_SwapchainFormat;
-        vk::ColorSpaceKHR m_SwapchainColorSpace;
-        vk::PresentModeKHR m_SwapchainPresentMode;
-		uint32_t m_SwapchainImageCount;
+        vk::Format m_Format;
+        vk::ColorSpaceKHR m_ColorSpace;
+        vk::PresentModeKHR m_PresentMode;
+		uint32_t m_ImageCount;
 	
 		vk::Extent2D m_Extent;
 	
@@ -39,16 +43,36 @@ namespace vkcv
          * @param format
          */
          // TODO:
-        SwapChain(const Surface &surface,
+        Swapchain(const Surface &surface,
                   vk::SwapchainKHR swapchain,
                   vk::Format format,
                   vk::ColorSpaceKHR colorSpace,
                   vk::PresentModeKHR presentMode,
                   uint32_t imageCount,
 				  vk::Extent2D extent) noexcept;
+	
+		/**
+		 * TODO
+		 *
+		 * @return
+		 */
+		bool shouldUpdateSwapchain() const;
+	
+		/**
+		 * TODO
+		 *
+		 * context
+		 * window
+		 */
+		void updateSwapchain(const Context &context, const Window &window);
+	
+		/**
+		 *
+		 */
+		void signalSwapchainRecreation();
 
     public:
-    	SwapChain(const SwapChain& other);
+    	Swapchain(const Swapchain& other);
 
         /**
          * @return The swapchain linked with the #SwapChain class
@@ -69,7 +93,7 @@ namespace vkcv
          * @return gets the chosen swapchain format
          */
         [[nodiscard]]
-        vk::Format getSwapchainFormat() const;
+        vk::Format getFormat() const;
 
         /**
          * creates a swap chain object out of the given window and the given context
@@ -77,37 +101,17 @@ namespace vkcv
          * @param context of the application
          * @return returns an object of swapChain
          */
-        static SwapChain create(const Window &window, const Context &context);
+        static Swapchain create(const Window &window, const Context &context);
 
         /**
          * Destructor of SwapChain
          */
-        virtual ~SwapChain();
+        virtual ~Swapchain();
 
 		/**
 		 * @return number of images in swapchain
 		*/
-		uint32_t getImageCount();
-		
-		/**
-		 * TODO
-		 *
-		 * @return
-		 */
-		bool shouldUpdateSwapchain() const;
-
-		/**
-		 * TODO
-		 *
-		 * context
-		 * window
-		 */
-		void updateSwapchain(const Context &context, const Window &window);
-		
-		/**
-		 *
-		 */
-        void signalSwapchainRecreation();
+		uint32_t getImageCount() const;
 	
         /**
          * TODO
@@ -118,4 +122,5 @@ namespace vkcv
 		const vk::Extent2D& getExtent() const;
     
     };
+    
 }
diff --git a/include/vkcv/VertexLayout.hpp b/include/vkcv/VertexLayout.hpp
index 9f609b48472386cd7628ff40b5fa4b90bc91649a..0600b99a24a327605e89b2e8ec304c20dbf7ad2e 100644
--- a/include/vkcv/VertexLayout.hpp
+++ b/include/vkcv/VertexLayout.hpp
@@ -63,4 +63,4 @@ namespace vkcv{
 
         std::vector<VertexBinding> vertexBindings;
     };
-}
\ No newline at end of file
+}
diff --git a/include/vkcv/Window.hpp b/include/vkcv/Window.hpp
index f11a38b797f9666815478b7596b63e6bfd0c0296..f3b3a8fe88ae6e8791d7d92361ad5b6bf2447dcb 100644
--- a/include/vkcv/Window.hpp
+++ b/include/vkcv/Window.hpp
@@ -14,16 +14,17 @@ struct GLFWwindow;
 
 namespace vkcv {
 
-    class Window final {
-    private:
-        GLFWwindow *m_window;
-
-        /**
+    class Window {
+	protected:
+		GLFWwindow *m_window;
+	
+		/**
          *
          * @param GLFWwindow of the class
          */
-        explicit Window(GLFWwindow *window);
-
+		explicit Window(GLFWwindow *window);
+		
+    private:
         /**
          * mouse callback for moving the mouse on the screen
          * @param[in] window The window that received the event.
@@ -40,6 +41,12 @@ namespace vkcv {
          */
         static void onMouseButtonEvent(GLFWwindow *callbackWindow, int button, int action, int mods);
 
+        /**
+         * @brief A callback function for handling mouse scrolling events.
+         * @param[in] callbackWindow The window that received the event.
+         * @param[in] xoffset The extent of horizontal scrolling.
+         * @param[in] yoffset The extent of vertical scrolling.
+         */
         static void onMouseScrollEvent(GLFWwindow *callbackWindow, double xoffset, double yoffset);
 
         /**
@@ -59,6 +66,19 @@ namespace vkcv {
          * @param[in] mods Bit field describing which [modifier keys](@ref mods) were held down.
          */
         static void onKeyEvent(GLFWwindow *callbackWindow, int key, int scancode, int action, int mods);
+	
+        /**
+         * char callback for any typed character
+         * @param[in] window The window that received the event
+         * @param[in] c The character that got typed
+         */
+		static void onCharEvent(GLFWwindow *callbackWindow, unsigned int c);
+
+        /**
+         * @brief A callback function for gamepad input events.
+         * @param gamepadIndex The gamepad index.
+         */
+        static void onGamepadEvent(int gamepadIndex);
 
     public:
         /**
@@ -78,11 +98,6 @@ namespace vkcv {
         [[nodiscard]]
         bool isWindowOpen() const;
 
-        /**
-         * binds windowEvents to lambda events
-         */
-        void initEvents();
-
         /**
          * polls all events on the GLFWwindow
          */
@@ -96,6 +111,8 @@ namespace vkcv {
         event< double, double > e_mouseScroll;
         event< int, int > e_resize;
         event< int, int, int, int > e_key;
+        event< unsigned int > e_char;
+        event< int > e_gamepad;
 
         /**
          * returns the current window
@@ -140,6 +157,13 @@ namespace vkcv {
          * Destructor of #Window, terminates GLFW
          */
         virtual ~Window();
+        
+        /**
+         * gets the windows framebuffer size
+         * @param width
+         * @param height
+         */
+        void getFramebufferSize(int& width, int& height) const;
     };
 
-}
\ No newline at end of file
+}
diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt
index f29ff2fc86c88aa8bae2560f199d3882c9919b65..28b2184b2a83515a514f1428733bcf8cf1499633 100644
--- a/modules/CMakeLists.txt
+++ b/modules/CMakeLists.txt
@@ -1,5 +1,8 @@
 
 # Add new modules here:
 add_subdirectory(asset_loader)
+add_subdirectory(material)
 add_subdirectory(camera)
+add_subdirectory(gui)
+add_subdirectory(shader_compiler)
 add_subdirectory(testing)
diff --git a/modules/asset_loader/CMakeLists.txt b/modules/asset_loader/CMakeLists.txt
index d2a9b817ea68c7851fd2123f76b378d8a4d85ac0..c5a1fd0eb9620d3a95af1c52a9e7456337047c7b 100644
--- a/modules/asset_loader/CMakeLists.txt
+++ b/modules/asset_loader/CMakeLists.txt
@@ -38,3 +38,5 @@ target_include_directories(vkcv_asset_loader SYSTEM BEFORE PRIVATE ${vkcv_asset_
 
 # add the own include directory for public headers
 target_include_directories(vkcv_asset_loader BEFORE PUBLIC ${vkcv_asset_loader_include})
+
+target_compile_definitions(vkcv_asset_loader PUBLIC ${vkcv_asset_loader_definitions})
diff --git a/modules/asset_loader/config/STB.cmake b/modules/asset_loader/config/STB.cmake
index da20d3ec07f98c865b4c6e38518f668b226cbfb9..1287d0a9ddda559e061ddd680bc815e24b3e2075 100644
--- a/modules/asset_loader/config/STB.cmake
+++ b/modules/asset_loader/config/STB.cmake
@@ -1,6 +1,10 @@
 
 if (EXISTS "${vkcv_asset_loader_lib_path}/stb")
 	list(APPEND vkcv_asset_loader_includes ${vkcv_asset_loader_lib}/stb)
+	
+	list(APPEND vkcv_asset_loader_definitions STB_IMAGE_IMPLEMENTATION)
+	list(APPEND vkcv_asset_loader_definitions STBI_ONLY_JPEG)
+	list(APPEND vkcv_asset_loader_definitions STBI_ONLY_PNG)
 else()
 	message(WARNING "STB is required..! Update the submodules!")
 endif ()
diff --git a/modules/asset_loader/include/vkcv/asset/asset_loader.hpp b/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
index d687bbf60a03a59eee8c29f9b676f10cc7f2f2b3..471870fb1e5af3d3c448a66611d9754db9597f85 100644
--- a/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
+++ b/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
@@ -1,15 +1,17 @@
 #pragma once
 /**
- * @authors Trevor Hollmann
+ * @authors Trevor Hollmann, Mara Vogt, Susanne Dötsch
  * @file include/vkcv/asset/asset_loader.h
  * @brief Interface of the asset loader module for the vkcv framework.
  */
 
 #include <string>
 #include <vector>
+#include <array>
 #include <cstdint>
+#include <filesystem>
 
-/* These macros define limits of the following structs. Implementations can
+/** These macros define limits of the following structs. Implementations can
  * test against these limits when performing sanity checks. The main constraint
  * expressed is that of the data type: Material indices are identified by a
  * uint8_t in the VertexGroup struct, so there can't be more than UINT8_MAX
@@ -19,17 +21,18 @@
 #define MAX_MATERIALS_PER_MESH UINT8_MAX
 #define MAX_VERTICES_PER_VERTEX_GROUP UINT32_MAX
 
-/* LOADING MESHES
+/** LOADING MESHES
  * The description of meshes is a hierarchy of structures with the Mesh at the
  * top.
  *
  * Each Mesh has an array of one or more vertex groups (called "primitives" in
- * glTF parlance) and an array of zero or more Materials.
+ * glTF parlance). Specifically, it has an array of indices into an array of
+ * vertex groups defined by the Scene struct.
  *
  * Each vertex group describes a part of the meshes vertices by defining how
  * they should be rendered (as points, lines, triangles), how many indices and
  * vertices there are, how the content of the vertex buffer is to be
- * interpreted and which material from the Meshes materials array should be
+ * interpreted and which material from the Scenes materials array should be
  * used for the surface of the vertices.
  * As a bonus there is also the axis aligned bounding box of the vertices.
  *
@@ -43,37 +46,100 @@
 
 namespace vkcv::asset {
 
-/* This enum matches modes in fx-gltf, the library returns a standard mode
+/** This enum matches modes in fx-gltf, the library returns a standard mode
  * (TRIANGLES) if no mode is given in the file. */
-enum PrimitiveMode {
+enum class PrimitiveMode : uint8_t {
 	POINTS=0, LINES, LINELOOP, LINESTRIP, TRIANGLES, TRIANGLESTRIP,
 	TRIANGLEFAN
 };
-/* The indices in the index buffer can be of different bit width. */
-enum IndexType { UINT32=0, UINT16=1, UINT8=2 };
 
-/* With these enums, 0 is reserved to signal uninitialized or invalid data. */
+/** The indices in the index buffer can be of different bit width. */
+enum class IndexType : uint8_t { UNDEFINED=0, UINT8=1, UINT16=2, UINT32=3 };
+
+typedef struct {
+	// TODO define struct for samplers (low priority)
+	// NOTE: glTF defines samplers based on OpenGL, which can not be
+	// directly translated to Vulkan. Specifically, OpenGL (and glTF)
+	// define a different set of Min/Mag-filters than Vulkan.
+} Sampler;
+
+/** struct for defining the loaded texture */
+typedef struct {
+	int sampler;		// index into the sampler array of the Scene
+	uint8_t channels;	// number of channels
+	uint16_t w, h;		// width and height of the texture
+	std::vector<uint8_t> data;	// binary data of the decoded texture
+} Texture;
+
+/** The asset loader module only supports the PBR-MetallicRoughness model for
+ * materials.*/
+typedef struct {
+	uint16_t textureMask;	// bit mask with active texture targets
+	// Indices into the Array.textures array
+	int baseColor, metalRough, normal, occlusion, emissive;
+	// Scaling factors for each texture target
+	struct { float r, g, b, a; } baseColorFactor;
+	float metallicFactor, roughnessFactor;
+	float normalScale;
+	float occlusionStrength;
+	struct { float r, g, b; } emissiveFactor;
+} Material;
+
+/** Flags for the bit-mask in the Material struct. To check if a material has a
+ * certain texture target, you can use the hasTexture() function below, passing
+ * the material struct and the enum. */
+enum class PBRTextureTarget {
+	baseColor=1, metalRough=2, normal=4, occlusion=8, emissive=16
+};
+
+/** This macro translates the index of an enum in the defined order to an
+ * integer with a single bit set in the corresponding place. It is used for
+ * working with the bitmask of texture targets ("textureMask") in the Material
+ * struct:
+ * 	Material mat = ...;
+ * 	if (mat.textureMask & bitflag(PBRTextureTarget::baseColor)) {...}
+ * However, this logic is also encapsulated in the convenience-function
+ * materialHasTexture() so users of the asset loader module can avoid direct
+ * contact with bit-level operations. */
+#define bitflag(ENUM) (0x1u << ((unsigned)(ENUM)))
+
+/** To signal that a certain texture target is active in a Material struct, its
+ * bit is set in the textureMask. You can use this function to check that:
+ * Material mat = ...;
+ * if (materialHasTexture(&mat, baseColor)) {...} */
+bool materialHasTexture(const Material *const m, const PBRTextureTarget t);
+
+/** With these enums, 0 is reserved to signal uninitialized or invalid data. */
 enum class PrimitiveType : uint32_t {
     UNDEFINED = 0,
     POSITION = 1,
     NORMAL = 2,
-    TEXCOORD_0 = 3
+    TEXCOORD_0 = 3,
+    TEXCOORD_1 = 4,
+    TANGENT = 5
 };
-/* This struct describes one vertex attribute of a vertex buffer. */
+
+/** These integer values are used the same way in OpenGL, Vulkan and glTF. This
+ * enum is not needed for translation, it's only for the programmers
+ * convenience (easier to read in if/switch statements etc). While this enum
+ * exists in (almost) the same definition in the fx-gltf library, we want to
+ * avoid exposing that dependency, thus it is re-defined here. */
+enum class ComponentType : uint16_t {
+    NONE = 0, INT8 = 5120, UINT8 = 5121, INT16 = 5122, UINT16 = 5123,
+    UINT32 = 5125, FLOAT32 = 5126
+};
+
+/** This struct describes one vertex attribute of a vertex buffer. */
 typedef struct {
     PrimitiveType type;			// POSITION, NORMAL, ...
     uint32_t offset;			// offset in bytes
     uint32_t length;			// length of ... in bytes
     uint32_t stride;			// stride in bytes
-    uint16_t componentType;		// eg. 5126 for float
+    ComponentType componentType;		// eg. 5126 for float
     uint8_t  componentCount;	// eg. 3 for vec3
 } VertexAttribute;
 
-typedef struct {
-	// TODO not yet needed for the first (unlit) triangle
-} Material;
-
-/* This struct represents one (possibly the only) part of a mesh. There is
+/** This struct represents one (possibly the only) part of a mesh. There is
  * always one vertexBuffer and zero or one indexBuffer (indexed rendering is
  * common but not always used). If there is no index buffer, this is indicated
  * by indexBuffer.data being empty. Each vertex buffer can have one or more
@@ -91,34 +157,46 @@ typedef struct {
 	} vertexBuffer;
 	struct { float x, y, z; } min;	// bounding box lower left
 	struct { float x, y, z; } max;	// bounding box upper right
-	uint8_t materialIndex;		// index to one of the meshes materials
+	int materialIndex;		// index to one of the materials
 } VertexGroup;
 
-/* This struct represents a single mesh loaded from a glTF file. It consists of
- * at least one VertexVroup and any number of Materials. */
+/** This struct represents a single mesh as it was loaded from a glTF file. It
+ * consists of at least one VertexGroup, which then references other resources
+ * such as Materials. */
 typedef struct {
 	std::string name;
-	std::vector<VertexGroup> vertexGroups;
-	std::vector<Material> materials;
-	// FIXME Dirty hack to get one(!) texture for our cube demo
-	// hardcoded to always have RGBA channel layout
-	struct {
-		int w, h, ch;	// width, height and channels of image
-		uint8_t *img;	// raw bytes, free after use (deal with it)
-	} texture_hack;
+	std::array<float, 16> modelMatrix;
+	std::vector<int> vertexGroups;
 } Mesh;
 
+/** The scene struct is simply a collection of objects in the scene as well as
+ * the resources used by those objects.
+ * For now the only type of object are the meshes and they are represented in a
+ * flat array.
+ * Note that parent-child relations are not yet possible. */
+typedef struct {
+	std::vector<Mesh> meshes;
+	std::vector<VertexGroup> vertexGroups;
+	std::vector<Material> materials;
+	std::vector<Texture> textures;
+	std::vector<Sampler> samplers;
+} Scene;
 
 /**
- * In its first iteration the asset loader module will only allow loading
- * single meshes, one per glTF file.
- * It will later be extended to allow loading entire scenes from glTF files.
+ * Load every mesh from the glTF file, as well as materials and textures.
  *
- * @param path must be the path to a glTF file containing a single mesh.
- * @param mesh is a reference to a Mesh struct that will be filled with the
+ * @param path must be the path to a glTF or glb file.
+ * @param scene is a reference to a Scene struct that will be filled with the
  * 	content of the glTF file being loaded.
  * */
-int loadMesh(const std::string &path, Mesh &mesh);
+int loadScene(const std::string &path, Scene &scene);
 
+struct TextureData {
+    int width;
+    int height;
+    int componentCount;
+    std::vector<char*> data;
+};
+TextureData loadTexture(const std::filesystem::path& path);
 
 }
diff --git a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
index f3823cc8f3fe54b53835f356dd14a086515118dd..e3d3072543bd33e1f5a67ae7dac61d229005947a 100644
--- a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
+++ b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
@@ -3,11 +3,9 @@
 #include <string.h>	// memcpy(3)
 #include <stdlib.h>	// calloc(3)
 #include <fx/gltf.h>
-#define STB_IMAGE_IMPLEMENTATION
-#define STBI_ONLY_JPEG
 #include <stb_image.h>
-
 #include <vkcv/Logger.hpp>
+#include <algorithm>
 
 namespace vkcv::asset {
 
@@ -51,160 +49,339 @@ void print_what (const std::exception& e, const std::string &path) {
 	}
 }
 
-int loadMesh(const std::string &path, Mesh &mesh) {
-	fx::gltf::Document object;
-
-	try {
-		if (path.rfind(".glb", (path.length()-4)) != std::string::npos) {
-			object = fx::gltf::LoadFromBinary(path);
-		} else {
-			object = fx::gltf::LoadFromText(path);
-		}
-	} catch (const std::system_error &err) {
-		print_what(err, path);
-		return 0;
-	} catch (const std::exception &e) {
-		print_what(e, path);
-		return 0;
-	}
-
-	// TODO Temporary restriction: Only one mesh per glTF file allowed
-	// currently. Later, we want to support whole scenes with more than
-	// just meshes.
-	if (object.meshes.size() != 1) return 0;
-
-	fx::gltf::Mesh const &objectMesh = object.meshes[0];
-	// TODO We want to support more than one vertex group per mesh
-	// eventually... right now this is hard-coded to use only the first one
-	// because we only care about the example triangle and cube
-	fx::gltf::Primitive const &objectPrimitive = objectMesh.primitives[0];
-	fx::gltf::Accessor posAccessor;
-	
-	std::vector<VertexAttribute> vertexAttributes;
-	vertexAttributes.reserve(objectPrimitive.attributes.size());
-	
-	for (auto const & attrib : objectPrimitive.attributes) {
-		fx::gltf::Accessor accessor =  object.accessors[attrib.second];
-		VertexAttribute attribute;
-
-		if (attrib.first == "POSITION") {
-			attribute.type = PrimitiveType::POSITION;
-			posAccessor = accessor;
-		} else if (attrib.first == "NORMAL") {
-			attribute.type = PrimitiveType::NORMAL;
-		} else if (attrib.first == "TEXCOORD_0") {
-			attribute.type = PrimitiveType::TEXCOORD_0;
-		} else {
-			return 0;
-		}
-		
-		attribute.offset = object.bufferViews[accessor.bufferView].byteOffset;
-		attribute.length = object.bufferViews[accessor.bufferView].byteLength;
-		attribute.stride = object.bufferViews[accessor.bufferView].byteStride;
-		attribute.componentType = static_cast<uint16_t>(accessor.componentType);
-		
-		if (convertTypeToInt(accessor.type) != 10) {
-			attribute.componentCount = convertTypeToInt(accessor.type);
-		} else {
-			return 0;
-		}
-		
-		vertexAttributes.push_back(attribute);
-	}
-
-	// TODO consider the case where there is no index buffer (not all
-	// meshes have to use indexed rendering)
-	const fx::gltf::Accessor &indexAccessor = object.accessors[objectPrimitive.indices];
-	const fx::gltf::BufferView &indexBufferView = object.bufferViews[indexAccessor.bufferView];
-	const fx::gltf::Buffer &indexBuffer = object.buffers[indexBufferView.buffer];
-	
-	std::vector<uint8_t> indexBufferData;
-	indexBufferData.resize(indexBufferView.byteLength);
-	{
-		const size_t off = indexBufferView.byteOffset;
-		const void *const ptr = ((char*)indexBuffer.data.data()) + off;
-		if (!memcpy(indexBufferData.data(), ptr, indexBufferView.byteLength)) {
-			vkcv_log(LogLevel::ERROR, "Copying index buffer data");
-			return 0;
-		}
-	}
-
-	const fx::gltf::BufferView&	vertexBufferView	= object.bufferViews[posAccessor.bufferView];
-	const fx::gltf::Buffer&		vertexBuffer		= object.buffers[vertexBufferView.buffer];
-	
-	// FIXME: This only works when all vertex attributes are in one buffer
-	std::vector<uint8_t> vertexBufferData;
-	vertexBufferData.resize(vertexBuffer.byteLength);
-	{
-		const size_t off = 0;
-		const void *const ptr = ((char*)vertexBuffer.data.data()) + off;
-		if (!memcpy(vertexBufferData.data(), ptr, vertexBuffer.byteLength)) {
-			vkcv_log(LogLevel::ERROR, "Copying vertex buffer data");
-			return 0;
-		}
-	}
-
-	IndexType indexType;
-	switch(indexAccessor.componentType) {
+/** Translate the component type used in the index accessor of fx-gltf to our
+ * enum for index type. The reason we have defined an incompatible enum that
+ * needs translation is that only a subset of component types is valid for
+ * indices and we want to catch these incompatibilities here. */
+enum IndexType getIndexType(const enum fx::gltf::Accessor::ComponentType &t)
+{
+	switch (t) {
 	case fx::gltf::Accessor::ComponentType::UnsignedByte:
-		indexType = UINT8; break;
+		return IndexType::UINT8;
 	case fx::gltf::Accessor::ComponentType::UnsignedShort:
-		indexType = UINT16; break;
+		return IndexType::UINT16;
 	case fx::gltf::Accessor::ComponentType::UnsignedInt:
-		indexType = UINT32; break;
+		return IndexType::UINT32;
 	default:
-		vkcv_log(LogLevel::ERROR, "Index type (%u) not supported",
-				 static_cast<uint16_t>(indexAccessor.componentType));
-		return 0;
+		vkcv_log(LogLevel::ERROR, "Index type not supported: %u", static_cast<uint16_t>(t));
+		return IndexType::UNDEFINED;
 	}
+}
 
-	const size_t numVertexGroups = objectMesh.primitives.size();
-	
-	std::vector<VertexGroup> vertexGroups;
-	vertexGroups.reserve(numVertexGroups);
-	
-	vertexGroups.push_back({
-		static_cast<PrimitiveMode>(objectPrimitive.mode),
-		object.accessors[objectPrimitive.indices].count,
-		posAccessor.count,
-		{indexType, indexBufferData},
-		{vertexBufferData, vertexAttributes},
-		{posAccessor.min[0], posAccessor.min[1], posAccessor.min[2]},
-		{posAccessor.max[0], posAccessor.max[1], posAccessor.max[2]},
-		static_cast<uint8_t>(objectPrimitive.material)
-	});
-	
-	std::vector<Material> materials;
-
-	mesh = {
-		object.meshes[0].name,
-		vertexGroups,
-		materials,
-		0, 0, 0, NULL
-	};
-
-	// FIXME HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
-	// fail quietly if there is no texture
-	if (object.textures.size()) {
-		const std::string mime_type("image/jpeg");
-		const fx::gltf::Texture &tex = object.textures[0];
-		const fx::gltf::Image &img = object.images[tex.source];
-#ifndef NDEBUG
-		printf("texture name=%s sampler=%u source=%u\n",
-				tex.name.c_str(), tex.sampler, tex.source);
-		printf("image   name=%s uri=%s mime=%s\n", img.name.c_str(),
-				img.uri.c_str(), img.mimeType.c_str());
-#endif
-		
-		size_t pos = path.find_last_of("/");
-		auto dir = path.substr(0, pos);
-		
-		mesh.texture_hack.img = stbi_load((dir + "/" + img.uri).c_str(),
-				&mesh.texture_hack.w, &mesh.texture_hack.h,
-				&mesh.texture_hack.ch, 4);
-	}
-	// FIXME HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
-	return 1;
+/**
+ * This function computes the modelMatrix out of the data given in the gltf file. It also checks, whether a modelMatrix was given.
+ * @param translation possible translation vector (default 0,0,0)
+ * @param scale possible scale vector (default 1,1,1)
+ * @param rotation possible rotation, given in quaternion (default 0,0,0,1)
+ * @param matrix possible modelmatrix (default identity)
+ * @return model Matrix as an array of floats
+ */
+std::array<float, 16> computeModelMatrix(std::array<float, 3> translation, std::array<float, 3> scale, std::array<float, 4> rotation, std::array<float, 16> matrix){
+    std::array<float, 16> modelMatrix = {1,0,0,0,
+                                         0,1,0,0,
+                                         0,0,1,0,
+                                         0,0,0,1};
+    if (matrix != modelMatrix){
+        return matrix;
+    } else {
+        // translation
+        modelMatrix[3] = translation[0];
+        modelMatrix[7] = translation[1];
+        modelMatrix[11] = translation[2];
+        // 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;
+
+        return modelMatrix;
+    }
+
+}
+
+bool materialHasTexture(const Material *const m, const PBRTextureTarget t)
+{
+	return m->textureMask & bitflag(t);
+}
+
+int loadScene(const std::string &path, Scene &scene){
+    fx::gltf::Document sceneObjects;
+
+    try {
+        if (path.rfind(".glb", (path.length()-4)) != std::string::npos) {
+            sceneObjects = fx::gltf::LoadFromBinary(path);
+        } else {
+            sceneObjects = fx::gltf::LoadFromText(path);
+        }
+    } catch (const std::system_error &err) {
+        print_what(err, path);
+        return 0;
+    } catch (const std::exception &e) {
+        print_what(e, path);
+        return 0;
+    }
+    size_t pos = path.find_last_of("/");
+    auto dir = path.substr(0, pos);
+
+    // file has to contain at least one mesh
+    if (sceneObjects.meshes.size() == 0) return 0;
+
+    fx::gltf::Accessor posAccessor;
+    std::vector<VertexAttribute> vertexAttributes;
+    std::vector<Material> materials;
+    std::vector<Texture> textures;
+    std::vector<Sampler> samplers;
+    std::vector<Mesh> meshes;
+    std::vector<VertexGroup> vertexGroups;
+    int groupCount = 0;
+
+    Mesh mesh = {};
+
+    for(int i = 0; i < sceneObjects.meshes.size(); i++){
+        std::vector<int> vertexGroupsIndices;
+        fx::gltf::Mesh const &objectMesh = sceneObjects.meshes[i];
+
+        for(int j = 0; j < objectMesh.primitives.size(); j++){
+            fx::gltf::Primitive const &objectPrimitive = objectMesh.primitives[j];
+            vertexAttributes.clear();
+            vertexAttributes.reserve(objectPrimitive.attributes.size());
+
+            for (auto const & attrib : objectPrimitive.attributes) {
+
+                fx::gltf::Accessor accessor =  sceneObjects.accessors[attrib.second];
+                VertexAttribute attribute;
+
+                if (attrib.first == "POSITION") {
+                    attribute.type = PrimitiveType::POSITION;
+                    posAccessor = accessor;
+                } else if (attrib.first == "NORMAL") {
+                    attribute.type = PrimitiveType::NORMAL;
+                } else if (attrib.first == "TEXCOORD_0") {
+                    attribute.type = PrimitiveType::TEXCOORD_0;
+                }
+                else if (attrib.first == "TEXCOORD_1") {
+                    attribute.type = PrimitiveType::TEXCOORD_1;
+                } else if (attrib.first == "TANGENT") {
+                    attribute.type = PrimitiveType::TANGENT;
+                } else {
+                    return 0;
+                }
+
+                attribute.offset = sceneObjects.bufferViews[accessor.bufferView].byteOffset;
+                attribute.length = sceneObjects.bufferViews[accessor.bufferView].byteLength;
+                attribute.stride = sceneObjects.bufferViews[accessor.bufferView].byteStride;
+		        attribute.componentType = static_cast<ComponentType>(accessor.componentType);
+
+                if (convertTypeToInt(accessor.type) != 10) {
+                    attribute.componentCount = convertTypeToInt(accessor.type);
+                } else {
+                    return 0;
+                }
+
+                vertexAttributes.push_back(attribute);
+            }
+
+            IndexType indexType;
+            std::vector<uint8_t> indexBufferData = {};
+            if (objectPrimitive.indices >= 0){ // if there is no index buffer, -1 is returned from fx-gltf
+                const fx::gltf::Accessor &indexAccessor = sceneObjects.accessors[objectPrimitive.indices];
+                const fx::gltf::BufferView &indexBufferView = sceneObjects.bufferViews[indexAccessor.bufferView];
+                const fx::gltf::Buffer &indexBuffer = sceneObjects.buffers[indexBufferView.buffer];
+
+                indexBufferData.resize(indexBufferView.byteLength);
+                {
+                    const size_t off = indexBufferView.byteOffset;
+                    const void *const ptr = ((char*)indexBuffer.data.data()) + off;
+                    if (!memcpy(indexBufferData.data(), ptr, indexBufferView.byteLength)) {
+                        vkcv_log(LogLevel::ERROR, "Copying index buffer data");
+                        return 0;
+                    }
+                }
+
+                indexType = getIndexType(indexAccessor.componentType);
+                if (indexType == IndexType::UNDEFINED){
+                    vkcv_log(LogLevel::ERROR, "Index Type undefined.");
+                    return 0;
+                }
+            }
+
+            const fx::gltf::BufferView&	vertexBufferView	= sceneObjects.bufferViews[posAccessor.bufferView];
+            const fx::gltf::Buffer&		vertexBuffer		= sceneObjects.buffers[vertexBufferView.buffer];
+
+            // only copy relevant part of vertex data
+            uint32_t relevantBufferOffset = std::numeric_limits<uint32_t>::max();
+            uint32_t relevantBufferEnd = 0;
+            for (const auto &attribute : vertexAttributes) {
+                relevantBufferOffset = std::min(attribute.offset, relevantBufferOffset);
+                const uint32_t attributeEnd = attribute.offset + attribute.length;
+                relevantBufferEnd = std::max(relevantBufferEnd, attributeEnd);    // TODO: need to incorporate stride?
+            }
+            const uint32_t relevantBufferSize = relevantBufferEnd - relevantBufferOffset;
+
+            // FIXME: This only works when all vertex attributes are in one buffer
+            std::vector<uint8_t> vertexBufferData;
+            vertexBufferData.resize(relevantBufferSize);
+            {
+                const void *const ptr = ((char*)vertexBuffer.data.data()) + relevantBufferOffset;
+                if (!memcpy(vertexBufferData.data(), ptr, relevantBufferSize)) {
+                    vkcv_log(LogLevel::ERROR, "Copying vertex buffer data");
+                    return 0;
+                }
+            }
+
+            // make vertex attributes relative to copied section
+            for (auto &attribute : vertexAttributes) {
+                attribute.offset -= relevantBufferOffset;
+            }
+
+            const size_t numVertexGroups = objectMesh.primitives.size();
+            vertexGroups.reserve(numVertexGroups);
+
+            vertexGroups.push_back({
+                static_cast<PrimitiveMode>(objectPrimitive.mode),
+                sceneObjects.accessors[objectPrimitive.indices].count,
+                posAccessor.count,
+                {indexType, indexBufferData},
+                {vertexBufferData, vertexAttributes},
+                {posAccessor.min[0], posAccessor.min[1], posAccessor.min[2]},
+                {posAccessor.max[0], posAccessor.max[1], posAccessor.max[2]},
+                static_cast<uint8_t>(objectPrimitive.material)
+            });
+
+            vertexGroupsIndices.push_back(groupCount);
+            groupCount++;
+        }
+
+        mesh.name = sceneObjects.meshes[i].name;
+        mesh.vertexGroups = vertexGroupsIndices;
+
+        meshes.push_back(mesh);
+    }
+
+    for(int m = 0; m < sceneObjects.nodes.size(); m++) {
+        meshes[sceneObjects.nodes[m].mesh].modelMatrix = computeModelMatrix(sceneObjects.nodes[m].translation,
+                                                                            sceneObjects.nodes[m].scale,
+                                                                            sceneObjects.nodes[m].rotation,
+                                                                            sceneObjects.nodes[m].matrix);
+    }
+
+    if (sceneObjects.textures.size() > 0){
+        textures.reserve(sceneObjects.textures.size());
+
+        for(int k = 0; k < sceneObjects.textures.size(); k++){
+            const fx::gltf::Texture &tex = sceneObjects.textures[k];
+            const fx::gltf::Image &img = sceneObjects.images[tex.source];
+            std::string img_uri = dir + "/" + img.uri;
+            int w, h, c;
+            uint8_t *data = stbi_load(img_uri.c_str(), &w, &h, &c, 4);
+            c = 4;	// FIXME hardcoded to always have RGBA channel layout
+            if (!data) {
+                vkcv_log(LogLevel::ERROR, "Loading texture image data.")
+                return 0;
+            }
+            const size_t byteLen = w * h * c;
+
+            std::vector<uint8_t> imgdata;
+            imgdata.resize(byteLen);
+            if (!memcpy(imgdata.data(), data, byteLen)) {
+                vkcv_log(LogLevel::ERROR, "Copying texture image data")
+                free(data);
+                return 0;
+            }
+            free(data);
+
+            textures.push_back({
+                0,
+                static_cast<uint8_t>(c),
+                static_cast<uint16_t>(w),
+                static_cast<uint16_t>(h),
+                imgdata
+            });
+
+        }
+    }
+
+    if (sceneObjects.materials.size() > 0){
+        materials.reserve(sceneObjects.materials.size());
+
+        for (int l = 0; l < sceneObjects.materials.size(); l++){
+            fx::gltf::Material material = sceneObjects.materials[l];
+	    // TODO I think we shouldn't set the index for a texture target if
+	    // it isn't defined. So we need to test first if there is a normal
+	    // texture before assigning material.normalTexture.index.
+	    // About the bitmask: If a normal texture is there, modify the
+	    // materials textureMask like this:
+	    // 		mat.textureMask |= bitflag(asset::normal);
+            materials.push_back({
+               0,
+               material.pbrMetallicRoughness.baseColorTexture.index,
+               material.pbrMetallicRoughness.metallicRoughnessTexture.index,
+               material.normalTexture.index,
+               material.occlusionTexture.index,
+               material.emissiveTexture.index,
+               {
+                   material.pbrMetallicRoughness.baseColorFactor[0],
+                   material.pbrMetallicRoughness.baseColorFactor[1],
+                   material.pbrMetallicRoughness.baseColorFactor[2],
+                   material.pbrMetallicRoughness.baseColorFactor[3]
+               },
+               material.pbrMetallicRoughness.metallicFactor,
+               material.pbrMetallicRoughness.roughnessFactor,
+               material.normalTexture.scale,
+               material.occlusionTexture.strength,
+               {
+                   material.emissiveFactor[0],
+                   material.emissiveFactor[1],
+                   material.emissiveFactor[2]
+               }
+
+            });
+        }
+    }
+
+    scene = {
+            meshes,
+            vertexGroups,
+            materials,
+            textures,
+            samplers
+    };
+
+    return 1;
+}
+
+TextureData loadTexture(const std::filesystem::path& path) {
+    TextureData texture;
+    
+    uint8_t* data = stbi_load(path.string().c_str(), &texture.width, &texture.height, &texture.componentCount, 4);
+    
+    if (!data) {
+		vkcv_log(LogLevel::ERROR, "Texture could not be loaded from '%s'", path.c_str());
+    	
+    	texture.width = 0;
+    	texture.height = 0;
+    	texture.componentCount = 0;
+    	return texture;
+    }
+    
+    texture.data.resize(texture.width * texture.height * 4);
+    memcpy(texture.data.data(), data, texture.data.size());
+    return texture;
 }
 
 }
diff --git a/modules/camera/CMakeLists.txt b/modules/camera/CMakeLists.txt
index 73f2dd1c81be9c6cadf563f7936bfaba8c1d0025..60cfca4cf97cef30d989bdab064e20547764041c 100644
--- a/modules/camera/CMakeLists.txt
+++ b/modules/camera/CMakeLists.txt
@@ -36,3 +36,4 @@ target_include_directories(vkcv_camera SYSTEM BEFORE PRIVATE ${vkcv_camera_inclu
 # add the own include directory for public headers
 target_include_directories(vkcv_camera BEFORE PUBLIC ${vkcv_camera_include} ${vkcv_camera_includes})
 
+target_compile_definitions(vkcv_camera PUBLIC ${vkcv_camera_definitions})
diff --git a/modules/camera/config/GLM.cmake b/modules/camera/config/GLM.cmake
index c4d14392ef0ea24243a45b19cd8583d90d3267be..efd6444451100b912aa0b5b4a532dc8f448b0b40 100644
--- a/modules/camera/config/GLM.cmake
+++ b/modules/camera/config/GLM.cmake
@@ -4,11 +4,17 @@ find_package(glm QUIET)
 if (glm_FOUND)
     list(APPEND vkcv_camera_includes ${GLM_INCLUDE_DIRS})
     list(APPEND vkcv_camera_libraries glm)
+
+    list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
+    list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
 else()
     if (EXISTS "${vkcv_camera_lib_path}/glm")
         add_subdirectory(${vkcv_camera_lib}/glm)
         
         list(APPEND vkcv_camera_libraries glm)
+        
+        list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
+        list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
     else()
         message(WARNING "GLM is required..! Update the submodules!")
     endif ()
diff --git a/modules/camera/include/vkcv/camera/Camera.hpp b/modules/camera/include/vkcv/camera/Camera.hpp
index dc9f2dcb3038655f51fb2404abc21f98a2120399..9d85df7dce6d043630fd9d39287cace8530dbd6a 100644
--- a/modules/camera/include/vkcv/camera/Camera.hpp
+++ b/modules/camera/include/vkcv/camera/Camera.hpp
@@ -1,7 +1,5 @@
 #pragma once
 
-#define GLM_DEPTH_ZERO_TO_ONE
-#define GLM_FORCE_LEFT_HANDED
 #include <glm/glm.hpp>
 #include <glm/gtc/matrix_transform.hpp>
 #include <glm/gtc/matrix_access.hpp>
@@ -77,7 +75,7 @@ namespace vkcv::camera {
          * @brief Gets the current projection of the camera
          * @return The current projection matrix
          */
-        const glm::mat4& getProjection() const;
+        glm::mat4 getProjection() const;
 
         /**
          * @brief Gets the model-view-projection matrix of the camera with y-axis-correction applied
diff --git a/modules/camera/include/vkcv/camera/CameraController.hpp b/modules/camera/include/vkcv/camera/CameraController.hpp
index 5fe7aba586068beff15525617d8e4817662746b7..90fc97401851851194ec89a10757bbfb1453990d 100644
--- a/modules/camera/include/vkcv/camera/CameraController.hpp
+++ b/modules/camera/include/vkcv/camera/CameraController.hpp
@@ -59,6 +59,14 @@ namespace vkcv::camera {
          * @param[in] camera The camera object.
          */
         virtual void mouseButtonCallback(int button, int action, int mods, Camera &camera) = 0;
+
+        /**
+         * @brief A callback function for gamepad input events.
+         * @param gamepadIndex The gamepad index.
+         * @param camera The camera object.
+         * @param frametime The current frametime.
+         */
+        virtual void gamepadCallback(int gamepadIndex, Camera &camera, double frametime) = 0;
     };
 
 }
\ No newline at end of file
diff --git a/modules/camera/include/vkcv/camera/CameraManager.hpp b/modules/camera/include/vkcv/camera/CameraManager.hpp
index 69c4b311a561b9f3ce27e3d9906d68f21e2334ac..409f9196599be02e4215f3924c1102f0b8c72899 100644
--- a/modules/camera/include/vkcv/camera/CameraManager.hpp
+++ b/modules/camera/include/vkcv/camera/CameraManager.hpp
@@ -25,11 +25,12 @@ namespace vkcv::camera {
      */
     class CameraManager{
     private:
-        std::function<void(int, int, int, int)> m_keyHandle;
-        std::function<void(double, double)> m_mouseMoveHandle;
-        std::function<void(double, double)> m_mouseScrollHandle;
-        std::function<void(int, int, int)> m_mouseButtonHandle;
-        std::function<void(int, int)> m_resizeHandle;
+		event_handle<int, int, int, int> m_keyHandle;
+		event_handle<double, double> m_mouseMoveHandle;
+		event_handle<double, double> m_mouseScrollHandle;
+		event_handle<int, int, int> m_mouseButtonHandle;
+		event_handle<int, int> m_resizeHandle;
+        event_handle<int> m_gamepadHandle;
 
         Window& m_window;
         std::vector<Camera> m_cameras;
@@ -42,6 +43,9 @@ namespace vkcv::camera {
         double m_lastX;
         double m_lastY;
 
+        double m_inputDelayTimer;
+        double m_frameTime;
+
         /**
          * @brief Binds the camera object to the window event handles.
          */
@@ -86,6 +90,13 @@ namespace vkcv::camera {
          * @param[in] height The new height of the window.
          */
         void resizeCallback(int width, int height);
+
+        /**
+         * @brief A callback function for gamepad input events. Currently, inputs are handled only for the first
+         * connected gamepad!
+         * @param gamepadIndex The gamepad index.
+         */
+        void gamepadCallback(int gamepadIndex);
 	
 		/**
 		 * @brief Gets a camera controller object of specified @p controllerType.
@@ -105,10 +116,8 @@ namespace vkcv::camera {
         /**
          * @brief The constructor of the #CameraManager.
          * @param[in] window The window.
-         * @param[in] width The width of the window.
-         * @param[in] height The height of the window.
          */
-        CameraManager(Window &window, float width, float height);
+        CameraManager(Window &window);
 
         /**
          * @brief The destructor of the #CameraManager. Destroying the #CameraManager leads to deletion of all stored
diff --git a/modules/camera/include/vkcv/camera/PilotCameraController.hpp b/modules/camera/include/vkcv/camera/PilotCameraController.hpp
index c6a9f7c7ffa9a3be77f12c29e456291fb8f6b845..2b64cdc0dd3045714aba7b3b7c6241af2337c706 100644
--- a/modules/camera/include/vkcv/camera/PilotCameraController.hpp
+++ b/modules/camera/include/vkcv/camera/PilotCameraController.hpp
@@ -17,6 +17,10 @@ namespace vkcv::camera {
         bool m_left;
         bool m_right;
 
+        float m_gamepadX;
+        float m_gamepadY;
+        float m_gamepadZ;
+
         bool m_rotationActive;
 
         float m_cameraSpeed;
@@ -133,6 +137,14 @@ namespace vkcv::camera {
          * @param[in] camera The camera object.
          */
         void mouseButtonCallback(int button, int action, int mods, Camera &camera);
+
+        /**
+         * @brief A callback function for gamepad input events.
+         * @param gamepadIndex The gamepad index.
+         * @param camera The camera object.
+         * @param frametime The current frametime.
+         */
+        void gamepadCallback(int gamepadIndex, Camera &camera, double frametime);
     };
 
 }
\ No newline at end of file
diff --git a/modules/camera/include/vkcv/camera/TrackballCameraController.hpp b/modules/camera/include/vkcv/camera/TrackballCameraController.hpp
index 0211043a9c6b862df8e500af190ad1f75a3c78aa..4166bda9f6cb62e4c8f1b650557b00c6ec94b2a1 100644
--- a/modules/camera/include/vkcv/camera/TrackballCameraController.hpp
+++ b/modules/camera/include/vkcv/camera/TrackballCameraController.hpp
@@ -95,6 +95,13 @@ namespace vkcv::camera {
          */
         void mouseButtonCallback(int button, int action, int mods, Camera &camera);
 
+        /**
+         * @brief A callback function for gamepad input events.
+         * @param gamepadIndex The gamepad index.
+         * @param camera The camera object.
+         * @param frametime The current frametime.
+         */
+        void gamepadCallback(int gamepadIndex, Camera &camera, double frametime);
     };
 
 }
\ No newline at end of file
diff --git a/modules/camera/src/vkcv/camera/Camera.cpp b/modules/camera/src/vkcv/camera/Camera.cpp
index eb1857968b2284287691c6ed41ba168db30d3f84..3541b1a5bc1253c6b0f2b044d757341855a5e900 100644
--- a/modules/camera/src/vkcv/camera/Camera.cpp
+++ b/modules/camera/src/vkcv/camera/Camera.cpp
@@ -1,6 +1,5 @@
 #include "vkcv/camera/Camera.hpp"
 
-#define _USE_MATH_DEFINES
 #include <math.h>
 
 namespace vkcv::camera {
@@ -38,22 +37,22 @@ namespace vkcv::camera {
 		m_view = view;
 	}
 
-    const glm::mat4& Camera::getProjection() const {
-        return m_projection;
+    const glm::mat4 y_correction(
+        1.0f, 0.0f, 0.0f, 0.0f,
+        0.0f, -1.0f, 0.0f, 0.0f,
+        0.0f, 0.0f, 1.0f, 0.0f,
+        0.0f, 0.0f, 0.0f, 1.0f
+    );
+
+    glm::mat4 Camera::getProjection() const {
+        return y_correction * m_projection;
     }
 
     void Camera::setProjection(const glm::mat4& projection) {
-        m_projection =  projection;
+        m_projection = glm::inverse(y_correction) * projection;
     }
 
     glm::mat4 Camera::getMVP() const {
-		const glm::mat4 y_correction (
-				1.0f,  0.0f,  0.0f,  0.0f,
-				0.0f, -1.0f,  0.0f,  0.0f,
-				0.0f,  0.0f,  1.0f,  0.0f,
-				0.0f,  0.0f,  0.0f,  1.0f
-		);
-    	
         return y_correction * m_projection * m_view;
     }
 
diff --git a/modules/camera/src/vkcv/camera/CameraManager.cpp b/modules/camera/src/vkcv/camera/CameraManager.cpp
index 977c61d03dac51598281262acf9609540063a9e4..f129f3a248325957cb56470e2547a0146bc7c971 100644
--- a/modules/camera/src/vkcv/camera/CameraManager.cpp
+++ b/modules/camera/src/vkcv/camera/CameraManager.cpp
@@ -1,20 +1,28 @@
 
 #include "vkcv/camera/CameraManager.hpp"
-
 #include <vkcv/Logger.hpp>
 
 namespace vkcv::camera {
 
-    CameraManager::CameraManager(Window &window, float width, float height)
+    CameraManager::CameraManager(Window& window)
     : m_window(window)
     {
         bindCameraToEvents();
         m_activeCameraIndex = 0;
         m_lastX = static_cast<float>(window.getWidth()) / 2.0f;
         m_lastY = static_cast<float>(window.getHeight()) / 2.0f;
+        m_inputDelayTimer = glfwGetTime() + 0.2;
+        m_frameTime = 0;
     }
 
-    CameraManager::~CameraManager() {}
+    CameraManager::~CameraManager() {
+    	m_window.e_key.remove(m_keyHandle);
+		m_window.e_mouseMove.remove(m_mouseMoveHandle);
+		m_window.e_mouseScroll.remove(m_mouseScrollHandle);
+		m_window.e_mouseButton.remove(m_mouseButtonHandle);
+		m_window.e_resize.remove(m_resizeHandle);
+		m_window.e_gamepad.remove(m_gamepadHandle);
+    }
 
     void CameraManager::bindCameraToEvents() {
         m_keyHandle = m_window.e_key.add( [&](int key, int scancode, int action, int mods) { this->keyCallback(key, scancode, action, mods); });
@@ -22,11 +30,14 @@ namespace vkcv::camera {
         m_mouseScrollHandle =  m_window.e_mouseScroll.add([&](double offsetX, double offsetY) {this->scrollCallback( offsetX, offsetY);} );
         m_mouseButtonHandle = m_window.e_mouseButton.add([&] (int button, int action, int mods) {this->mouseButtonCallback( button,  action,  mods);});
         m_resizeHandle = m_window.e_resize.add([&](int width, int height) {this->resizeCallback(width, height);});
+        m_gamepadHandle = m_window.e_gamepad.add([&](int gamepadIndex) {this->gamepadCallback(gamepadIndex);});
     }
 
     void CameraManager::resizeCallback(int width, int height) {
-        for (size_t i = 0; i < m_cameras.size(); i++) {
-            getCamera(i).setRatio(static_cast<float>(width) / static_cast<float>(height));;
+        if (glfwGetWindowAttrib(m_window.getWindow(), GLFW_ICONIFIED) == GLFW_FALSE) {
+            for (size_t i = 0; i < m_cameras.size(); i++) {
+                getCamera(i).setRatio(static_cast<float>(width) / static_cast<float>(height));;
+            }
         }
     }
 
@@ -75,15 +86,39 @@ namespace vkcv::camera {
                 break;
         }
     }
-    
+
+    void CameraManager::gamepadCallback(int gamepadIndex) {
+        // handle camera switching
+        GLFWgamepadstate gamepadState;
+        glfwGetGamepadState(gamepadIndex, &gamepadState);
+
+        double time = glfwGetTime();
+        if (time - m_inputDelayTimer > 0.2) {
+            int switchDirection = gamepadState.buttons[GLFW_GAMEPAD_BUTTON_DPAD_RIGHT] - gamepadState.buttons[GLFW_GAMEPAD_BUTTON_DPAD_LEFT];
+            m_activeCameraIndex += switchDirection;
+            if (std::greater<int>{}(m_activeCameraIndex, m_cameras.size() - 1)) {
+                m_activeCameraIndex = 0;
+            }
+            else if (std::less<int>{}(m_activeCameraIndex, 0)) {
+                m_activeCameraIndex = m_cameras.size() - 1;
+            }
+            uint32_t triggered = abs(switchDirection);
+            m_inputDelayTimer = (1-triggered)*m_inputDelayTimer + triggered * time; // Only reset timer, if dpad was pressed - is this cheaper than if-clause?
+        }
+
+        getActiveController().gamepadCallback(gamepadIndex, getActiveCamera(), m_frameTime);     // handle camera rotation, translation
+    }
+
     CameraController& CameraManager::getActiveController() {
     	const ControllerType type = getControllerType(getActiveCameraIndex());
     	return getControllerByType(type);
     }
 	
 	uint32_t CameraManager::addCamera(ControllerType controllerType) {
+    	const float ratio = static_cast<float>(m_window.getWidth()) / static_cast<float>(m_window.getHeight());
+    	
         Camera camera;
-        camera.setPerspective(glm::radians(60.0f), m_window.getWidth() / m_window.getHeight(), 0.1f, 10.0f);
+        camera.setPerspective(glm::radians(60.0f), ratio, 0.1f, 10.0f);
         return addCamera(controllerType, camera);
     }
     
@@ -150,7 +185,10 @@ namespace vkcv::camera {
     }
 
     void CameraManager::update(double deltaTime) {
-		getActiveController().updateCamera(deltaTime, getActiveCamera());
+        m_frameTime = deltaTime;
+        if (glfwGetWindowAttrib(m_window.getWindow(), GLFW_FOCUSED) == GLFW_TRUE) {
+            getActiveController().updateCamera(deltaTime, getActiveCamera());
+        }
 	}
 	
-}
\ No newline at end of file
+}
diff --git a/modules/camera/src/vkcv/camera/PilotCameraController.cpp b/modules/camera/src/vkcv/camera/PilotCameraController.cpp
index 1a50a0efa4b4e75adb81ce869d6b927bd0046758..5460858ab48d81252787b3c0141dd72982faca7d 100644
--- a/modules/camera/src/vkcv/camera/PilotCameraController.cpp
+++ b/modules/camera/src/vkcv/camera/PilotCameraController.cpp
@@ -1,5 +1,4 @@
 #include "vkcv/camera/PilotCameraController.hpp"
-
 #include <GLFW/glfw3.h>
 
 namespace vkcv::camera {
@@ -12,9 +11,13 @@ namespace vkcv::camera {
         m_left = false;
         m_right = false;
 
+        m_gamepadX = 0.0f;
+        m_gamepadY = 0.0f;
+        m_gamepadZ = 0.0f;
+
         m_rotationActive = false;
 
-        m_cameraSpeed = 2.0f;
+        m_cameraSpeed = 2.5f;
 
         m_fov_nsteps = 100;
         m_fov_min = 10;
@@ -22,6 +25,11 @@ namespace vkcv::camera {
     }
 
     void PilotCameraController::changeFov(double offset, Camera &camera){
+        // update only if there is (valid) input
+        if (offset == 0.0) {
+            return;
+        }
+
         float fov = camera.getFov();
         float fov_range = m_fov_max - m_fov_min;
         float fov_stepsize = glm::radians(fov_range) / static_cast<float>(m_fov_nsteps);
@@ -36,24 +44,19 @@ namespace vkcv::camera {
     }
 
     void PilotCameraController::panView(double xOffset, double yOffset, Camera &camera) {
-        // handle yaw rotation
-        float yaw = camera.getYaw() + xOffset;
-        if (yaw < -180.0f) {
-            yaw += 360.0f;
-        }
-        else if (yaw > 180.0f) {
-            yaw -= 360.0f;
+        // update only if there is (valid) input
+        if (xOffset == 0.0 && yOffset == 0.0) {
+            return;
         }
+
+        // handle yaw rotation
+        float yaw = camera.getYaw() + static_cast<float>(xOffset);
+        yaw += 360.0f * (yaw < -180.0f) - 360.0f * (yaw > 180.0f);
         camera.setYaw(yaw);
 
         // handle pitch rotation
-        float pitch = camera.getPitch() - yOffset;
-        if (pitch > 89.0f) {
-            pitch = 89.0f;
-        }
-        if (pitch < -89.0f) {
-            pitch = -89.0f;
-        }
+        float pitch = camera.getPitch() - static_cast<float>(yOffset);
+        pitch = glm::clamp(pitch, -89.0f, 89.0f);
         camera.setPitch(pitch);
     }
     
@@ -70,9 +73,9 @@ namespace vkcv::camera {
 	
 		const float distance = m_cameraSpeed * static_cast<float>(deltaTime);
 	
-		position += distance * getDirectionFactor(m_forward, m_backward) * front;
-		position += distance * getDirectionFactor(m_left, m_right) * left;
-		position += distance * getDirectionFactor(m_upward, m_downward) * up;
+		position += distance * (getDirectionFactor(m_forward, m_backward) + m_gamepadZ) * front;
+		position += distance * (getDirectionFactor(m_left, m_right) + m_gamepadX) * left;
+		position += distance * (getDirectionFactor(m_upward, m_downward) + m_gamepadY) * up;
 	
 		camera.lookAt(position, position + front, up);
     }
@@ -127,6 +130,39 @@ namespace vkcv::camera {
         }
     }
 
+    void PilotCameraController::gamepadCallback(int gamepadIndex, Camera &camera, double frametime) {
+        GLFWgamepadstate gamepadState;
+        glfwGetGamepadState(gamepadIndex, &gamepadState);
+
+        float sensitivity = 100.0f;
+        double threshold = 0.1;
+
+        // handle rotations
+        double stickRightX = static_cast<double>(gamepadState.axes[GLFW_GAMEPAD_AXIS_RIGHT_X]);
+        double stickRightY = static_cast<double>(gamepadState.axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]);
+
+        double rightXVal = glm::clamp(std::abs(stickRightX) - threshold, 0.0, 1.0)
+                * copysign(1.0, stickRightX) * sensitivity * frametime;
+        double rightYVal = glm::clamp(std::abs(stickRightY) - threshold, 0.0, 1.0)
+                * copysign(1.0, stickRightY) * sensitivity * frametime;
+        panView(rightXVal, rightYVal, camera);
+
+        // handle zooming
+        double zoom = static_cast<double>((gamepadState.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER]
+                - gamepadState.axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER])
+                * sensitivity * frametime);
+        changeFov(zoom, camera);
+
+        // handle translation
+        m_gamepadY = gamepadState.buttons[GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER] - gamepadState.buttons[GLFW_GAMEPAD_BUTTON_LEFT_BUMPER];
+        float stickLeftX = gamepadState.axes[GLFW_GAMEPAD_AXIS_LEFT_X];
+        float stickLeftY = gamepadState.axes[GLFW_GAMEPAD_AXIS_LEFT_Y];
+        m_gamepadZ = glm::clamp(std::abs(stickLeftY) - threshold, 0.0, 1.0)
+                     * -copysign(1.0, stickLeftY);
+        m_gamepadX = glm::clamp(std::abs(stickLeftX) - threshold, 0.0, 1.0)
+                     * -copysign(1.0, stickLeftX);
+    }
+
 
     void PilotCameraController::moveForward(int action){
         m_forward = static_cast<bool>(action);
diff --git a/modules/camera/src/vkcv/camera/TrackballCameraController.cpp b/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
index 201c6ecdc1c703dbcd53b7dc4b179c86576f2312..cdd66cdb7fdd650d5112fe7bb4738f1fcded7783 100644
--- a/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
+++ b/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
@@ -1,5 +1,4 @@
 #include "vkcv/camera/TrackballCameraController.hpp"
-
 #include <GLFW/glfw3.h>
 
 namespace vkcv::camera {
@@ -12,41 +11,36 @@ namespace vkcv::camera {
     }
 
     void TrackballCameraController::setRadius(const float radius) {
-        if (radius < 0.1f) {
-            m_radius = 0.1f;
-        }
-        else {
-            m_radius = radius;
-        }
+        m_radius = 0.1f * (radius < 0.1f) + radius * (1 - (radius < 0.1f));
     }
 
     void TrackballCameraController::panView(double xOffset, double yOffset, Camera &camera) {
-        // handle yaw rotation
-        float yaw = camera.getYaw() + xOffset * m_cameraSpeed;
-        if (yaw < 0.0f) {
-            yaw += 360.0f;
-        }
-        else if (yaw > 360.0f) {
-            yaw -= 360.0f;
+        // update only if there is (valid) input
+        if (xOffset == 0.0 && yOffset == 0.0) {
+            return;
         }
+
+        // handle yaw rotation
+        float yaw = camera.getYaw() + static_cast<float>(xOffset) * m_cameraSpeed;
+        yaw += 360.0f * (yaw < 0.0f) - 360.0f * (yaw > 360.0f);
         camera.setYaw(yaw);
 
         // handle pitch rotation
-        float pitch = camera.getPitch() + yOffset * m_cameraSpeed;
-        if (pitch < 0.0f) {
-            pitch += 360.0f;
-        }
-        else if (pitch > 360.0f) {
-            pitch -= 360.0f;
-        }
+        float pitch = camera.getPitch() + static_cast<float>(yOffset) * m_cameraSpeed;
+        pitch += 360.0f * (pitch < 0.0f) - 360.0f * (pitch > 360.0f);
         camera.setPitch(pitch);
     }
 
     void TrackballCameraController::updateRadius(double offset, Camera &camera) {
+        // update only if there is (valid) input
+        if (offset == 0.0) {
+            return;
+        }
+
         glm::vec3 cameraPosition = camera.getPosition();
         glm::vec3 cameraCenter = camera.getCenter();
         float radius = glm::length(cameraCenter - cameraPosition);  // get current camera radius
-        setRadius(radius - offset * m_scrollSensitivity);
+        setRadius(radius - static_cast<float>(offset) * m_scrollSensitivity);
     }
 
     void TrackballCameraController::updateCamera(double deltaTime, Camera &camera) {
@@ -82,7 +76,7 @@ namespace vkcv::camera {
             return;
         }
 
-        float sensitivity = 0.05f;
+        float sensitivity = 0.025f;
         xoffset *= sensitivity;
         yoffset *= sensitivity;
 
@@ -97,4 +91,28 @@ namespace vkcv::camera {
             m_rotationActive = false;
         }
     }
+
+    void TrackballCameraController::gamepadCallback(int gamepadIndex, Camera &camera, double frametime) {
+        GLFWgamepadstate gamepadState;
+        glfwGetGamepadState(gamepadIndex, &gamepadState);
+
+        float sensitivity = 100.0f;
+        double threshold = 0.1;
+
+        // handle rotations
+        double stickRightX = static_cast<double>(gamepadState.axes[GLFW_GAMEPAD_AXIS_RIGHT_X]);
+        double stickRightY = static_cast<double>(gamepadState.axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]);
+        
+        double rightXVal = glm::clamp((abs(stickRightX)-threshold), 0.0, 1.0)
+                * std::copysign(1.0, stickRightX) * sensitivity * frametime;
+        double rightYVal = glm::clamp((abs(stickRightY)-threshold), 0.0, 1.0)
+                * std::copysign(1.0, stickRightY) * sensitivity * frametime;
+        panView(rightXVal, rightYVal, camera);
+
+        // handle translation
+        double stickLeftY = static_cast<double>(gamepadState.axes[GLFW_GAMEPAD_AXIS_LEFT_Y]);
+        double leftYVal = glm::clamp((abs(stickLeftY)-threshold), 0.0, 1.0)
+                * std::copysign(1.0, stickLeftY) * sensitivity * frametime;
+        updateRadius(-leftYVal, camera);
+    }
 }
\ No newline at end of file
diff --git a/modules/gui/CMakeLists.txt b/modules/gui/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3b5202ccfe454f38745c53ac711cc05095ef88a1
--- /dev/null
+++ b/modules/gui/CMakeLists.txt
@@ -0,0 +1,36 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_gui)
+
+# setting c++ standard for the module
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_gui_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_gui_include ${PROJECT_SOURCE_DIR}/include)
+
+# Add source and header files to the module
+set(vkcv_gui_sources
+		${vkcv_gui_include}/vkcv/gui/GUI.hpp
+		${vkcv_gui_source}/vkcv/gui/GUI.cpp
+		)
+
+# Setup some path variables to load libraries
+set(vkcv_gui_lib lib)
+set(vkcv_gui_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_gui_lib})
+
+# Check and load IMGUI
+include(config/ImGui.cmake)
+
+# adding source files to the module
+add_library(vkcv_gui STATIC ${vkcv_gui_sources} ${vkcv_imgui_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_gui ${vkcv_gui_libraries} vkcv ${vkcv_libraries})
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_gui SYSTEM BEFORE PRIVATE ${vkcv_gui_includes} ${vkcv_include} ${vkcv_includes})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_gui BEFORE PUBLIC ${vkcv_gui_include} ${vkcv_imgui_includes})
+
+target_compile_definitions(vkcv_gui PUBLIC ${vkcv_gui_defines})
diff --git a/modules/gui/config/ImGui.cmake b/modules/gui/config/ImGui.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..90cdafdeee355af9e63723632572799e135b04da
--- /dev/null
+++ b/modules/gui/config/ImGui.cmake
@@ -0,0 +1,27 @@
+
+if (EXISTS "${vkcv_gui_lib_path}/imgui")
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_glfw.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_glfw.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_vulkan.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_vulkan.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imconfig.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_draw.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_demo.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_internal.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_tables.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_widgets.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imstb_rectpack.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imstb_textedit.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imstb_truetype.h)
+	
+	list(APPEND vkcv_imgui_includes ${vkcv_gui_lib}/imgui)
+	list(APPEND vkcv_imgui_includes ${vkcv_gui_lib}/imgui/backend)
+	
+	list(APPEND vkcv_gui_include ${vkcv_gui_lib})
+	
+	list(APPEND vkcv_gui_defines IMGUI_DISABLE_WIN32_FUNCTIONS=1)
+else()
+	message(WARNING "IMGUI is required..! Update the submodules!")
+endif ()
diff --git a/modules/gui/include/vkcv/gui/GUI.hpp b/modules/gui/include/vkcv/gui/GUI.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d1a9986c5f69bfd9d4392bd5ae50f0b1f8b60642
--- /dev/null
+++ b/modules/gui/include/vkcv/gui/GUI.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "imgui/imgui.h"
+#include "imgui/backends/imgui_impl_glfw.h"
+#include "imgui/backends/imgui_impl_vulkan.h"
+
+#include <vkcv/Core.hpp>
+#include <vkcv/Window.hpp>
+
+namespace vkcv::gui {
+
+	class GUI final {
+	private:
+		Window& m_window;
+		Core& m_core;
+		
+		const Context& m_context;
+		
+		ImGuiContext* m_gui_context;
+		
+		vk::DescriptorPool m_descriptor_pool;
+		vk::RenderPass m_render_pass;
+		
+		event_handle<int,int,int> f_mouseButton;
+		event_handle<double,double> f_mouseScroll;
+		event_handle<int,int,int,int> f_key;
+		event_handle<unsigned int> f_char;
+		
+	public:
+		/**
+		 * Constructor of a new instance of ImGui management
+		 *
+		 * @param core Valid #Core instance of the framework
+		 * @param window Valid #Window instance of the framework
+		 */
+		GUI(Core& core, Window& window);
+		
+		GUI(const GUI& other) = delete;
+		GUI(GUI&& other) = delete;
+		
+		GUI& operator=(const GUI& other) = delete;
+		GUI& operator=(GUI&& other) = delete;
+		
+		/**
+		 * Destructor of a #GUI instance
+		 */
+		virtual ~GUI();
+		
+		/**
+		 * Sets up a new frame for ImGui to draw
+		 */
+		void beginGUI();
+		
+		/**
+		 * Ends a frame for ImGui, renders it and draws it onto
+		 * the currently active swapchain image of the core (ready to present).
+		 */
+		void endGUI();
+		
+	};
+
+}
diff --git a/modules/gui/lib/imgui b/modules/gui/lib/imgui
new file mode 160000
index 0000000000000000000000000000000000000000..d5828cd988db525f27128edeadb1a689cd2d7461
--- /dev/null
+++ b/modules/gui/lib/imgui
@@ -0,0 +1 @@
+Subproject commit d5828cd988db525f27128edeadb1a689cd2d7461
diff --git a/modules/gui/src/vkcv/gui/GUI.cpp b/modules/gui/src/vkcv/gui/GUI.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..38bb6894fb2b40c6ab10445f19431f87f7370afc
--- /dev/null
+++ b/modules/gui/src/vkcv/gui/GUI.cpp
@@ -0,0 +1,241 @@
+
+#include "vkcv/gui/GUI.hpp"
+
+#include <GLFW/glfw3.h>
+#include <vkcv/Logger.hpp>
+
+namespace vkcv::gui {
+	
+	static void checkVulkanResult(VkResult resultCode) {
+		if (resultCode == 0)
+			return;
+		
+		const auto result = vk::Result(resultCode);
+		
+		vkcv_log(LogLevel::ERROR, "ImGui has a problem with Vulkan! (%s)", vk::to_string(result).c_str());
+	}
+	
+	GUI::GUI(Core& core, Window& window) :
+	m_window(window),
+	m_core(core),
+	m_context(m_core.getContext()),
+	m_gui_context(nullptr) {
+		IMGUI_CHECKVERSION();
+		
+		m_gui_context = ImGui::CreateContext();
+		
+		ImGui_ImplGlfw_InitForVulkan(m_window.getWindow(), false);
+		
+		f_mouseButton = m_window.e_mouseButton.add([&](int button, int action, int mods) {
+			ImGui_ImplGlfw_MouseButtonCallback(m_window.getWindow(), button, action, mods);
+		});
+		
+		f_mouseScroll = m_window.e_mouseScroll.add([&](double xoffset, double yoffset) {
+			ImGui_ImplGlfw_ScrollCallback(m_window.getWindow(), xoffset, yoffset);
+		});
+		
+		f_key = m_window.e_key.add([&](int key, int scancode, int action, int mods) {
+			ImGui_ImplGlfw_KeyCallback(m_window.getWindow(), key, scancode, action, mods);
+		});
+		
+		f_char = m_window.e_char.add([&](unsigned int c) {
+			ImGui_ImplGlfw_CharCallback(m_window.getWindow(), c);
+		});
+		
+		vk::DescriptorPoolSize pool_sizes[] = {
+				vk::DescriptorPoolSize(vk::DescriptorType::eSampler, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eSampledImage, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageImage, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eUniformTexelBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageTexelBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eUniformBufferDynamic, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageBufferDynamic, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eInputAttachment, 1000)
+		};
+		
+		const vk::DescriptorPoolCreateInfo descriptorPoolCreateInfo (
+				vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet,
+				static_cast<uint32_t>(1000 * IM_ARRAYSIZE(pool_sizes)),
+				static_cast<uint32_t>(IM_ARRAYSIZE(pool_sizes)),
+				pool_sizes
+		);
+		
+		m_descriptor_pool = m_context.getDevice().createDescriptorPool(descriptorPoolCreateInfo);
+		
+		const vk::PhysicalDevice& physicalDevice = m_context.getPhysicalDevice();
+		const Swapchain& swapchain = m_core.getSwapchain();
+		
+		const uint32_t graphicsQueueFamilyIndex = (
+				m_context.getQueueManager().getGraphicsQueues()[0].familyIndex
+		);
+		
+		ImGui_ImplVulkan_InitInfo init_info = {};
+		init_info.Instance = static_cast<VkInstance>(m_context.getInstance());
+		init_info.PhysicalDevice = static_cast<VkPhysicalDevice>(m_context.getPhysicalDevice());
+		init_info.Device = static_cast<VkDevice>(m_context.getDevice());
+		init_info.QueueFamily = graphicsQueueFamilyIndex;
+		init_info.Queue = static_cast<VkQueue>(m_context.getQueueManager().getGraphicsQueues()[0].handle);
+		init_info.PipelineCache = 0;
+		init_info.DescriptorPool = static_cast<VkDescriptorPool>(m_descriptor_pool);
+		init_info.Allocator = nullptr;
+		init_info.MinImageCount = swapchain.getImageCount();
+		init_info.ImageCount = swapchain.getImageCount();
+		init_info.CheckVkResultFn = checkVulkanResult;
+		
+		const vk::AttachmentDescription attachment (
+				vk::AttachmentDescriptionFlags(),
+				swapchain.getFormat(),
+				vk::SampleCountFlagBits::e1,
+				vk::AttachmentLoadOp::eLoad,
+				vk::AttachmentStoreOp::eStore,
+				vk::AttachmentLoadOp::eDontCare,
+				vk::AttachmentStoreOp::eDontCare,
+				vk::ImageLayout::eUndefined,
+				vk::ImageLayout::ePresentSrcKHR
+		);
+		
+		const vk::AttachmentReference attachmentReference (
+				0,
+				vk::ImageLayout::eColorAttachmentOptimal
+		);
+		
+		const vk::SubpassDescription subpass (
+				vk::SubpassDescriptionFlags(),
+				vk::PipelineBindPoint::eGraphics,
+				0,
+				nullptr,
+				1,
+				&attachmentReference,
+				nullptr,
+				nullptr,
+				0,
+				nullptr
+		);
+		
+		const vk::SubpassDependency dependency (
+				VK_SUBPASS_EXTERNAL,
+				0,
+				vk::PipelineStageFlagBits::eColorAttachmentOutput,
+				vk::PipelineStageFlagBits::eColorAttachmentOutput,
+				vk::AccessFlags(),
+				vk::AccessFlagBits::eColorAttachmentWrite,
+				vk::DependencyFlags()
+		);
+		
+		const vk::RenderPassCreateInfo passCreateInfo (
+				vk::RenderPassCreateFlags(),
+				1,
+				&attachment,
+				1,
+				&subpass,
+				1,
+				&dependency
+		);
+		
+		m_render_pass = m_context.getDevice().createRenderPass(passCreateInfo);
+		
+		ImGui_ImplVulkan_Init(&init_info, static_cast<VkRenderPass>(m_render_pass));
+		
+		const SubmitInfo submitInfo { QueueType::Graphics, {}, {} };
+		
+		m_core.recordAndSubmitCommandsImmediate(submitInfo, [](const vk::CommandBuffer& commandBuffer) {
+			ImGui_ImplVulkan_CreateFontsTexture(static_cast<VkCommandBuffer>(commandBuffer));
+		}, []() {
+			ImGui_ImplVulkan_DestroyFontUploadObjects();
+		});
+		
+		m_context.getDevice().waitIdle();
+	}
+	
+	GUI::~GUI() {
+		m_context.getDevice().waitIdle();
+		
+		ImGui_ImplVulkan_Shutdown();
+		
+		m_context.getDevice().destroyRenderPass(m_render_pass);
+		m_context.getDevice().destroyDescriptorPool(m_descriptor_pool);
+		
+		ImGui_ImplGlfw_Shutdown();
+		
+		m_window.e_mouseButton.remove(f_mouseButton);
+		m_window.e_mouseScroll.remove(f_mouseScroll);
+		m_window.e_key.remove(f_key);
+		m_window.e_char.remove(f_char);
+		
+		if (m_gui_context) {
+			ImGui::DestroyContext(m_gui_context);
+		}
+	}
+	
+	void GUI::beginGUI() {
+		const Swapchain& swapchain = m_core.getSwapchain();
+		const auto extent = swapchain.getExtent();
+		
+		if ((extent.width > 0) && (extent.height > 0)) {
+			ImGui_ImplVulkan_SetMinImageCount(swapchain.getImageCount());
+		}
+		
+		ImGui_ImplVulkan_NewFrame();
+		ImGui_ImplGlfw_NewFrame();
+		ImGui::NewFrame();
+	}
+	
+	void GUI::endGUI() {
+		ImGui::Render();
+		
+		ImDrawData* drawData = ImGui::GetDrawData();
+		
+		if ((!drawData) ||
+			(drawData->DisplaySize.x <= 0.0f) ||
+			(drawData->DisplaySize.y <= 0.0f)) {
+			return;
+		}
+		
+		const Swapchain& swapchain = m_core.getSwapchain();
+		const auto extent = swapchain.getExtent();
+		
+		const vk::ImageView swapchainImageView = m_core.getSwapchainImageView();
+
+		const vk::FramebufferCreateInfo framebufferCreateInfo (
+				vk::FramebufferCreateFlags(),
+				m_render_pass,
+				1,
+				&swapchainImageView,
+				extent.width,
+				extent.height,
+				1
+		);
+		
+		const vk::Framebuffer framebuffer = m_context.getDevice().createFramebuffer(framebufferCreateInfo);
+		
+		SubmitInfo submitInfo;
+		submitInfo.queueType = QueueType::Graphics;
+		
+		m_core.recordAndSubmitCommandsImmediate(submitInfo, [&](const vk::CommandBuffer& commandBuffer) {
+			const vk::Rect2D renderArea (
+					vk::Offset2D(0, 0),
+					extent
+			);
+			
+			const vk::RenderPassBeginInfo beginInfo (
+					m_render_pass,
+					framebuffer,
+					renderArea,
+					0,
+					nullptr
+			);
+			
+			commandBuffer.beginRenderPass(beginInfo, vk::SubpassContents::eInline);
+			
+			ImGui_ImplVulkan_RenderDrawData(drawData, static_cast<VkCommandBuffer>(commandBuffer));
+			
+			commandBuffer.endRenderPass();
+		}, [&]() {
+			m_context.getDevice().destroyFramebuffer(framebuffer);
+		});
+	}
+	
+}
diff --git a/modules/material/CMakeLists.txt b/modules/material/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d5b654cc6d00ce77d93b4666f48b7a5097e674b3
--- /dev/null
+++ b/modules/material/CMakeLists.txt
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_material)
+
+# setting c++ standard for the module
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_material_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_material_include ${PROJECT_SOURCE_DIR}/include)
+
+# Add source and header files to the module
+set(vkcv_material_sources
+		${vkcv_material_include}/vkcv/material/Material.hpp
+		${vkcv_material_source}/vkcv/material/Material.cpp
+		${vkcv_material_include}/vkcv/material/PBRMaterial.hpp
+		${vkcv_material_source}/vkcv/material/PBRMaterial.cpp
+)
+
+# adding source files to the module
+add_library(vkcv_material STATIC ${vkcv_material_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_material vkcv ${vkcv_libraries})
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_material SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_material BEFORE PUBLIC ${vkcv_material_include})
diff --git a/modules/material/include/vkcv/material/Material.hpp b/modules/material/include/vkcv/material/Material.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..00b492072fa4ef8b7b41f70202d515ee4ac828fa
--- /dev/null
+++ b/modules/material/include/vkcv/material/Material.hpp
@@ -0,0 +1,14 @@
+#pragma once
+#include <vkcv/Handles.hpp>
+
+namespace vkcv::material {
+
+	class Material {
+	private:
+	public:
+		const DescriptorSetHandle m_DescriptorSetHandle;
+	protected:
+		Material(const DescriptorSetHandle& setHandle); 
+	};
+	
+}
diff --git a/modules/material/include/vkcv/material/PBRMaterial.hpp b/modules/material/include/vkcv/material/PBRMaterial.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..09a5214b0e748a09ef8caefe5bf2b1a69ecbd8e1
--- /dev/null
+++ b/modules/material/include/vkcv/material/PBRMaterial.hpp
@@ -0,0 +1,103 @@
+#pragma once
+
+#include <vector>
+
+#include <vkcv/DescriptorConfig.hpp>
+#include <vkcv/Core.hpp>
+
+
+#include "Material.hpp"
+
+namespace vkcv::material
+{
+    class PBRMaterial : Material
+    {
+    private:
+        struct vec3 {
+            float x, y, z;
+        };
+        struct vec4 {
+            float x, y, z, a;
+        };
+        PBRMaterial(const ImageHandle& colorImg,
+            const SamplerHandle& colorSmp,
+            const ImageHandle& normalImg,
+            const SamplerHandle& normalSmp,
+            const ImageHandle& metRoughImg,
+            const SamplerHandle& metRoughSmp,
+            const ImageHandle& occlusionImg,
+            const SamplerHandle& occlusionSmp,
+            const ImageHandle& emissiveImg,
+            const SamplerHandle& emissiveSmp,
+            const DescriptorSetHandle& setHandle,
+            vec4 baseColorFactor,
+            float metallicFactor,
+            float roughnessFactor,
+            float normalScale,
+            float occlusionStrength,
+            vec3 emissiveFactor) noexcept;
+
+
+    public:
+        PBRMaterial() = delete;
+        
+        const ImageHandle   m_ColorTexture;
+        const SamplerHandle m_ColorSampler;
+
+        const ImageHandle   m_NormalTexture;
+        const SamplerHandle m_NormalSampler;
+
+        const ImageHandle   m_MetRoughTexture;
+        const SamplerHandle m_MetRoughSampler;
+
+        const ImageHandle m_OcclusionTexture;
+        const SamplerHandle m_OcclusionSampler;
+
+        const ImageHandle m_EmissiveTexture;
+        const SamplerHandle m_EmissiveSampler;
+
+        //
+        const vec4 m_BaseColorFactor;
+		const float m_MetallicFactor;
+		const float m_RoughnessFactor;
+		const float m_NormalScale;
+		const float m_OcclusionStrength;
+		const vec3 m_EmissiveFactor;
+
+        /*
+        * Returns the material's necessary descriptor bindings which serves as its descriptor layout
+        * The binding is in the following order:
+        * 0 - diffuse texture
+        * 1 - diffuse sampler
+        * 2 - normal texture
+        * 3 - normal sampler
+        * 4 - metallic roughness texture
+        * 5 - metallic roughness sampler
+        * 6 - occlusion texture
+        * 7 - occlusion sampler
+        * 8 - emissive texture
+        * 9 - emissive sampler
+        */
+        static std::vector<DescriptorBinding> getDescriptorBindings() noexcept;
+
+        static PBRMaterial create(
+            vkcv::Core* core,
+            ImageHandle          &colorImg,
+            SamplerHandle        &colorSmp,
+            ImageHandle          &normalImg,
+            SamplerHandle        &normalSmp,
+            ImageHandle          &metRoughImg,
+            SamplerHandle        &metRoughSmp,
+			ImageHandle			&occlusionImg,
+			SamplerHandle		&occlusionSmp,
+			ImageHandle			&emissiveImg,
+			SamplerHandle		&emissiveSmp,
+            vec4 baseColorFactor,
+            float metallicFactor,
+            float roughnessFactor,
+            float normalScale,
+            float occlusionStrength,
+            vec3 emissiveFactor);
+
+    };
+}
\ No newline at end of file
diff --git a/modules/material/src/vkcv/material/Material.cpp b/modules/material/src/vkcv/material/Material.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9168bcfbf924e9868ceaaff74aef5d3c6b99739c
--- /dev/null
+++ b/modules/material/src/vkcv/material/Material.cpp
@@ -0,0 +1,12 @@
+
+#include "vkcv/material/Material.hpp"
+
+namespace vkcv::material {
+
+	//TODO
+
+	Material::Material(const DescriptorSetHandle& setHandle) : m_DescriptorSetHandle(setHandle)
+	{
+	}
+
+}
diff --git a/modules/material/src/vkcv/material/PBRMaterial.cpp b/modules/material/src/vkcv/material/PBRMaterial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d27e755c06a39e369d22efc997a0b411d067c132
--- /dev/null
+++ b/modules/material/src/vkcv/material/PBRMaterial.cpp
@@ -0,0 +1,194 @@
+#include "vkcv/material/PBRMaterial.hpp"
+
+
+namespace vkcv::material
+{
+    PBRMaterial::PBRMaterial(
+        const ImageHandle& colorImg,
+        const SamplerHandle& colorSmp,
+        const ImageHandle& normalImg,
+        const SamplerHandle& normalSmp,
+        const ImageHandle& metRoughImg,
+        const SamplerHandle& metRoughSmp,
+        const ImageHandle& occlusionImg,
+        const SamplerHandle& occlusionSmp,
+        const ImageHandle& emissiveImg,
+        const SamplerHandle& emissiveSmp,
+        const DescriptorSetHandle& setHandle,
+        vec4 baseColorFactor,
+        float metallicFactor,
+        float roughnessFactor,
+        float normalScale,
+        float occlusionStrength,
+        vec3 emissiveFactor) noexcept :
+        m_ColorTexture(colorImg),
+        m_ColorSampler(colorSmp),
+        m_NormalTexture(normalImg),
+        m_NormalSampler(normalSmp),
+        m_MetRoughTexture(metRoughImg),
+        m_MetRoughSampler(metRoughSmp),
+        m_OcclusionTexture(occlusionImg),
+        m_OcclusionSampler(occlusionSmp),
+        m_EmissiveTexture(emissiveImg),
+        m_EmissiveSampler(emissiveSmp),
+        Material(setHandle),
+        m_BaseColorFactor(baseColorFactor),
+        m_MetallicFactor(metallicFactor),
+        m_RoughnessFactor(roughnessFactor),
+        m_NormalScale(normalScale),
+        m_OcclusionStrength(occlusionStrength),
+        m_EmissiveFactor(emissiveFactor)
+    {
+    }
+
+    std::vector<DescriptorBinding> PBRMaterial::getDescriptorBindings() noexcept
+    {
+		static std::vector<DescriptorBinding> bindings;
+		
+		if (bindings.empty()) {
+			bindings.emplace_back(0, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(1, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(2, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(3, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(4, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(5, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(6, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(7, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(8, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+			bindings.emplace_back(9, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+		}
+    	
+        return bindings;
+    }
+
+    PBRMaterial PBRMaterial::create(
+        vkcv::Core* core,
+        ImageHandle& colorImg,
+        SamplerHandle& colorSmp,
+        ImageHandle& normalImg,
+        SamplerHandle& normalSmp,
+        ImageHandle& metRoughImg,
+        SamplerHandle& metRoughSmp,
+        ImageHandle& occlusionImg,
+        SamplerHandle& occlusionSmp,
+        ImageHandle& emissiveImg,
+        SamplerHandle& emissiveSmp,
+        vec4 baseColorFactor,
+        float metallicFactor,
+        float roughnessFactor,
+        float normalScale,
+        float occlusionStrength,
+        vec3 emissiveFactor)
+    {
+        //Test if Images and samplers valid, if not use default
+         if (!colorImg) {
+            vkcv::Image defaultColor = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
+            vec4 colorData{ 228, 51, 255,1 };
+            defaultColor.fill(&colorData);
+            colorImg = defaultColor.getHandle();
+        }
+        if (!normalImg) {
+            vkcv::Image defaultNormal = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
+            vec4 normalData{ 0, 0, 1,0 };
+            defaultNormal.fill(&normalData);
+            normalImg = defaultNormal.getHandle();
+        }
+        if (!metRoughImg) {
+            vkcv::Image defaultRough = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
+            vec4 roughData{ 228, 51, 255,1 };
+            defaultRough.fill(&roughData);
+            metRoughImg = defaultRough.getHandle();
+        }
+        if (!occlusionImg) {
+            vkcv::Image defaultOcclusion = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
+            vec4 occlusionData{ 228, 51, 255,1 };
+            defaultOcclusion.fill(&occlusionData);
+            occlusionImg = defaultOcclusion.getHandle();
+        }
+        if (!emissiveImg) {
+            vkcv::Image defaultEmissive = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
+            vec4 emissiveData{ 0, 0, 0,1 };
+            defaultEmissive.fill(&emissiveData);
+            emissiveImg = defaultEmissive.getHandle();
+        }
+        if (!colorSmp) {            
+            colorSmp = core->createSampler(
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerMipmapMode::LINEAR,
+                vkcv::SamplerAddressMode::REPEAT
+            );            
+        }
+        if (!normalSmp) {            
+            normalSmp = core->createSampler(
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerMipmapMode::LINEAR,
+                vkcv::SamplerAddressMode::REPEAT
+            );            
+        }
+        if (!metRoughSmp) {
+            metRoughSmp = core->createSampler(
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerMipmapMode::LINEAR,
+                vkcv::SamplerAddressMode::REPEAT
+            );            
+        }
+        if (!occlusionSmp) {
+            occlusionSmp = core->createSampler(
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerMipmapMode::LINEAR,
+                vkcv::SamplerAddressMode::REPEAT
+            );
+        }
+        if (!emissiveSmp) {
+            emissiveSmp = core->createSampler(
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerFilterType::LINEAR,
+                vkcv::SamplerMipmapMode::LINEAR,
+                vkcv::SamplerAddressMode::REPEAT
+            );
+        }
+        
+
+
+        //create descriptorset
+        vkcv::DescriptorSetHandle descriptorSetHandle = core->createDescriptorSet(getDescriptorBindings());
+        //writes
+        vkcv::DescriptorWrites setWrites;
+        setWrites.sampledImageWrites = {
+            vkcv::SampledImageDescriptorWrite(0, colorImg),
+            vkcv::SampledImageDescriptorWrite(2, normalImg),
+            vkcv::SampledImageDescriptorWrite(4, metRoughImg),
+            vkcv::SampledImageDescriptorWrite(6, occlusionImg),
+            vkcv::SampledImageDescriptorWrite(8, emissiveImg) };
+        setWrites.samplerWrites = {
+            vkcv::SamplerDescriptorWrite(1, colorSmp),
+            vkcv::SamplerDescriptorWrite(3, normalSmp),
+            vkcv::SamplerDescriptorWrite(5, metRoughSmp),
+            vkcv::SamplerDescriptorWrite(7, occlusionSmp),
+            vkcv::SamplerDescriptorWrite(9, emissiveSmp) };
+        core->writeDescriptorSet(descriptorSetHandle, setWrites);
+
+        return PBRMaterial(
+            colorImg,
+            colorSmp,
+            normalImg,
+            normalSmp,
+            metRoughImg,
+            metRoughSmp,
+            occlusionImg,
+            occlusionSmp,
+            emissiveImg,
+            emissiveSmp,
+            descriptorSetHandle,
+            baseColorFactor,
+            metallicFactor,
+            roughnessFactor,
+            normalScale,
+            occlusionStrength,
+            emissiveFactor);
+    }
+}
\ No newline at end of file
diff --git a/modules/shader_compiler/CMakeLists.txt b/modules/shader_compiler/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4b674ec41ed4ea5f42dc73187c212e6a69952cec
--- /dev/null
+++ b/modules/shader_compiler/CMakeLists.txt
@@ -0,0 +1,34 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_shader_compiler)
+
+# setting c++ standard for the module
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_shader_compiler_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_shader_compiler_include ${PROJECT_SOURCE_DIR}/include)
+
+# Add source and header files to the module
+set(vkcv_shader_compiler_sources
+		${vkcv_shader_compiler_include}/vkcv/shader/GLSLCompiler.hpp
+		${vkcv_shader_compiler_source}/vkcv/shader/GLSLCompiler.cpp
+)
+
+# adding source files to the module
+add_library(vkcv_shader_compiler STATIC ${vkcv_shader_compiler_sources})
+
+# Setup some path variables to load libraries
+set(vkcv_shader_compiler_lib lib)
+set(vkcv_shader_compiler_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_shader_compiler_lib})
+
+# Check and load GLSLANG
+include(config/GLSLANG.cmake)
+
+# link the required libraries to the module
+target_link_libraries(vkcv_shader_compiler ${vkcv_shader_compiler_libraries} vkcv)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_shader_compiler SYSTEM BEFORE PRIVATE ${vkcv_shader_compiler_includes} ${vkcv_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_shader_compiler BEFORE PUBLIC ${vkcv_shader_compiler_include})
diff --git a/modules/shader_compiler/config/GLSLANG.cmake b/modules/shader_compiler/config/GLSLANG.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..50b9fd46bd0db9421c632aa0b80fb8df7e3f2123
--- /dev/null
+++ b/modules/shader_compiler/config/GLSLANG.cmake
@@ -0,0 +1,28 @@
+
+if (EXISTS "${vkcv_shader_compiler_lib_path}/glslang")
+	set(SKIP_GLSLANG_INSTALL ON CACHE INTERNAL "")
+	set(ENABLE_SPVREMAPPER OFF CACHE INTERNAL "")
+	set(ENABLE_GLSLANG_BINARIES OFF CACHE INTERNAL "")
+	set(ENABLE_GLSLANG_JS OFF CACHE INTERNAL "")
+	set(ENABLE_GLSLANG_WEBMIN OFF CACHE INTERNAL "")
+	set(ENABLE_GLSLANG_WEBMIN_DEVEL OFF CACHE INTERNAL "")
+	set(ENABLE_EMSCRIPTEN_SINGLE_FILE OFF CACHE INTERNAL "")
+	set(ENABLE_EMSCRIPTEN_ENVIRONMENT_NODE OFF CACHE INTERNAL "")
+	set(ENABLE_HLSL OFF CACHE INTERNAL "")
+	set(ENABLE_RTTI OFF CACHE INTERNAL "")
+	set(ENABLE_EXCEPTIONS OFF CACHE INTERNAL "")
+	set(ENABLE_OPT OFF CACHE INTERNAL "")
+	set(ENABLE_PCH OFF CACHE INTERNAL "")
+	set(ENABLE_CTEST OFF CACHE INTERNAL "")
+	set(USE_CCACHE OFF CACHE INTERNAL "")
+	
+	set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "")
+	set(BUILD_EXTERNAL OFF CACHE INTERNAL "")
+	
+	add_subdirectory(${vkcv_shader_compiler_lib}/glslang)
+	
+	list(APPEND vkcv_shader_compiler_libraries glslang SPIRV)
+	list(APPEND vkcv_shader_compiler_includes ${vkcv_shader_compiler_lib})
+else()
+	message(WARNING "GLSLANG is required..! Update the submodules!")
+endif ()
diff --git a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d7b7af7178531aea358cecbc8b86a29527173014
--- /dev/null
+++ b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <vkcv/Event.hpp>
+
+namespace vkcv::shader {
+	
+	typedef typename event_function<ShaderStage, const std::filesystem::path&>::type ShaderCompiledFunction;
+	
+	class Compiler {
+	private:
+	public:
+		virtual void compile(ShaderStage shaderStage, const std::filesystem::path& shaderPath,
+							 const ShaderCompiledFunction& compiled, bool update = false) = 0;
+		
+	};
+	
+}
diff --git a/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7105d93a0c3e153bf3abe1d624d0c13c6f09ac6d
--- /dev/null
+++ b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <filesystem>
+
+#include <vkcv/ShaderStage.hpp>
+#include "Compiler.hpp"
+
+namespace vkcv::shader {
+	
+	class GLSLCompiler {
+	private:
+	public:
+		GLSLCompiler();
+		
+		GLSLCompiler(const GLSLCompiler& other);
+		GLSLCompiler(GLSLCompiler&& other) = default;
+	
+		~GLSLCompiler();
+		
+		GLSLCompiler& operator=(const GLSLCompiler& other);
+		GLSLCompiler& operator=(GLSLCompiler&& other) = default;
+		
+		void compile(ShaderStage shaderStage, const std::filesystem::path& shaderPath,
+					 const ShaderCompiledFunction& compiled, bool update = false);
+		
+	};
+	
+}
diff --git a/modules/shader_compiler/lib/glslang b/modules/shader_compiler/lib/glslang
new file mode 160000
index 0000000000000000000000000000000000000000..fe15158676657bf965e41c32e15ae5db7ea2ab6a
--- /dev/null
+++ b/modules/shader_compiler/lib/glslang
@@ -0,0 +1 @@
+Subproject commit fe15158676657bf965e41c32e15ae5db7ea2ab6a
diff --git a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ec358188b8e871da6f4d62ffd397f32bfb795ee2
--- /dev/null
+++ b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
@@ -0,0 +1,283 @@
+
+#include "vkcv/shader/GLSLCompiler.hpp"
+
+#include <fstream>
+#include <glslang/SPIRV/GlslangToSpv.h>
+#include <glslang/StandAlone/DirStackFileIncluder.h>
+
+#include <vkcv/Logger.hpp>
+
+namespace vkcv::shader {
+	
+	static uint32_t s_CompilerCount = 0;
+	
+	GLSLCompiler::GLSLCompiler() {
+		if (s_CompilerCount == 0) {
+			glslang::InitializeProcess();
+		}
+		
+		s_CompilerCount++;
+	}
+	
+	GLSLCompiler::GLSLCompiler(const GLSLCompiler &other) {
+		s_CompilerCount++;
+	}
+	
+	GLSLCompiler::~GLSLCompiler() {
+		s_CompilerCount--;
+		
+		if (s_CompilerCount == 0) {
+			glslang::FinalizeProcess();
+		}
+	}
+	
+	GLSLCompiler &GLSLCompiler::operator=(const GLSLCompiler &other) {
+		s_CompilerCount++;
+		return *this;
+	}
+	
+	constexpr EShLanguage findShaderLanguage(ShaderStage shaderStage) {
+		switch (shaderStage) {
+			case ShaderStage::VERTEX:
+				return EShLangVertex;
+			case ShaderStage::TESS_CONTROL:
+				return EShLangTessControl;
+			case ShaderStage::TESS_EVAL:
+				return EShLangTessEvaluation;
+			case ShaderStage::GEOMETRY:
+				return EShLangGeometry;
+			case ShaderStage::FRAGMENT:
+				return EShLangFragment;
+			case ShaderStage::COMPUTE:
+				return EShLangCompute;
+			default:
+				return EShLangCount;
+		}
+	}
+	
+	static void initResources(TBuiltInResource& resources) {
+		resources.maxLights = 32;
+		resources.maxClipPlanes = 6;
+		resources.maxTextureUnits = 32;
+		resources.maxTextureCoords = 32;
+		resources.maxVertexAttribs = 64;
+		resources.maxVertexUniformComponents = 4096;
+		resources.maxVaryingFloats = 64;
+		resources.maxVertexTextureImageUnits = 32;
+		resources.maxCombinedTextureImageUnits = 80;
+		resources.maxTextureImageUnits = 32;
+		resources.maxFragmentUniformComponents = 4096;
+		resources.maxDrawBuffers = 32;
+		resources.maxVertexUniformVectors = 128;
+		resources.maxVaryingVectors = 8;
+		resources.maxFragmentUniformVectors = 16;
+		resources.maxVertexOutputVectors = 16;
+		resources.maxFragmentInputVectors = 15;
+		resources.minProgramTexelOffset = -8;
+		resources.maxProgramTexelOffset = 7;
+		resources.maxClipDistances = 8;
+		resources.maxComputeWorkGroupCountX = 65535;
+		resources.maxComputeWorkGroupCountY = 65535;
+		resources.maxComputeWorkGroupCountZ = 65535;
+		resources.maxComputeWorkGroupSizeX = 1024;
+		resources.maxComputeWorkGroupSizeY = 1024;
+		resources.maxComputeWorkGroupSizeZ = 64;
+		resources.maxComputeUniformComponents = 1024;
+		resources.maxComputeTextureImageUnits = 16;
+		resources.maxComputeImageUniforms = 8;
+		resources.maxComputeAtomicCounters = 8;
+		resources.maxComputeAtomicCounterBuffers = 1;
+		resources.maxVaryingComponents = 60;
+		resources.maxVertexOutputComponents = 64;
+		resources.maxGeometryInputComponents = 64;
+		resources.maxGeometryOutputComponents = 128;
+		resources.maxFragmentInputComponents = 128;
+		resources.maxImageUnits = 8;
+		resources.maxCombinedImageUnitsAndFragmentOutputs = 8;
+		resources.maxCombinedShaderOutputResources = 8;
+		resources.maxImageSamples = 0;
+		resources.maxVertexImageUniforms = 0;
+		resources.maxTessControlImageUniforms = 0;
+		resources.maxTessEvaluationImageUniforms = 0;
+		resources.maxGeometryImageUniforms = 0;
+		resources.maxFragmentImageUniforms = 8;
+		resources.maxCombinedImageUniforms = 8;
+		resources.maxGeometryTextureImageUnits = 16;
+		resources.maxGeometryOutputVertices = 256;
+		resources.maxGeometryTotalOutputComponents = 1024;
+		resources.maxGeometryUniformComponents = 1024;
+		resources.maxGeometryVaryingComponents = 64;
+		resources.maxTessControlInputComponents = 128;
+		resources.maxTessControlOutputComponents = 128;
+		resources.maxTessControlTextureImageUnits = 16;
+		resources.maxTessControlUniformComponents = 1024;
+		resources.maxTessControlTotalOutputComponents = 4096;
+		resources.maxTessEvaluationInputComponents = 128;
+		resources.maxTessEvaluationOutputComponents = 128;
+		resources.maxTessEvaluationTextureImageUnits = 16;
+		resources.maxTessEvaluationUniformComponents = 1024;
+		resources.maxTessPatchComponents = 120;
+		resources.maxPatchVertices = 32;
+		resources.maxTessGenLevel = 64;
+		resources.maxViewports = 16;
+		resources.maxVertexAtomicCounters = 0;
+		resources.maxTessControlAtomicCounters = 0;
+		resources.maxTessEvaluationAtomicCounters = 0;
+		resources.maxGeometryAtomicCounters = 0;
+		resources.maxFragmentAtomicCounters = 8;
+		resources.maxCombinedAtomicCounters = 8;
+		resources.maxAtomicCounterBindings = 1;
+		resources.maxVertexAtomicCounterBuffers = 0;
+		resources.maxTessControlAtomicCounterBuffers = 0;
+		resources.maxTessEvaluationAtomicCounterBuffers = 0;
+		resources.maxGeometryAtomicCounterBuffers = 0;
+		resources.maxFragmentAtomicCounterBuffers = 1;
+		resources.maxCombinedAtomicCounterBuffers = 1;
+		resources.maxAtomicCounterBufferSize = 16384;
+		resources.maxTransformFeedbackBuffers = 4;
+		resources.maxTransformFeedbackInterleavedComponents = 64;
+		resources.maxCullDistances = 8;
+		resources.maxCombinedClipAndCullDistances = 8;
+		resources.maxSamples = 4;
+		resources.maxMeshOutputVerticesNV = 256;
+		resources.maxMeshOutputPrimitivesNV = 512;
+		resources.maxMeshWorkGroupSizeX_NV = 32;
+		resources.maxMeshWorkGroupSizeY_NV = 1;
+		resources.maxMeshWorkGroupSizeZ_NV = 1;
+		resources.maxTaskWorkGroupSizeX_NV = 32;
+		resources.maxTaskWorkGroupSizeY_NV = 1;
+		resources.maxTaskWorkGroupSizeZ_NV = 1;
+		resources.maxMeshViewCountNV = 4;
+		resources.limits.nonInductiveForLoops = 1;
+		resources.limits.whileLoops = 1;
+		resources.limits.doWhileLoops = 1;
+		resources.limits.generalUniformIndexing = 1;
+		resources.limits.generalAttributeMatrixVectorIndexing = 1;
+		resources.limits.generalVaryingIndexing = 1;
+		resources.limits.generalSamplerIndexing = 1;
+		resources.limits.generalVariableIndexing = 1;
+		resources.limits.generalConstantMatrixVectorIndexing = 1;
+	}
+	
+	static std::vector<char> readShaderCode(const std::filesystem::path &shaderPath) {
+		std::ifstream file (shaderPath.string(), std::ios::ate);
+		
+		if (!file.is_open()) {
+			vkcv_log(LogLevel::ERROR, "The file could not be opened (%s)", shaderPath.string().c_str());
+			return std::vector<char>{};
+		}
+		
+		std::streamsize fileSize = file.tellg();
+		std::vector<char> buffer (fileSize + 1);
+		
+		file.seekg(0);
+		file.read(buffer.data(), fileSize);
+		file.close();
+		
+		buffer[fileSize] = '\0';
+		return buffer;
+	}
+	
+	static bool writeSpirvCode(const std::filesystem::path &shaderPath, const std::vector<uint32_t>& spirv) {
+		std::ofstream file (shaderPath.string(), std::ios::out | std::ios::binary);
+		
+		if (!file.is_open()) {
+			vkcv_log(LogLevel::ERROR, "The file could not be opened (%s)", shaderPath.string().c_str());
+			return false;
+		}
+		
+		const auto fileSize = static_cast<std::streamsize>(
+				sizeof(uint32_t) * spirv.size()
+		);
+		
+		file.seekp(0);
+		file.write(reinterpret_cast<const char*>(spirv.data()), fileSize);
+		file.close();
+		
+		return true;
+	}
+	
+	void GLSLCompiler::compile(ShaderStage shaderStage, const std::filesystem::path &shaderPath,
+							   const ShaderCompiledFunction& compiled, bool update) {
+		const EShLanguage language = findShaderLanguage(shaderStage);
+		
+		if (language == EShLangCount) {
+			vkcv_log(LogLevel::ERROR, "Shader stage not supported (%s)", shaderPath.string().c_str());
+			return;
+		}
+		
+		const std::vector<char> code = readShaderCode(shaderPath);
+		
+		glslang::TShader shader (language);
+		glslang::TProgram program;
+		
+		const char *shaderStrings [1];
+		shaderStrings[0] = code.data();
+		
+		shader.setStrings(shaderStrings, 1);
+		
+		TBuiltInResource resources = {};
+		initResources(resources);
+
+		const auto messages = (EShMessages)(
+			EShMsgSpvRules |
+			EShMsgVulkanRules
+			);
+
+		std::string preprocessedGLSL;
+
+		DirStackFileIncluder includer;
+		includer.pushExternalLocalDirectory(shaderPath.parent_path().string());
+
+		if (!shader.preprocess(&resources, 100, ENoProfile, false, false, messages, &preprocessedGLSL, includer)) {
+			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n} (%s)",
+				shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
+			return;
+		}
+		
+		const char* preprocessedCString = preprocessedGLSL.c_str();
+		shader.setStrings(&preprocessedCString, 1);
+
+		if (!shader.parse(&resources, 100, false, messages)) {
+			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n} (%s)",
+					 shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
+			return;
+		}
+		
+		program.addShader(&shader);
+		
+		if (!program.link(messages)) {
+			vkcv_log(LogLevel::ERROR, "Shader linking failed {\n%s\n%s\n} (%s)",
+					 shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
+			return;
+		}
+		
+		const glslang::TIntermediate* intermediate = program.getIntermediate(language);
+		
+		if (!intermediate) {
+			vkcv_log(LogLevel::ERROR, "No valid intermediate representation (%s)", shaderPath.string().c_str());
+			return;
+		}
+		
+		std::vector<uint32_t> spirv;
+		glslang::GlslangToSpv(*intermediate, spirv);
+		
+		const std::filesystem::path tmp_path (std::tmpnam(nullptr));
+		
+		if (!writeSpirvCode(tmp_path, spirv)) {
+			vkcv_log(LogLevel::ERROR, "Spir-V could not be written to disk (%s)", shaderPath.string().c_str());
+			return;
+		}
+		
+		if (compiled) {
+			compiled(shaderStage, tmp_path);
+		}
+		
+		std::filesystem::remove(tmp_path);
+		
+		if (update) {
+			// TODO: Shader hot compilation during runtime
+		}
+	}
+	
+}
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index ccbdaf4101c5dabb3e9d43788e255eab85ad5776..1c6e3afe2347f6ef8ea8a62be7acbe0ea750497d 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -1,5 +1,8 @@
 
 # Add new projects/examples here:
+add_subdirectory(bloom)
 add_subdirectory(first_triangle)
 add_subdirectory(first_mesh)
-add_subdirectory(cmd_sync_test)
\ No newline at end of file
+add_subdirectory(particle_simulation)
+add_subdirectory(first_scene)
+add_subdirectory(voxelization)
diff --git a/projects/bloom/.gitignore b/projects/bloom/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3643183e0628e666abab193e1dd1d92c1774ac61
--- /dev/null
+++ b/projects/bloom/.gitignore
@@ -0,0 +1 @@
+bloom
\ No newline at end of file
diff --git a/projects/bloom/CMakeLists.txt b/projects/bloom/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8171938e7cb430aacce5562af44f628c11c97c54
--- /dev/null
+++ b/projects/bloom/CMakeLists.txt
@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.16)
+project(bloom)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(bloom src/main.cpp)
+
+target_sources(bloom PRIVATE
+		src/BloomAndFlares.cpp
+		src/BloomAndFlares.hpp)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(bloom PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(bloom PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+    
+    # in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(bloom PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(bloom SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(bloom vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler)
diff --git a/projects/bloom/resources/Sponza/Sponza.bin b/projects/bloom/resources/Sponza/Sponza.bin
new file mode 100644
index 0000000000000000000000000000000000000000..cfedd26ca5a67b6d0a47d44d13a75e14a141717a
--- /dev/null
+++ b/projects/bloom/resources/Sponza/Sponza.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4b809f7a17687dc99e6f41ca1ea32c06eded8779bf34d16f1f565d750b0ffd68
+size 6347696
diff --git a/projects/bloom/resources/Sponza/Sponza.gltf b/projects/bloom/resources/Sponza/Sponza.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..172ea07e21c94465211c860cd805355704cef230
--- /dev/null
+++ b/projects/bloom/resources/Sponza/Sponza.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cc0ecad5c4694088ff820e663619c370421afc1323ac487406e8e9b4735d787
+size 713962
diff --git a/projects/bloom/resources/Sponza/background.png b/projects/bloom/resources/Sponza/background.png
new file mode 100644
index 0000000000000000000000000000000000000000..b64def129da38f4e23d89e21b4af1039008a4327
--- /dev/null
+++ b/projects/bloom/resources/Sponza/background.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f5b5f900ff8ed83a31750ec8e428b5b91273794ddcbfc4e4b8a6a7e781f8c686
+size 1417666
diff --git a/projects/bloom/resources/Sponza/chain_texture.png b/projects/bloom/resources/Sponza/chain_texture.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1e1768cff78e0614ad707eca8602a4c4edab5e5
--- /dev/null
+++ b/projects/bloom/resources/Sponza/chain_texture.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8362cfd472880daeaea37439326a4651d1338680ae69bb2513fc6b17c8de7d4
+size 490895
diff --git a/projects/bloom/resources/Sponza/lion.png b/projects/bloom/resources/Sponza/lion.png
new file mode 100644
index 0000000000000000000000000000000000000000..c49c7f0ed31e762e19284d0d3624fbc47664e56b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/lion.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f882f746c3a9cd51a9c6eedc1189b97668721d91a3fe49232036e789912c652
+size 2088728
diff --git a/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png b/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..cde4c7a6511e9a5f03c63ad996437fcdba3ce2df
--- /dev/null
+++ b/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b94219c2f5f943f3f4715c74e7d1038bf0ab3b3b3216a758eaee67f875df0851
+size 1928829
diff --git a/projects/bloom/resources/Sponza/sponza_arch_diff.png b/projects/bloom/resources/Sponza/sponza_arch_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..bcd9bda2918d226039f9e2d03902d377b706fab6
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_arch_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c0df2c8a01b2843b1c792b494f7173cdbc4f834840fc2177af3e5d690fceda57
+size 1596151
diff --git a/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png b/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..59de631ffac4414cabf69b2dc794c46fc187d6cb
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab6c187a81aa68f4eba30119e17fce2e4882a9ec320f70c90482dbe9da82b1c6
+size 1872074
diff --git a/projects/bloom/resources/Sponza/sponza_column_a_diff.png b/projects/bloom/resources/Sponza/sponza_column_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..01a82432d3f9939bbefe850bdb900f1ff9a3f6db
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_column_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2c291507e2808bb83e160ab4b020689817df273baad3713a9ad19ac15fac6826
+size 1840992
diff --git a/projects/bloom/resources/Sponza/sponza_column_b_diff.png b/projects/bloom/resources/Sponza/sponza_column_b_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..10a660cce2a5a9b8997772c746058ce23e7d45d7
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_column_b_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2820b0267c4289c6cedbb42721792a57ef244ec2d0935941011c2a7d3fe88a9b
+size 2170433
diff --git a/projects/bloom/resources/Sponza/sponza_column_c_diff.png b/projects/bloom/resources/Sponza/sponza_column_c_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc46fd979044a938d3adca7601689e71504e48bf
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_column_c_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a0bc993ff59865468ef4530798930c7dfefb07482d71db45bc2a520986b27735
+size 2066950
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..384c8c2c051160d530eb3ac8b05c9c60752a2d2b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b85c6bb3cd5105f48d3812ec8e7a1068521ce69e917300d79e136e19d45422fb
+size 9510905
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..af842e9f5fe18c1f609875e00899a6770fa4488b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_curtain_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:563c56bdbbee395a6ef7f0c51c8ac9223c162e517b4cdba0d4654e8de27c98d8
+size 9189263
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..6c9b6391a199407637fa71033d79fb58b8b4f0d7
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:238fe1c7f481388d1c1d578c2da8d411b99e8f0030ab62060a306db333124476
+size 8785458
diff --git a/projects/bloom/resources/Sponza/sponza_details_diff.png b/projects/bloom/resources/Sponza/sponza_details_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..12656686362c3e0a297e060491f33bd7351551f9
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_details_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb1223b3bb82f8757e7df25a6891f1239cdd7ec59990340e952fb2d6b7ea570c
+size 1522643
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..879d16ef84722a4fc13e83a771778de326e4bc54
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:467d290bf5d4b2a017da140ba9e244ed8a8a9be5418a9ac9bcb4ad572ae2d7ab
+size 2229440
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..3311287a219d2148620b87fe428fea071688d051
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_fabric_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1594f59cc2848db26add47361f4e665e3d8afa147760ed915d839fea42b20287
+size 2267382
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..de110f369004388dae4cd5067c63428db3a07834
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:902b87faab221173bf370cea7c74cb9060b4d870ac6316b190dafded1cb12993
+size 2258220
diff --git a/projects/bloom/resources/Sponza/sponza_flagpole_diff.png b/projects/bloom/resources/Sponza/sponza_flagpole_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f6e0812a0df80346318baa3cb50a6888afc58f8
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_flagpole_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bfffb62e770959c725d0f3db6dc7dbdd46a380ec55ef884dab94d44ca017b438
+size 1425673
diff --git a/projects/bloom/resources/Sponza/sponza_floor_a_diff.png b/projects/bloom/resources/Sponza/sponza_floor_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..788ed764f79ba724f04a2d603076a5b85013e188
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_floor_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a16f9230fa91f9f31dfca6216ce205f1ef132d44f3b012fbf6efc0fba69770ab
+size 1996838
diff --git a/projects/bloom/resources/Sponza/sponza_roof_diff.png b/projects/bloom/resources/Sponza/sponza_roof_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5b84261fdd1cc776a94b3ce398c7806b895f9a3
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_roof_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7fc412138c20da19f8173e53545e771f4652558dff624d4dc67143e40efe562b
+size 2320533
diff --git a/projects/bloom/resources/Sponza/sponza_thorn_diff.png b/projects/bloom/resources/Sponza/sponza_thorn_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a9142674a7d4a6f94a48c5152cf0300743b597a
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_thorn_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a73a17c883cd0d0d67cfda2dc4118400a916366c05b9a5ac465f0c8b30fd9c8e
+size 635001
diff --git a/projects/bloom/resources/Sponza/vase_dif.png b/projects/bloom/resources/Sponza/vase_dif.png
new file mode 100644
index 0000000000000000000000000000000000000000..61236a81cb324af8797b05099cd264cefe189e56
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_dif.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53d06f52bf9e59df4cf00237707cca76c4f692bda61a62b06a30d321311d6dd9
+size 1842101
diff --git a/projects/bloom/resources/Sponza/vase_hanging.png b/projects/bloom/resources/Sponza/vase_hanging.png
new file mode 100644
index 0000000000000000000000000000000000000000..36a3cee71d8213225090c74f8c0dce33b9d44378
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_hanging.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9d10b4f27a3c9a78d5bac882fdd4b6a6987c262f48fa490670fe5e235951e31
+size 1432804
diff --git a/projects/bloom/resources/Sponza/vase_plant.png b/projects/bloom/resources/Sponza/vase_plant.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ad95e702e229f1ebd803e5203a266d15f2c07b9
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_plant.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2087371ff02212fb7014b6daefa191cf5676d2227193fff261a5d02f554cb8e
+size 998089
diff --git a/projects/bloom/resources/Sponza/vase_round.png b/projects/bloom/resources/Sponza/vase_round.png
new file mode 100644
index 0000000000000000000000000000000000000000..c17953abc000c44b8991e23c136c2b67348f3d1b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_round.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa23d48d492d5d4ada2ddb27d1ef22952b214e6eb3b301c65f9d88442723d20a
+size 1871399
diff --git a/projects/bloom/resources/shaders/comp.spv b/projects/bloom/resources/shaders/comp.spv
new file mode 100644
index 0000000000000000000000000000000000000000..85c7e74cfc0a89917bf6dd1a7ec449368274c1d3
Binary files /dev/null and b/projects/bloom/resources/shaders/comp.spv differ
diff --git a/projects/bloom/resources/shaders/composite.comp b/projects/bloom/resources/shaders/composite.comp
new file mode 100644
index 0000000000000000000000000000000000000000..190bed0657d70e0217bf654820d0b2b2c58f12c2
--- /dev/null
+++ b/projects/bloom/resources/shaders/composite.comp
@@ -0,0 +1,38 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          blurImage;
+layout(set=0, binding=1) uniform texture2D                          lensImage;
+layout(set=0, binding=2) uniform sampler                            linearSampler;
+layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    vec4 composite_color = vec4(0.0f);
+
+    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
+    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
+    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
+
+    // composite blur and lens features
+    float bloom_weight = 0.25f;
+    float lens_weight  = 0.25f;
+    float main_weight = 1 - (bloom_weight + lens_weight);
+
+    composite_color.rgb = blur_color * bloom_weight +
+                          lens_color * lens_weight  +
+                          main_color * main_weight;
+
+    imageStore(colorBuffer, pixel_coord, composite_color);
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/downsample.comp b/projects/bloom/resources/shaders/downsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2ab00c7c92798769153634f3479c5b7f3fb61d94
--- /dev/null
+++ b/projects/bloom/resources/shaders/downsample.comp
@@ -0,0 +1,76 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inBlurImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+    vec2  UV_offset     = UV + 0.5f * pixel_size;
+
+    vec2 color_fetches[13] = {
+        // center neighbourhood (RED)
+        vec2(-1,  1), // LT
+        vec2(-1, -1), // LB
+        vec2( 1, -1), // RB
+        vec2( 1,  1), // RT
+
+        vec2(-2, 2), // LT
+        vec2( 0, 2), // CT
+        vec2( 2, 2), // RT
+
+        vec2(0 ,-2), // LC
+        vec2(0 , 0), // CC
+        vec2(2,  0), // CR
+
+        vec2(-2, -2), // LB
+        vec2(0 , -2), // CB
+        vec2(2 , -2)  // RB
+    };
+
+    float color_weights[13] = {
+        // 0.5f
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f,
+
+        // 0.25f
+        1.f/16.f,
+        1.f/8.f,
+        1.f/16.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f
+    };
+
+    vec3 sampled_color = vec3(0.0f);
+
+    for(uint i = 0; i < 13; i++)
+    {
+        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
+        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
+        color *= color_weights[i];
+        sampled_color += color;
+    }
+
+    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/gammaCorrection.comp b/projects/bloom/resources/shaders/gammaCorrection.comp
new file mode 100644
index 0000000000000000000000000000000000000000..f89ad167c846cca8e80f69d33eda83bd6ed00d46
--- /dev/null
+++ b/projects/bloom/resources/shaders/gammaCorrection.comp
@@ -0,0 +1,20 @@
+#version 440
+
+layout(set=0, binding=0, r11f_g11f_b10f)    uniform image2D inImage;
+layout(set=0, binding=1, rgba8)             uniform image2D outImage;
+
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(inImage)))){
+        return;
+    }
+    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
+    vec3 linearColor = imageLoad(inImage, uv).rgb;
+    // cheap Reinhard tone mapping
+    linearColor = linearColor/(linearColor + 1.0f);
+    vec3 gammaCorrected = pow(linearColor, vec3(1.f / 2.2f));
+    imageStore(outImage, uv, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/lensFlares.comp b/projects/bloom/resources/shaders/lensFlares.comp
new file mode 100644
index 0000000000000000000000000000000000000000..ce27d8850b709f61332d467914ddc944dc63109f
--- /dev/null
+++ b/projects/bloom/resources/shaders/lensFlares.comp
@@ -0,0 +1,109 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          blurBuffer;
+layout(set=0, binding=1) uniform sampler                            linearSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+vec3 sampleColorChromaticAberration(vec2 _uv)
+{
+    vec2 toCenter = (vec2(0.5) - _uv);
+
+    vec3    colorScales     = vec3(-1, 0, 1);
+    float   aberrationScale = 0.1;
+    vec3 scaleFactors = colorScales * aberrationScale;
+
+    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
+    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
+    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
+    return vec3(r, g, b);
+}
+
+// _uv assumed to be flipped UV coordinates!
+vec3 ghost_vectors(vec2 _uv)
+{
+    vec2 ghost_vec = (vec2(0.5f) - _uv);
+
+    const uint c_ghost_count = 64;
+    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
+
+    ghost_vec *= c_ghost_spacing;
+
+    vec3 ret_color = vec3(0.0f);
+
+    for (uint i = 0; i < c_ghost_count; ++i)
+    {
+        // sample scene color
+        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
+        vec3 s = sampleColorChromaticAberration(s_uv);
+
+        // tint/weight
+        float d = distance(s_uv, vec2(0.5));
+        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
+        s *= weight;
+
+        ret_color += s;
+    }
+
+    ret_color /= c_ghost_count;
+    return ret_color;
+}
+
+vec3 halo(vec2 _uv)
+{
+    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
+    const float c_radius = 0.6f;
+    const float c_halo_thickness = 0.1f;
+
+    vec2 halo_vec = vec2(0.5) - _uv;
+    //halo_vec.x /= c_aspect_ratio;
+    halo_vec = normalize(halo_vec);
+    //halo_vec.x *= c_aspect_ratio;
+
+
+    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
+    vec2 w_uv = _uv;
+    float d = distance(w_uv, vec2(0.5)); // distance to center
+
+    float distance_to_halo = abs(d - c_radius);
+
+    float halo_weight = 0.0f;
+    if(abs(d - c_radius) <= c_halo_thickness)
+    {
+        float distance_to_border = c_halo_thickness - distance_to_halo;
+        halo_weight = distance_to_border / c_halo_thickness;
+
+        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
+        halo_weight = pow(halo_weight, 2.0f);
+
+
+        //halo_weight = 1.0f;
+    }
+
+    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
+}
+
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    vec2 flipped_UV = vec2(1.0f) - UV;
+
+    vec3 color = vec3(0.0f);
+
+    color += ghost_vectors(flipped_UV);
+    color += halo(UV);
+    color  *= 0.5f;
+
+    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/perMeshResources.inc b/projects/bloom/resources/shaders/perMeshResources.inc
new file mode 100644
index 0000000000000000000000000000000000000000..95e4fb7c27009965659d14a9c72acfec950c37e3
--- /dev/null
+++ b/projects/bloom/resources/shaders/perMeshResources.inc
@@ -0,0 +1,2 @@
+layout(set=1, binding=0) uniform texture2D  albedoTexture;
+layout(set=1, binding=1) uniform sampler    textureSampler;
\ No newline at end of file
diff --git a/projects/cmd_sync_test/resources/shaders/shader.frag b/projects/bloom/resources/shaders/shader.frag
similarity index 69%
rename from projects/cmd_sync_test/resources/shaders/shader.frag
rename to projects/bloom/resources/shaders/shader.frag
index 95f1b3319e1ca5c7c34ff94e5e7198819c0233c1..3e95b4508f112c1ed9aa4a7050a98fa789dccd09 100644
--- a/projects/cmd_sync_test/resources/shaders/shader.frag
+++ b/projects/bloom/resources/shaders/shader.frag
@@ -1,5 +1,8 @@
 #version 450
 #extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "perMeshResources.inc"
 
 layout(location = 0) in vec3 passNormal;
 layout(location = 1) in vec2 passUV;
@@ -7,14 +10,12 @@ layout(location = 2) in vec3 passPos;
 
 layout(location = 0) out vec3 outColor;
 
-layout(set=0, binding=0) uniform texture2D  meshTexture;
-layout(set=0, binding=1) uniform sampler    textureSampler;
-layout(set=0, binding=2) uniform sunBuffer {
+layout(set=0, binding=0) uniform sunBuffer {
     vec3 L; float padding;
     mat4 lightMatrix;
 };
-layout(set=0, binding=3) uniform texture2D  shadowMap;
-layout(set=0, binding=4) uniform sampler    shadowMapSampler;
+layout(set=0, binding=1) uniform texture2D  shadowMap;
+layout(set=0, binding=2) uniform sampler    shadowMapSampler;
 
 float shadowTest(vec3 worldPos){
     vec4 lightPos = lightMatrix * vec4(worldPos, 1);
@@ -35,10 +36,10 @@ float shadowTest(vec3 worldPos){
 
 void main()	{
     vec3 N = normalize(passNormal);
-    vec3 sunColor = vec3(1);
+    vec3 sunColor = vec3(10);
     vec3 sun = sunColor * clamp(dot(N, L), 0, 1);
     sun *= shadowTest(passPos);
-    vec3 ambient = vec3(0.1);
-    vec3 albedo = texture(sampler2D(meshTexture, textureSampler), passUV).rgb;
+    vec3 ambient = vec3(0.05);
+    vec3 albedo = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
 	outColor = albedo * (sun + ambient);
 }
\ No newline at end of file
diff --git a/projects/cmd_sync_test/resources/shaders/shader.vert b/projects/bloom/resources/shaders/shader.vert
similarity index 82%
rename from projects/cmd_sync_test/resources/shaders/shader.vert
rename to projects/bloom/resources/shaders/shader.vert
index 0ab82c203806356d0f35dc52c0a6988b286d90d1..926f86af2860cb57c44d2d5ee78712b6ae155e5c 100644
--- a/projects/cmd_sync_test/resources/shaders/shader.vert
+++ b/projects/bloom/resources/shaders/shader.vert
@@ -16,7 +16,7 @@ layout( push_constant ) uniform constants{
 
 void main()	{
 	gl_Position = mvp * vec4(inPosition, 1.0);
-	passNormal  = inNormal;
+	passNormal  = mat3(model) * inNormal;    // assuming no weird stuff like shearing or non-uniform scaling
     passUV      = inUV;
     passPos     = (model * vec4(inPosition, 1)).xyz;
 }
\ No newline at end of file
diff --git a/projects/cmd_sync_test/resources/shaders/shadow.frag b/projects/bloom/resources/shaders/shadow.frag
similarity index 100%
rename from projects/cmd_sync_test/resources/shaders/shadow.frag
rename to projects/bloom/resources/shaders/shadow.frag
diff --git a/projects/cmd_sync_test/resources/shaders/shadow.vert b/projects/bloom/resources/shaders/shadow.vert
similarity index 100%
rename from projects/cmd_sync_test/resources/shaders/shadow.vert
rename to projects/bloom/resources/shaders/shadow.vert
diff --git a/projects/bloom/resources/shaders/upsample.comp b/projects/bloom/resources/shaders/upsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..0ddeedb5b5af9e476dc19012fed6430544006c0e
--- /dev/null
+++ b/projects/bloom/resources/shaders/upsample.comp
@@ -0,0 +1,45 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
+        return;
+    }
+
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
+    const float gauss_weight = 16.f;
+
+    vec3 sampled_color = vec3(0.f);
+
+    for(int i = -1; i <= 1; i++)
+    {
+        for(int j = -1; j <= 1; j++)
+        {
+            vec2 sample_location = UV + vec2(j, i) * pixel_size;
+            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
+            color *= gauss_kernel[j+1];
+            color *= gauss_kernel[i+1];
+            color /= gauss_weight;
+
+            sampled_color += color;
+        }
+    }
+
+    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
+    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
+    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
+
+    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/bloom/src/BloomAndFlares.cpp b/projects/bloom/src/BloomAndFlares.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6f26db9de0f2c8334b6dd7e5dd6cf4b6f48baedc
--- /dev/null
+++ b/projects/bloom/src/BloomAndFlares.cpp
@@ -0,0 +1,274 @@
+#include "BloomAndFlares.hpp"
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+BloomAndFlares::BloomAndFlares(
+        vkcv::Core *p_Core,
+        vk::Format colorBufferFormat,
+        uint32_t width,
+        uint32_t height) :
+
+        p_Core(p_Core),
+        m_ColorBufferFormat(colorBufferFormat),
+        m_Width(width),
+        m_Height(height),
+        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
+                                              vkcv::SamplerFilterType::LINEAR,
+                                              vkcv::SamplerMipmapMode::LINEAR,
+                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
+        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
+        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
+{
+    vkcv::shader::GLSLCompiler compiler;
+
+    // DOWNSAMPLE
+    vkcv::ShaderProgram dsProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "resources/shaders/downsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         dsProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+		m_DownsampleDescSets.push_back(
+                p_Core->createDescriptorSet(dsProg.getReflectedDescriptors()[0]));
+    }
+    m_DownsamplePipe = p_Core->createComputePipeline(
+            dsProg, { p_Core->getDescriptorSet(m_DownsampleDescSets[0]).layout });
+
+    // UPSAMPLE
+    vkcv::ShaderProgram usProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "resources/shaders/upsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         usProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+        m_UpsampleDescSets.push_back(
+                p_Core->createDescriptorSet(usProg.getReflectedDescriptors()[0]));
+    }
+    m_UpsamplePipe = p_Core->createComputePipeline(
+            usProg, { p_Core->getDescriptorSet(m_UpsampleDescSets[0]).layout });
+
+    // LENS FEATURES
+    vkcv::ShaderProgram lensProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "resources/shaders/lensFlares.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         lensProg.addShader(shaderStage, path);
+                     });
+    m_LensFlareDescSet = p_Core->createDescriptorSet(lensProg.getReflectedDescriptors()[0]);
+    m_LensFlarePipe = p_Core->createComputePipeline(
+            lensProg, { p_Core->getDescriptorSet(m_LensFlareDescSet).layout });
+
+    // COMPOSITE
+    vkcv::ShaderProgram compProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "resources/shaders/composite.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         compProg.addShader(shaderStage, path);
+                     });
+    m_CompositeDescSet = p_Core->createDescriptorSet(compProg.getReflectedDescriptors()[0]);
+    m_CompositePipe = p_Core->createComputePipeline(
+            compProg, { p_Core->getDescriptorSet(m_CompositeDescSet).layout });
+}
+
+void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
+                                        const vkcv::ImageHandle &colorAttachment)
+{
+    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
+    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+    // blur dispatch
+    uint32_t initialDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+
+    // downsample dispatch of original color attachment
+    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    vkcv::DescriptorWrites initialDownsampleWrites;
+    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
+    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
+    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
+
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_DownsamplePipe,
+            initialDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 0));
+
+    // downsample dispatches of blur buffer's mip maps
+    float mipDispatchCountX = dispatchCountX;
+    float mipDispatchCountY = dispatchCountY;
+    for(uint32_t mipLevel = 1; mipLevel < m_DownsampleDescSets.size(); mipLevel++)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipDescriptorWrites;
+        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
+        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
+        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
+
+        // mip dispatch calculation
+        mipDispatchCountX  /= 2.0f;
+        mipDispatchCountY /= 2.0f;
+
+        uint32_t mipDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
+                1
+        };
+
+        if(mipDispatchCount[0] == 0)
+            mipDispatchCount[0] = 1;
+        if(mipDispatchCount[1] == 0)
+            mipDispatchCount[1] = 1;
+
+        // mip blur dispatch
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_DownsamplePipe,
+                mipDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstantData(nullptr, 0));
+
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // upsample dispatch
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    const uint32_t upsampleMipLevels = std::min(
+    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
+    		static_cast<uint32_t>(5)
+	);
+
+    // upsample dispatch for each mip map
+    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipUpsampleWrites;
+        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
+        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
+        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
+
+        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
+
+        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
+        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
+        upsampleDispatchX /= 8.0f;
+        upsampleDispatchY /= 8.0f;
+
+        const uint32_t upsampleDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
+                1
+        };
+
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_UpsamplePipe,
+                upsampleDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstantData(nullptr, 0)
+        );
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // lens feature generation descriptor writes
+    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
+    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
+
+    vkcv::DescriptorWrites lensFeatureWrites;
+    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
+    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
+    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
+
+    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
+    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+    // lens feature generation dispatch
+    uint32_t lensFeatureDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_LensFlarePipe,
+            lensFeatureDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 0));
+}
+
+void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
+                                       const vkcv::ImageHandle &colorAttachment)
+{
+    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
+    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
+    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
+
+    // bloom composite descriptor write
+    vkcv::DescriptorWrites compositeWrites;
+    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
+                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
+    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
+    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
+    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
+
+    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
+    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+
+    uint32_t compositeDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+
+    // bloom composite dispatch
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_CompositePipe,
+            compositeDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 0));
+}
+
+void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
+                                       const vkcv::ImageHandle &colorAttachment)
+{
+    execDownsamplePipe(cmdStream, colorAttachment);
+    execUpsamplePipe(cmdStream);
+    execLensFeaturePipe(cmdStream);
+    execCompositePipe(cmdStream, colorAttachment);
+}
+
+void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
+{
+    m_Width  = width;
+    m_Height = height;
+
+    p_Core->getContext().getDevice().waitIdle();
+    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
+    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
+}
+
+
diff --git a/projects/bloom/src/BloomAndFlares.hpp b/projects/bloom/src/BloomAndFlares.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..756b1ca154ea5232df04eb09a88bb743c5bd28aa
--- /dev/null
+++ b/projects/bloom/src/BloomAndFlares.hpp
@@ -0,0 +1,47 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include <glm/glm.hpp>
+
+class BloomAndFlares{
+public:
+    BloomAndFlares(vkcv::Core *p_Core,
+                   vk::Format colorBufferFormat,
+                   uint32_t width,
+                   uint32_t height);
+
+    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+
+    void updateImageDimensions(uint32_t width, uint32_t height);
+
+private:
+    vkcv::Core *p_Core;
+
+    vk::Format m_ColorBufferFormat;
+    uint32_t m_Width;
+    uint32_t m_Height;
+
+    vkcv::SamplerHandle m_LinearSampler;
+    vkcv::Image m_Blur;
+    vkcv::Image m_LensFeatures;
+
+
+    vkcv::PipelineHandle                     m_DownsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_DownsampleDescSets; // per mip desc set
+
+    vkcv::PipelineHandle                     m_UpsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_UpsampleDescSets;   // per mip desc set
+
+    vkcv::PipelineHandle                     m_LensFlarePipe;
+    vkcv::DescriptorSetHandle                m_LensFlareDescSet;
+
+    vkcv::PipelineHandle                     m_CompositePipe;
+    vkcv::DescriptorSetHandle                m_CompositeDescSet;
+
+    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+};
+
+
+
diff --git a/projects/bloom/src/main.cpp b/projects/bloom/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7a17a51f1c7d638575c0b5aafcdca49b589533ef
--- /dev/null
+++ b/projects/bloom/src/main.cpp
@@ -0,0 +1,419 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/Logger.hpp>
+#include "BloomAndFlares.hpp"
+#include <glm/glm.hpp>
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "Bloom";
+
+	uint32_t windowWidth = 1920;
+	uint32_t windowHeight = 1080;
+	
+	vkcv::Window window = vkcv::Window::create(
+		applicationName,
+		windowWidth,
+		windowHeight,
+		true
+	);
+
+    vkcv::camera::CameraManager cameraManager(window);
+    uint32_t camIndex = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+    uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+    
+    cameraManager.getCamera(camIndex).setPosition(glm::vec3(0.f, 0.f, 3.f));
+    cameraManager.getCamera(camIndex).setNearFar(0.1f, 30.0f);
+	cameraManager.getCamera(camIndex).setYaw(180.0f);
+	
+	cameraManager.getCamera(camIndex2).setNearFar(0.1f, 30.0f);
+
+	vkcv::Core core = vkcv::Core::create(
+		window,
+		applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
+		{},
+		{ "VK_KHR_swapchain" }
+	);
+
+	const char* path = argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf";
+	vkcv::asset::Scene scene;
+	int result = vkcv::asset::loadScene(path, scene);
+
+	if (result == 1) {
+		std::cout << "Scene loading successful!" << std::endl;
+	}
+	else {
+		std::cout << "Scene loading failed: " << result << std::endl;
+		return 1;
+	}
+
+	// build index and vertex buffers
+	assert(!scene.vertexGroups.empty());
+	std::vector<std::vector<uint8_t>> vBuffers;
+	std::vector<std::vector<uint8_t>> iBuffers;
+
+	std::vector<vkcv::VertexBufferBinding> vBufferBindings;
+	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
+	std::vector<vkcv::asset::VertexAttribute> vAttributes;
+
+	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+
+		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
+		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);
+
+		auto& attributes = scene.vertexGroups[i].vertexBuffer.attributes;
+
+		std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
+			return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
+		});
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> vertexBuffers;
+	for (const vkcv::asset::VertexGroup& group : scene.vertexGroups) {
+		vertexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::VERTEX,
+			group.vertexBuffer.data.size()));
+		vertexBuffers.back().fill(group.vertexBuffer.data);
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> indexBuffers;
+	for (const auto& dataBuffer : iBuffers) {
+		indexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::INDEX,
+			dataBuffer.size()));
+		indexBuffers.back().fill(dataBuffer);
+	}
+
+	int vertexBufferIndex = 0;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
+			vAttributes.push_back(attribute);
+			vBufferBindings.push_back(vkcv::VertexBufferBinding(attribute.offset, vertexBuffers[vertexBufferIndex].getVulkanHandle()));
+		}
+		vertexBufferBindings.push_back(vBufferBindings);
+		vBufferBindings.clear();
+		vertexBufferIndex++;
+	}
+
+	const vk::Format colorBufferFormat = vk::Format::eB10G11R11UfloatPack32;
+	const vkcv::AttachmentDescription color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		colorBufferFormat
+	);
+	
+	const vk::Format depthBufferFormat = vk::Format::eD32Sfloat;
+	const vkcv::AttachmentDescription depth_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		depthBufferFormat
+	);
+
+	vkcv::PassConfig forwardPassDefinition({ color_attachment, depth_attachment });
+	vkcv::PassHandle forwardPass = core.createPass(forwardPassDefinition);
+
+	vkcv::shader::GLSLCompiler compiler;
+
+	vkcv::ShaderProgram forwardProgram;
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"), 
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		forwardProgram.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		forwardProgram.addShader(shaderStage, path);
+	});
+
+	const std::vector<vkcv::VertexAttachment> vertexAttachments = forwardProgram.getVertexAttachments();
+
+	std::vector<vkcv::VertexBinding> vertexBindings;
+	for (size_t i = 0; i < vertexAttachments.size(); i++) {
+		vertexBindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+	}
+	const vkcv::VertexLayout vertexLayout (vertexBindings);
+
+	// shadow map
+	vkcv::SamplerHandle shadowSampler = core.createSampler(
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerMipmapMode::NEAREST,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
+	const vk::Format shadowMapFormat = vk::Format::eD16Unorm;
+	const uint32_t shadowMapResolution = 1024;
+	const vkcv::Image shadowMap = core.createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1);
+
+	// light info buffer
+	struct LightInfo {
+		glm::vec3 direction;
+		float padding;
+		glm::mat4 lightMatrix;
+	};
+	LightInfo lightInfo;
+	vkcv::Buffer lightBuffer = core.createBuffer<LightInfo>(vkcv::BufferType::UNIFORM, sizeof(glm::vec3));
+
+	vkcv::DescriptorSetHandle forwardShadingDescriptorSet = 
+		core.createDescriptorSet({ forwardProgram.getReflectedDescriptors()[0] });
+
+	vkcv::DescriptorWrites forwardDescriptorWrites;
+	forwardDescriptorWrites.uniformBufferWrites = { vkcv::UniformBufferDescriptorWrite(0, lightBuffer.getHandle()) };
+	forwardDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(1, shadowMap.getHandle()) };
+	forwardDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(2, shadowSampler) };
+	core.writeDescriptorSet(forwardShadingDescriptorSet, forwardDescriptorWrites);
+
+	vkcv::SamplerHandle colorSampler = core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::REPEAT
+	);
+
+	// prepare per mesh descriptor sets
+	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
+	std::vector<vkcv::Image> sceneImages;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		perMeshDescriptorSets.push_back(core.createDescriptorSet(forwardProgram.getReflectedDescriptors()[1]));
+
+		const auto& material = scene.materials[vertexGroup.materialIndex];
+
+		int baseColorIndex = material.baseColor;
+		if (baseColorIndex < 0) {
+			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks base color");
+			baseColorIndex = 0;
+		}
+
+		vkcv::asset::Texture& sceneTexture = scene.textures[baseColorIndex];
+
+		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, sceneTexture.w, sceneTexture.h));
+		sceneImages.back().fill(sceneTexture.data.data());
+
+		vkcv::DescriptorWrites setWrites;
+		setWrites.sampledImageWrites = {
+			vkcv::SampledImageDescriptorWrite(0, sceneImages.back().getHandle())
+		};
+		setWrites.samplerWrites = {
+			vkcv::SamplerDescriptorWrite(1, colorSampler),
+		};
+		core.writeDescriptorSet(perMeshDescriptorSets.back(), setWrites);
+	}
+
+	const vkcv::PipelineConfig forwardPipelineConfig {
+		forwardProgram,
+		windowWidth,
+		windowHeight,
+		forwardPass,
+		vertexLayout,
+		{	core.getDescriptorSet(forwardShadingDescriptorSet).layout, 
+			core.getDescriptorSet(perMeshDescriptorSets[0]).layout },
+		true
+	};
+	
+	vkcv::PipelineHandle forwardPipeline = core.createGraphicsPipeline(forwardPipelineConfig);
+	
+	if (!forwardPipeline) {
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::ImageHandle depthBuffer       = core.createImage(depthBufferFormat, windowWidth, windowHeight).getHandle();
+	vkcv::ImageHandle colorBuffer       = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
+
+	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+	vkcv::ShaderProgram shadowShader;
+	compiler.compile(vkcv::ShaderStage::VERTEX, "resources/shaders/shadow.vert",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shadowShader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, "resources/shaders/shadow.frag",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shadowShader.addShader(shaderStage, path);
+	});
+
+	const std::vector<vkcv::AttachmentDescription> shadowAttachments = {
+		vkcv::AttachmentDescription(vkcv::AttachmentOperation::STORE, vkcv::AttachmentOperation::CLEAR, shadowMapFormat)
+	};
+	const vkcv::PassConfig shadowPassConfig(shadowAttachments);
+	const vkcv::PassHandle shadowPass = core.createPass(shadowPassConfig);
+	const vkcv::PipelineConfig shadowPipeConfig{
+		shadowShader,
+		shadowMapResolution,
+		shadowMapResolution,
+		shadowPass,
+		vertexLayout,
+		{},
+		false
+	};
+	const vkcv::PipelineHandle shadowPipe = core.createGraphicsPipeline(shadowPipeConfig);
+
+	std::vector<std::array<glm::mat4, 2>> mainPassMatrices;
+	std::vector<glm::mat4> mvpLight;
+
+	// gamma correction compute shader
+	vkcv::ShaderProgram gammaCorrectionProgram;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/gammaCorrection.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		gammaCorrectionProgram.addShader(shaderStage, path);
+	});
+	vkcv::DescriptorSetHandle gammaCorrectionDescriptorSet = core.createDescriptorSet(gammaCorrectionProgram.getReflectedDescriptors()[0]);
+	vkcv::PipelineHandle gammaCorrectionPipeline = core.createComputePipeline(gammaCorrectionProgram,
+		{ core.getDescriptorSet(gammaCorrectionDescriptorSet).layout });
+
+    BloomAndFlares baf(&core, colorBufferFormat, windowWidth, windowHeight);
+
+
+	// model matrices per mesh
+	std::vector<glm::mat4> modelMatrices;
+	modelMatrices.resize(scene.vertexGroups.size(), glm::mat4(1.f));
+	for (const auto& mesh : scene.meshes) {
+		const glm::mat4 m = *reinterpret_cast<const glm::mat4*>(&mesh.modelMatrix[0]);
+		for (const auto& vertexGroupIndex : mesh.vertexGroups) {
+			modelMatrices[vertexGroupIndex] = m;
+		}
+	}
+
+	// prepare drawcalls
+	std::vector<vkcv::Mesh> meshes;
+	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+		vkcv::Mesh mesh(
+			vertexBufferBindings[i], 
+			indexBuffers[i].getVulkanHandle(), 
+			scene.vertexGroups[i].numIndices);
+		meshes.push_back(mesh);
+	}
+
+	std::vector<vkcv::DrawcallInfo> drawcalls;
+	std::vector<vkcv::DrawcallInfo> shadowDrawcalls;
+	for (int i = 0; i < meshes.size(); i++) {
+		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
+			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
+			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+		shadowDrawcalls.push_back(vkcv::DrawcallInfo(meshes[i], {}));
+	}
+
+	auto start = std::chrono::system_clock::now();
+	const auto appStartTime = start;
+	while (window.isWindowOpen()) {
+		vkcv::Window::pollEvents();
+
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+			continue;
+		}
+
+		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
+			depthBuffer = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight).getHandle();
+			colorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
+
+			baf.updateImageDimensions(swapchainWidth, swapchainHeight);
+
+			windowWidth = swapchainWidth;
+			windowHeight = swapchainHeight;
+		}
+
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
+
+		start = end;
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+
+		auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - appStartTime);
+		
+		const float sunTheta = 0.0001f * static_cast<float>(duration.count());
+		lightInfo.direction = glm::normalize(glm::vec3(std::cos(sunTheta), 1, std::sin(sunTheta)));
+
+		const float shadowProjectionSize = 20.f;
+		glm::mat4 projectionLight = glm::ortho(
+			-shadowProjectionSize,
+			shadowProjectionSize,
+			-shadowProjectionSize,
+			shadowProjectionSize,
+			-shadowProjectionSize,
+			shadowProjectionSize);
+
+		glm::mat4 vulkanCorrectionMatrix(1.f);
+		vulkanCorrectionMatrix[2][2] = 0.5;
+		vulkanCorrectionMatrix[3][2] = 0.5;
+		projectionLight = vulkanCorrectionMatrix * projectionLight;
+
+		const glm::mat4 viewLight = glm::lookAt(glm::vec3(0), -lightInfo.direction, glm::vec3(0, -1, 0));
+
+		lightInfo.lightMatrix = projectionLight * viewLight;
+		lightBuffer.fill({ lightInfo });
+
+		const glm::mat4 viewProjectionCamera = cameraManager.getActiveCamera().getMVP();
+
+		mainPassMatrices.clear();
+		mvpLight.clear();
+		for (const auto& m : modelMatrices) {
+			mainPassMatrices.push_back({ viewProjectionCamera * m, m });
+			mvpLight.push_back(lightInfo.lightMatrix * m);
+		}
+
+		vkcv::PushConstantData pushConstantData((void*)mainPassMatrices.data(), 2 * sizeof(glm::mat4));
+		const std::vector<vkcv::ImageHandle> renderTargets = { colorBuffer, depthBuffer };
+
+		const vkcv::PushConstantData shadowPushConstantData((void*)mvpLight.data(), sizeof(glm::mat4));
+
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		// shadow map
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			shadowPass,
+			shadowPipe,
+			shadowPushConstantData,
+			shadowDrawcalls,
+			{ shadowMap.getHandle() });
+		core.prepareImageForSampling(cmdStream, shadowMap.getHandle());
+
+		// main pass
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+            forwardPass,
+            forwardPipeline,
+			pushConstantData,
+			drawcalls,
+			renderTargets);
+
+        const uint32_t gammaCorrectionLocalGroupSize = 8;
+        const uint32_t gammaCorrectionDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(static_cast<float>(windowWidth) / static_cast<float>(gammaCorrectionLocalGroupSize))),
+                static_cast<uint32_t>(glm::ceil(static_cast<float>(windowHeight) / static_cast<float>(gammaCorrectionLocalGroupSize))),
+                1
+        };
+
+        baf.execWholePipeline(cmdStream, colorBuffer);
+
+        core.prepareImageForStorage(cmdStream, swapchainInput);
+        
+        // gamma correction descriptor write
+        vkcv::DescriptorWrites gammaCorrectionDescriptorWrites;
+        gammaCorrectionDescriptorWrites.storageImageWrites = {
+                vkcv::StorageImageDescriptorWrite(0, colorBuffer),
+                vkcv::StorageImageDescriptorWrite(1, swapchainInput) };
+        core.writeDescriptorSet(gammaCorrectionDescriptorSet, gammaCorrectionDescriptorWrites);
+
+        // gamma correction dispatch
+        core.recordComputeDispatchToCmdStream(
+			cmdStream, 
+			gammaCorrectionPipeline, 
+			gammaCorrectionDispatchCount,
+			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(gammaCorrectionDescriptorSet).vulkanHandle) },
+			vkcv::PushConstantData(nullptr, 0));
+
+		// present and end
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+
+		core.endFrame();
+	}
+	
+	return 0;
+}
diff --git a/projects/cmd_sync_test/.gitignore b/projects/cmd_sync_test/.gitignore
deleted file mode 100644
index 16f72da367245ad14a38ee756816f06f8cbbe3d2..0000000000000000000000000000000000000000
--- a/projects/cmd_sync_test/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-cmd_sync_test
\ No newline at end of file
diff --git a/projects/cmd_sync_test/resources/shaders/compile.bat b/projects/cmd_sync_test/resources/shaders/compile.bat
deleted file mode 100644
index 516c2f2f78001e1a5d182356e7c3fe82d66a45ee..0000000000000000000000000000000000000000
--- a/projects/cmd_sync_test/resources/shaders/compile.bat
+++ /dev/null
@@ -1,5 +0,0 @@
-%VULKAN_SDK%\Bin32\glslc.exe shader.vert -o vert.spv
-%VULKAN_SDK%\Bin32\glslc.exe shader.frag -o frag.spv
-%VULKAN_SDK%\Bin32\glslc.exe shadow.vert -o shadow_vert.spv
-%VULKAN_SDK%\Bin32\glslc.exe shadow.frag -o shadow_frag.spv
-pause
\ No newline at end of file
diff --git a/projects/cmd_sync_test/resources/shaders/frag.spv b/projects/cmd_sync_test/resources/shaders/frag.spv
deleted file mode 100644
index ff3110571871d65ce119dc6c5006e7e67aa53546..0000000000000000000000000000000000000000
Binary files a/projects/cmd_sync_test/resources/shaders/frag.spv and /dev/null differ
diff --git a/projects/cmd_sync_test/resources/shaders/shadow_frag.spv b/projects/cmd_sync_test/resources/shaders/shadow_frag.spv
deleted file mode 100644
index 6be3bd2518a3b1f234e39aea2503ba86cfb3314b..0000000000000000000000000000000000000000
Binary files a/projects/cmd_sync_test/resources/shaders/shadow_frag.spv and /dev/null differ
diff --git a/projects/cmd_sync_test/resources/shaders/shadow_vert.spv b/projects/cmd_sync_test/resources/shaders/shadow_vert.spv
deleted file mode 100644
index afaa0824ee9be2c22209d611943c6512587dce24..0000000000000000000000000000000000000000
Binary files a/projects/cmd_sync_test/resources/shaders/shadow_vert.spv and /dev/null differ
diff --git a/projects/cmd_sync_test/resources/shaders/vert.spv b/projects/cmd_sync_test/resources/shaders/vert.spv
deleted file mode 100644
index 5e514eef5983927316465679af5461f507497130..0000000000000000000000000000000000000000
Binary files a/projects/cmd_sync_test/resources/shaders/vert.spv and /dev/null differ
diff --git a/projects/cmd_sync_test/src/main.cpp b/projects/cmd_sync_test/src/main.cpp
index 9946a35373576d3690d22bf18a03cd8e52b15e56..5d56a94579c43369f01a5718276e8bb405d77eac 100644
--- a/projects/cmd_sync_test/src/main.cpp
+++ b/projects/cmd_sync_test/src/main.cpp
@@ -18,7 +18,7 @@ int main(int argc, const char** argv) {
 		true
 	);
 
-    vkcv::camera::CameraManager cameraManager(window, windowWidth, windowHeight);
+    vkcv::camera::CameraManager cameraManager(window);
     uint32_t camIndex = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
     uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
     
@@ -37,10 +37,10 @@ int main(int argc, const char** argv) {
 		{ "VK_KHR_swapchain" }
 	);
 
-	vkcv::asset::Mesh mesh;
+	vkcv::asset::Scene mesh;
 
 	const char* path = argc > 1 ? argv[1] : "resources/cube/cube.gltf";
-	int result = vkcv::asset::loadMesh(path, mesh);
+	int result = vkcv::asset::loadScene(path, mesh);
 
 	if (result == 1) {
 		std::cout << "Mesh loading successful!" << std::endl;
@@ -84,7 +84,7 @@ int main(int argc, const char** argv) {
 	const vkcv::AttachmentDescription present_color_attachment(
 		vkcv::AttachmentOperation::STORE,
 		vkcv::AttachmentOperation::CLEAR,
-		core.getSwapchainImageFormat()
+		core.getSwapchain().getFormat()
 	);
 	
 	const vkcv::AttachmentDescription depth_attachment(
@@ -117,14 +117,16 @@ int main(int argc, const char** argv) {
 	std::vector<vkcv::DescriptorBinding> descriptorBindings = { firstMeshProgram.getReflectedDescriptors()[0] };
 	vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(descriptorBindings);
 
-	const vkcv::PipelineConfig firstMeshPipelineConfig(
+	const vkcv::PipelineConfig firstMeshPipelineConfig {
         firstMeshProgram,
 		windowWidth,
 		windowHeight,
         firstMeshPass,
-        {firstMeshLayout},
+        firstMeshLayout,
 		{ core.getDescriptorSet(descriptorSet).layout },
-		true);
+		true
+	};
+	
 	vkcv::PipelineHandle firstMeshPipeline = core.createGraphicsPipeline(firstMeshPipelineConfig);
 	
 	if (!firstMeshPipeline) {
@@ -132,8 +134,11 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 	
-	vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, mesh.texture_hack.w, mesh.texture_hack.h);
-	texture.fill(mesh.texture_hack.img);
+	//vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, mesh.texture_hack.w, mesh.texture_hack.h);
+	//texture.fill(mesh.texture_hack.img);
+    vkcv::asset::Texture &tex = mesh.textures[0];
+    vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, tex.w, tex.h);
+    texture.fill(tex.data.data());
 
 	vkcv::SamplerHandle sampler = core.createSampler(
 		vkcv::SamplerFilterType::LINEAR,
@@ -168,8 +173,8 @@ int main(int argc, const char** argv) {
 	std::vector<vkcv::DrawcallInfo> shadowDrawcalls;
 	for (const auto& position : instancePositions) {
 		modelMatrices.push_back(glm::translate(glm::mat4(1.f), position));
-		drawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, { descriptorUsage }));
-		shadowDrawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, {}));
+		drawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, { descriptorUsage },1));
+		shadowDrawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, {},1));
 	}
 
 	modelMatrices.back() *= glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
@@ -190,14 +195,16 @@ int main(int argc, const char** argv) {
 
 	const uint32_t shadowMapResolution = 1024;
 	const vkcv::Image shadowMap = core.createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1);
-	const vkcv::PipelineConfig shadowPipeConfig(
+	const vkcv::PipelineConfig shadowPipeConfig {
 		shadowShader, 
 		shadowMapResolution, 
 		shadowMapResolution, 
 		shadowPass,
-        {firstMeshLayout},
-		{}, 
-		false);
+        firstMeshLayout,
+		{},
+		false
+	};
+	
 	const vkcv::PipelineHandle shadowPipe = core.createGraphicsPipeline(shadowPipeConfig);
 
 	struct LightInfo {
@@ -221,7 +228,7 @@ int main(int argc, const char** argv) {
 	auto start = std::chrono::system_clock::now();
 	const auto appStartTime = start;
 	while (window.isWindowOpen()) {
-		vkcv::Window::pollEvents();
+		window.pollEvents();
 		
 		uint32_t swapchainWidth, swapchainHeight;
 		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
diff --git a/projects/first_mesh/resources/Szene/Szene.bin b/projects/first_mesh/resources/Szene/Szene.bin
new file mode 100644
index 0000000000000000000000000000000000000000..c87d27637516b0bbf864251dd162773f5cc53e06
--- /dev/null
+++ b/projects/first_mesh/resources/Szene/Szene.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee4742718f720d589a2a03f5d879f8c50ba9057718d191a43b046eaa9071080d
+size 70328
diff --git a/projects/first_mesh/resources/Szene/Szene.gltf b/projects/first_mesh/resources/Szene/Szene.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..e5a32b29af5d0a2ac5f497e60b4b92c1873e1df9
--- /dev/null
+++ b/projects/first_mesh/resources/Szene/Szene.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:75ba834118792ebbacf528a1690c7d04df4b4c8122b9f99a9aa9a9d075d2c86a
+size 7421
diff --git a/projects/cmd_sync_test/resources/cube/boards2_vcyc_jpg.jpg b/projects/first_mesh/resources/Szene/boards2_vcyc.jpg
similarity index 100%
rename from projects/cmd_sync_test/resources/cube/boards2_vcyc_jpg.jpg
rename to projects/first_mesh/resources/Szene/boards2_vcyc.jpg
diff --git a/projects/first_mesh/resources/Szene/boards2_vcyc_jpg.jpg b/projects/first_mesh/resources/Szene/boards2_vcyc_jpg.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..2636039e272289c0fba3fa2d88a060b857501248
--- /dev/null
+++ b/projects/first_mesh/resources/Szene/boards2_vcyc_jpg.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cca33a6e58ddd1b37a6e6853a9aa0e7b15ca678937119194752393dd2a0a0564
+size 1192476
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index 7c32a76dc6f4b2fe320df733e65a54a88e3a42c1..e7546fc3a143b3638cceb36869c519336ebec751 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -27,10 +27,10 @@ int main(int argc, const char** argv) {
 		{ "VK_KHR_swapchain" }
 	);
 
-	vkcv::asset::Mesh mesh;
+	vkcv::asset::Scene mesh;
 
 	const char* path = argc > 1 ? argv[1] : "resources/cube/cube.gltf";
-	int result = vkcv::asset::loadMesh(path, mesh);
+	int result = vkcv::asset::loadScene(path, mesh);
 
 	if (result == 1) {
 		std::cout << "Mesh loading successful!" << std::endl;
@@ -61,7 +61,7 @@ int main(int argc, const char** argv) {
 	const vkcv::AttachmentDescription present_color_attachment(
 		vkcv::AttachmentOperation::STORE,
 		vkcv::AttachmentOperation::CLEAR,
-		core.getSwapchainImageFormat()
+		core.getSwapchain().getFormat()
 	);
 	
 	const vkcv::AttachmentDescription depth_attachment(
@@ -83,6 +83,7 @@ int main(int argc, const char** argv) {
     firstMeshProgram.addShader(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/frag.spv"));
 	
 	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
+
 	
 	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
 		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
@@ -100,14 +101,15 @@ int main(int argc, const char** argv) {
 	std::vector<vkcv::DescriptorBinding> descriptorBindings = { firstMeshProgram.getReflectedDescriptors()[setID] };
 	vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(descriptorBindings);
 
-	const vkcv::PipelineConfig firstMeshPipelineConfig(
+	const vkcv::PipelineConfig firstMeshPipelineConfig {
         firstMeshProgram,
         UINT32_MAX,
         UINT32_MAX,
         firstMeshPass,
         {firstMeshLayout},
 		{ core.getDescriptorSet(descriptorSet).layout },
-		true);
+		true
+	};
 	vkcv::PipelineHandle firstMeshPipeline = core.createGraphicsPipeline(firstMeshPipelineConfig);
 	
 	if (!firstMeshPipeline) {
@@ -115,8 +117,13 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 	
-	vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, mesh.texture_hack.w, mesh.texture_hack.h);
-	texture.fill(mesh.texture_hack.img);
+	// FIXME There should be a test here to make sure there is at least 1
+	// texture in the mesh.
+	vkcv::asset::Texture &tex = mesh.textures[0];
+	vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, tex.w, tex.h);
+	texture.fill(tex.data.data());
+	texture.generateMipChainImmediate();
+	texture.switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
 
 	vkcv::SamplerHandle sampler = core.createSampler(
 		vkcv::SamplerFilterType::LINEAR,
@@ -126,25 +133,26 @@ int main(int argc, const char** argv) {
 	);
 
 	const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
-			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[0].offset), vertexBuffer.getVulkanHandle()),
-			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[1].offset), vertexBuffer.getVulkanHandle()),
-			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[2].offset), vertexBuffer.getVulkanHandle()) };
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[0].offset), vertexBuffer.getVulkanHandle()),
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[1].offset), vertexBuffer.getVulkanHandle()),
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[2].offset), vertexBuffer.getVulkanHandle()) };
 
 	vkcv::DescriptorWrites setWrites;
-	setWrites.sampledImageWrites    = { vkcv::SampledImageDescriptorWrite(0, texture.getHandle()) };
-	setWrites.samplerWrites         = { vkcv::SamplerDescriptorWrite(1, sampler) };
+	setWrites.sampledImageWrites	= { vkcv::SampledImageDescriptorWrite(0, texture.getHandle()) };
+	setWrites.samplerWrites			= { vkcv::SamplerDescriptorWrite(1, sampler) };
+
 	core.writeDescriptorSet(descriptorSet, setWrites);
 
-	vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight).getHandle();
+	vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight, 1, false).getHandle();
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
 	const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices);
 
 	vkcv::DescriptorSetUsage    descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
-	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage });
+	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage },1);
 
-    vkcv::camera::CameraManager cameraManager(window, windowWidth, windowHeight);
+    vkcv::camera::CameraManager cameraManager(window);
     uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
 	uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
 	
@@ -153,7 +161,7 @@ int main(int argc, const char** argv) {
     auto start = std::chrono::system_clock::now();
     
 	while (window.isWindowOpen()) {
-        vkcv::Window::pollEvents();
+        window.pollEvents();
 		
 		if(window.getHeight() == 0 || window.getWidth() == 0)
 			continue;
@@ -191,7 +199,6 @@ int main(int argc, const char** argv) {
 			renderTargets);
 		core.prepareSwapchainImageForPresent(cmdStream);
 		core.submitCommandStream(cmdStream);
-
 		core.endFrame();
 	}
 	
diff --git a/projects/first_scene/.gitignore b/projects/first_scene/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b0d802667a184267e6ee764264e976dfead8e9dd
--- /dev/null
+++ b/projects/first_scene/.gitignore
@@ -0,0 +1 @@
+first_scene
diff --git a/projects/cmd_sync_test/CMakeLists.txt b/projects/first_scene/CMakeLists.txt
similarity index 52%
rename from projects/cmd_sync_test/CMakeLists.txt
rename to projects/first_scene/CMakeLists.txt
index da1d12949d9e8c918d78ab5cb0484106fae69b6a..8b90739750011a36b4c1d9e0bff7cba986074228 100644
--- a/projects/cmd_sync_test/CMakeLists.txt
+++ b/projects/first_scene/CMakeLists.txt
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.16)
-project(cmd_sync_test)
+project(first_scene)
 
 # setting c++ standard for the project
 set(CMAKE_CXX_STANDARD 17)
@@ -9,20 +9,20 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
 
 # adding source files to the project
-add_executable(cmd_sync_test src/main.cpp)
+add_executable(first_scene src/main.cpp)
 
 # this should fix the execution path to load local files from the project (for MSVC)
 if(MSVC)
-	set_target_properties(cmd_sync_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(cmd_sync_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(first_scene PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(first_scene 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(cmd_sync_test PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(first_scene PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
 endif()
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(cmd_sync_test SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include})
+target_include_directories(first_scene SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(cmd_sync_test vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera)
+target_link_libraries(first_scene vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera)
diff --git a/projects/first_scene/resources/Cutlery/Cutlery_chrome_BaseColor.png b/projects/first_scene/resources/Cutlery/Cutlery_chrome_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..8258525f22097f2382ec5c26ad0df7eb50718642
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Cutlery_chrome_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0ce87f6407ee40ffa60983587aeb52333d59b4b1c01a53e11f4bb227ba1099d9
+size 109
diff --git a/projects/first_scene/resources/Cutlery/Cutlery_chrome_Normal.png b/projects/first_scene/resources/Cutlery/Cutlery_chrome_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..620fe7621cf35409ae8c04099dc8bc4bbc04343b
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Cutlery_chrome_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:68a0064d457a6f7994814b07d943deda778754128935689874334300ede6161d
+size 2332064
diff --git a/projects/first_scene/resources/Cutlery/Cutlery_details_BaseColor.png b/projects/first_scene/resources/Cutlery/Cutlery_details_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..5570e88c569036b9d00155ef6113013aa23f2503
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Cutlery_details_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:42c2715635081eb29c4489ce631798b0e9c881460efc0aa63d0e81641a0dcfe9
+size 108
diff --git a/projects/first_scene/resources/Cutlery/Cutlery_details_Normal.png b/projects/first_scene/resources/Cutlery/Cutlery_details_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..d07681f5bfa25c279321c3074e21e4900c5eb0fa
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Cutlery_details_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15b0133e140899c47ccf35b0f99a7e337e3110ae089f45d27faf9983f3e0a1f7
+size 770758
diff --git a/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Caps_BaseColor.png b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Caps_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..1845e8a7d586a0d23300ad9d04a82f1d5048fcf5
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Caps_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ea7c82c0f9e25afa401470df1fb6903f508fa138d21ad30f57a9153b0395b198
+size 521315
diff --git a/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Caps_Normal.png b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Caps_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c800c0489693b66d6163bdc2156d453b68ca19b
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Caps_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:119efbbc020244ff9b7ff16ac9795a6d4b1808d1b90d81d20d2c874d0dc8a924
+size 1693468
diff --git a/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Glass_Wine_BaseColor.png b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Glass_Wine_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..36f46ebf25158c78bc26d83860e1c08b2c9e96cf
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Glass_Wine_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99896468c7d47dd5391d585eecf149f420eca3bfec31923c21fa86c45fe02d0f
+size 108
diff --git a/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Glass_Wine_Normal.png b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Glass_Wine_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..28c205d4e70867ec58448ca8ba2c2117c611a367
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Paris_LiquorBottle_01_Glass_Wine_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b31618aa5adce4ad476bec2c03718c5ae097250e784344f2d298b8a74c3bfd46
+size 90
diff --git a/projects/first_scene/resources/Cutlery/Plates_Ceramic_BaseColor.png b/projects/first_scene/resources/Cutlery/Plates_Ceramic_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..e0104189a1190c160152101e9e328e81ed89121b
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Plates_Ceramic_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6eff6ccd12d8b39d60ae5ee91edd73d4d7838fcb5d9bc6ff0e671bdf009134e9
+size 109
diff --git a/projects/first_scene/resources/Cutlery/Plates_Ceramic_Normal.png b/projects/first_scene/resources/Cutlery/Plates_Ceramic_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa13483d25595ee6b3a00e7fe6ce48aed7b6aaca
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Plates_Ceramic_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fe92c40ff4032fdaf10eeafd943657a0c6e0bfb3f38770f5654aa943a660f421
+size 59419
diff --git a/projects/first_scene/resources/Cutlery/Plates_Details_BaseColor-Plates_Details_BaseColor.png b/projects/first_scene/resources/Cutlery/Plates_Details_BaseColor-Plates_Details_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..b91d0ac62fbeabbef1ed78f1343f919836cbca40
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Plates_Details_BaseColor-Plates_Details_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ca7d436a68a2a1237aee6e763b2954f01666b21f1dbd46929a322ea277483d2
+size 779227
diff --git a/projects/first_scene/resources/Cutlery/Plates_Details_Normal.png b/projects/first_scene/resources/Cutlery/Plates_Details_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..6efd967984ee2e68b89de9e471b93396c13ca69a
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/Plates_Details_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b01fc6482054c64d7407b283731e57fce0601a8db28b6781c14fae3c6b30b0fe
+size 504362
diff --git a/projects/first_scene/resources/Cutlery/ToffeeJar_Label_BaseColor.png b/projects/first_scene/resources/Cutlery/ToffeeJar_Label_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0e0c4f4134cb99d3765d6f078f71efab8861bf1
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/ToffeeJar_Label_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df138ee68c1d455652d1b9ae3dd03e93fcd2f6a0d8a1f12e3710f39143088674
+size 1593466
diff --git a/projects/first_scene/resources/Cutlery/ToffeeJar_Label_Normal.png b/projects/first_scene/resources/Cutlery/ToffeeJar_Label_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f310653b5211575da3cab2f6deb47ec6826a936
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/ToffeeJar_Label_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6af5da97cbb25d79aea2dde8dd71ecbd495334fe34e99497ba17821be93fd7fd
+size 2696676
diff --git a/projects/first_scene/resources/Cutlery/TransparentGlass_BaseColor.png b/projects/first_scene/resources/Cutlery/TransparentGlass_BaseColor.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e4f0fcb312b8f8bd0df965bfe6b8ac62ec5df4d
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/TransparentGlass_BaseColor.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2796affdfdcf6bc805176d9f85505680b5ee52eeec625e9eaeea4f0ff3854883
+size 108
diff --git a/projects/first_scene/resources/Cutlery/TransparentGlass_Normal.png b/projects/first_scene/resources/Cutlery/TransparentGlass_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..28c205d4e70867ec58448ca8ba2c2117c611a367
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/TransparentGlass_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b31618aa5adce4ad476bec2c03718c5ae097250e784344f2d298b8a74c3bfd46
+size 90
diff --git a/projects/first_scene/resources/Cutlery/cutlerySzene.bin b/projects/first_scene/resources/Cutlery/cutlerySzene.bin
new file mode 100644
index 0000000000000000000000000000000000000000..ab9a0aa47205e8f3064d2f16a950d4733fb4f472
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/cutlerySzene.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f545b986e0a1ac5bff5d49693a52042aa37878425818f72c69c243da20d1f99d
+size 183324
diff --git a/projects/first_scene/resources/Cutlery/cutlerySzene.glb b/projects/first_scene/resources/Cutlery/cutlerySzene.glb
new file mode 100644
index 0000000000000000000000000000000000000000..b0c5f345aaaa3f96d7158a0992ee124aae99a69a
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/cutlerySzene.glb
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb1bad604192ca36222c0ca485ba87b846ecbd11ee8254327e04e3c993b00116
+size 11150396
diff --git a/projects/first_scene/resources/Cutlery/cutlerySzene.gltf b/projects/first_scene/resources/Cutlery/cutlerySzene.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..53e339cda4511f3f1a8670b36469e184aac530e2
--- /dev/null
+++ b/projects/first_scene/resources/Cutlery/cutlerySzene.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c77cd60e2327daca1a01044e45f2c38655f7b781bd07985fc0135328a8a96b57
+size 34312
diff --git a/projects/first_scene/resources/Sponza/Sponza.bin b/projects/first_scene/resources/Sponza/Sponza.bin
new file mode 100644
index 0000000000000000000000000000000000000000..cfedd26ca5a67b6d0a47d44d13a75e14a141717a
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/Sponza.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4b809f7a17687dc99e6f41ca1ea32c06eded8779bf34d16f1f565d750b0ffd68
+size 6347696
diff --git a/projects/first_scene/resources/Sponza/Sponza.gltf b/projects/first_scene/resources/Sponza/Sponza.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..172ea07e21c94465211c860cd805355704cef230
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/Sponza.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cc0ecad5c4694088ff820e663619c370421afc1323ac487406e8e9b4735d787
+size 713962
diff --git a/projects/first_scene/resources/Sponza/background.png b/projects/first_scene/resources/Sponza/background.png
new file mode 100644
index 0000000000000000000000000000000000000000..b64def129da38f4e23d89e21b4af1039008a4327
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/background.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f5b5f900ff8ed83a31750ec8e428b5b91273794ddcbfc4e4b8a6a7e781f8c686
+size 1417666
diff --git a/projects/first_scene/resources/Sponza/chain_texture.png b/projects/first_scene/resources/Sponza/chain_texture.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1e1768cff78e0614ad707eca8602a4c4edab5e5
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/chain_texture.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8362cfd472880daeaea37439326a4651d1338680ae69bb2513fc6b17c8de7d4
+size 490895
diff --git a/projects/first_scene/resources/Sponza/lion.png b/projects/first_scene/resources/Sponza/lion.png
new file mode 100644
index 0000000000000000000000000000000000000000..c49c7f0ed31e762e19284d0d3624fbc47664e56b
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/lion.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f882f746c3a9cd51a9c6eedc1189b97668721d91a3fe49232036e789912c652
+size 2088728
diff --git a/projects/first_scene/resources/Sponza/spnza_bricks_a_diff.png b/projects/first_scene/resources/Sponza/spnza_bricks_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..cde4c7a6511e9a5f03c63ad996437fcdba3ce2df
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/spnza_bricks_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b94219c2f5f943f3f4715c74e7d1038bf0ab3b3b3216a758eaee67f875df0851
+size 1928829
diff --git a/projects/first_scene/resources/Sponza/sponza_arch_diff.png b/projects/first_scene/resources/Sponza/sponza_arch_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..bcd9bda2918d226039f9e2d03902d377b706fab6
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_arch_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c0df2c8a01b2843b1c792b494f7173cdbc4f834840fc2177af3e5d690fceda57
+size 1596151
diff --git a/projects/first_scene/resources/Sponza/sponza_ceiling_a_diff.png b/projects/first_scene/resources/Sponza/sponza_ceiling_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..59de631ffac4414cabf69b2dc794c46fc187d6cb
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_ceiling_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab6c187a81aa68f4eba30119e17fce2e4882a9ec320f70c90482dbe9da82b1c6
+size 1872074
diff --git a/projects/first_scene/resources/Sponza/sponza_column_a_diff.png b/projects/first_scene/resources/Sponza/sponza_column_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..01a82432d3f9939bbefe850bdb900f1ff9a3f6db
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_column_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2c291507e2808bb83e160ab4b020689817df273baad3713a9ad19ac15fac6826
+size 1840992
diff --git a/projects/first_scene/resources/Sponza/sponza_column_b_diff.png b/projects/first_scene/resources/Sponza/sponza_column_b_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..10a660cce2a5a9b8997772c746058ce23e7d45d7
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_column_b_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2820b0267c4289c6cedbb42721792a57ef244ec2d0935941011c2a7d3fe88a9b
+size 2170433
diff --git a/projects/first_scene/resources/Sponza/sponza_column_c_diff.png b/projects/first_scene/resources/Sponza/sponza_column_c_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc46fd979044a938d3adca7601689e71504e48bf
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_column_c_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a0bc993ff59865468ef4530798930c7dfefb07482d71db45bc2a520986b27735
+size 2066950
diff --git a/projects/first_scene/resources/Sponza/sponza_curtain_blue_diff.png b/projects/first_scene/resources/Sponza/sponza_curtain_blue_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..384c8c2c051160d530eb3ac8b05c9c60752a2d2b
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_curtain_blue_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b85c6bb3cd5105f48d3812ec8e7a1068521ce69e917300d79e136e19d45422fb
+size 9510905
diff --git a/projects/first_scene/resources/Sponza/sponza_curtain_diff.png b/projects/first_scene/resources/Sponza/sponza_curtain_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..af842e9f5fe18c1f609875e00899a6770fa4488b
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_curtain_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:563c56bdbbee395a6ef7f0c51c8ac9223c162e517b4cdba0d4654e8de27c98d8
+size 9189263
diff --git a/projects/first_scene/resources/Sponza/sponza_curtain_green_diff.png b/projects/first_scene/resources/Sponza/sponza_curtain_green_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..6c9b6391a199407637fa71033d79fb58b8b4f0d7
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_curtain_green_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:238fe1c7f481388d1c1d578c2da8d411b99e8f0030ab62060a306db333124476
+size 8785458
diff --git a/projects/first_scene/resources/Sponza/sponza_details_diff.png b/projects/first_scene/resources/Sponza/sponza_details_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..12656686362c3e0a297e060491f33bd7351551f9
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_details_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb1223b3bb82f8757e7df25a6891f1239cdd7ec59990340e952fb2d6b7ea570c
+size 1522643
diff --git a/projects/first_scene/resources/Sponza/sponza_fabric_blue_diff.png b/projects/first_scene/resources/Sponza/sponza_fabric_blue_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..879d16ef84722a4fc13e83a771778de326e4bc54
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_fabric_blue_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:467d290bf5d4b2a017da140ba9e244ed8a8a9be5418a9ac9bcb4ad572ae2d7ab
+size 2229440
diff --git a/projects/first_scene/resources/Sponza/sponza_fabric_diff.png b/projects/first_scene/resources/Sponza/sponza_fabric_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..3311287a219d2148620b87fe428fea071688d051
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_fabric_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1594f59cc2848db26add47361f4e665e3d8afa147760ed915d839fea42b20287
+size 2267382
diff --git a/projects/first_scene/resources/Sponza/sponza_fabric_green_diff.png b/projects/first_scene/resources/Sponza/sponza_fabric_green_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..de110f369004388dae4cd5067c63428db3a07834
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_fabric_green_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:902b87faab221173bf370cea7c74cb9060b4d870ac6316b190dafded1cb12993
+size 2258220
diff --git a/projects/first_scene/resources/Sponza/sponza_flagpole_diff.png b/projects/first_scene/resources/Sponza/sponza_flagpole_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f6e0812a0df80346318baa3cb50a6888afc58f8
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_flagpole_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bfffb62e770959c725d0f3db6dc7dbdd46a380ec55ef884dab94d44ca017b438
+size 1425673
diff --git a/projects/first_scene/resources/Sponza/sponza_floor_a_diff.png b/projects/first_scene/resources/Sponza/sponza_floor_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..788ed764f79ba724f04a2d603076a5b85013e188
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_floor_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a16f9230fa91f9f31dfca6216ce205f1ef132d44f3b012fbf6efc0fba69770ab
+size 1996838
diff --git a/projects/first_scene/resources/Sponza/sponza_roof_diff.png b/projects/first_scene/resources/Sponza/sponza_roof_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5b84261fdd1cc776a94b3ce398c7806b895f9a3
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_roof_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7fc412138c20da19f8173e53545e771f4652558dff624d4dc67143e40efe562b
+size 2320533
diff --git a/projects/first_scene/resources/Sponza/sponza_thorn_diff.png b/projects/first_scene/resources/Sponza/sponza_thorn_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a9142674a7d4a6f94a48c5152cf0300743b597a
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/sponza_thorn_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a73a17c883cd0d0d67cfda2dc4118400a916366c05b9a5ac465f0c8b30fd9c8e
+size 635001
diff --git a/projects/first_scene/resources/Sponza/vase_dif.png b/projects/first_scene/resources/Sponza/vase_dif.png
new file mode 100644
index 0000000000000000000000000000000000000000..61236a81cb324af8797b05099cd264cefe189e56
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/vase_dif.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53d06f52bf9e59df4cf00237707cca76c4f692bda61a62b06a30d321311d6dd9
+size 1842101
diff --git a/projects/first_scene/resources/Sponza/vase_hanging.png b/projects/first_scene/resources/Sponza/vase_hanging.png
new file mode 100644
index 0000000000000000000000000000000000000000..36a3cee71d8213225090c74f8c0dce33b9d44378
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/vase_hanging.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9d10b4f27a3c9a78d5bac882fdd4b6a6987c262f48fa490670fe5e235951e31
+size 1432804
diff --git a/projects/first_scene/resources/Sponza/vase_plant.png b/projects/first_scene/resources/Sponza/vase_plant.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ad95e702e229f1ebd803e5203a266d15f2c07b9
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/vase_plant.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2087371ff02212fb7014b6daefa191cf5676d2227193fff261a5d02f554cb8e
+size 998089
diff --git a/projects/first_scene/resources/Sponza/vase_round.png b/projects/first_scene/resources/Sponza/vase_round.png
new file mode 100644
index 0000000000000000000000000000000000000000..c17953abc000c44b8991e23c136c2b67348f3d1b
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/vase_round.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa23d48d492d5d4ada2ddb27d1ef22952b214e6eb3b301c65f9d88442723d20a
+size 1871399
diff --git a/projects/first_scene/resources/Szene/Szene.bin b/projects/first_scene/resources/Szene/Szene.bin
new file mode 100644
index 0000000000000000000000000000000000000000..c87d27637516b0bbf864251dd162773f5cc53e06
--- /dev/null
+++ b/projects/first_scene/resources/Szene/Szene.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee4742718f720d589a2a03f5d879f8c50ba9057718d191a43b046eaa9071080d
+size 70328
diff --git a/projects/first_scene/resources/Szene/Szene.gltf b/projects/first_scene/resources/Szene/Szene.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..e5a32b29af5d0a2ac5f497e60b4b92c1873e1df9
--- /dev/null
+++ b/projects/first_scene/resources/Szene/Szene.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:75ba834118792ebbacf528a1690c7d04df4b4c8122b9f99a9aa9a9d075d2c86a
+size 7421
diff --git a/projects/first_scene/resources/Szene/boards2_vcyc.jpg b/projects/first_scene/resources/Szene/boards2_vcyc.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..2636039e272289c0fba3fa2d88a060b857501248
--- /dev/null
+++ b/projects/first_scene/resources/Szene/boards2_vcyc.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cca33a6e58ddd1b37a6e6853a9aa0e7b15ca678937119194752393dd2a0a0564
+size 1192476
diff --git a/projects/first_scene/resources/Szene/boards2_vcyc_jpg.jpg b/projects/first_scene/resources/Szene/boards2_vcyc_jpg.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..2636039e272289c0fba3fa2d88a060b857501248
--- /dev/null
+++ b/projects/first_scene/resources/Szene/boards2_vcyc_jpg.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cca33a6e58ddd1b37a6e6853a9aa0e7b15ca678937119194752393dd2a0a0564
+size 1192476
diff --git a/projects/first_scene/resources/shaders/compile.bat b/projects/first_scene/resources/shaders/compile.bat
new file mode 100644
index 0000000000000000000000000000000000000000..b4521235c40fe5fb163bab874560c2f219b7517f
--- /dev/null
+++ b/projects/first_scene/resources/shaders/compile.bat
@@ -0,0 +1,3 @@
+%VULKAN_SDK%\Bin32\glslc.exe shader.vert -o vert.spv
+%VULKAN_SDK%\Bin32\glslc.exe shader.frag -o frag.spv
+pause
\ No newline at end of file
diff --git a/projects/first_scene/resources/shaders/frag.spv b/projects/first_scene/resources/shaders/frag.spv
new file mode 100644
index 0000000000000000000000000000000000000000..087e4e22fb2fcec27d99b3ff2aa1a705fe755796
Binary files /dev/null and b/projects/first_scene/resources/shaders/frag.spv differ
diff --git a/projects/first_scene/resources/shaders/shader.frag b/projects/first_scene/resources/shaders/shader.frag
new file mode 100644
index 0000000000000000000000000000000000000000..b5494bea7d6497e2e3dcd8559606864a71adb74e
--- /dev/null
+++ b/projects/first_scene/resources/shaders/shader.frag
@@ -0,0 +1,15 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 passNormal;
+layout(location = 1) in vec2 passUV;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=0) uniform texture2D  meshTexture;
+layout(set=0, binding=1) uniform sampler    textureSampler;
+
+void main()	{
+	outColor = texture(sampler2D(meshTexture, textureSampler), passUV).rgb;
+    //outColor = passNormal * 0.5 + 0.5;
+}
\ No newline at end of file
diff --git a/projects/first_scene/resources/shaders/shader.vert b/projects/first_scene/resources/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..76855152253b48b7400f016d063ed4f0e507435e
--- /dev/null
+++ b/projects/first_scene/resources/shaders/shader.vert
@@ -0,0 +1,19 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+layout(location = 2) in vec2 inUV;
+
+layout(location = 0) out vec3 passNormal;
+layout(location = 1) out vec2 passUV;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+	passNormal  = inNormal;
+    passUV      = inUV;
+}
\ No newline at end of file
diff --git a/projects/first_scene/resources/shaders/vert.spv b/projects/first_scene/resources/shaders/vert.spv
new file mode 100644
index 0000000000000000000000000000000000000000..374c023e14b351eb43cbcda5951cbb8b3d6f96a1
Binary files /dev/null and b/projects/first_scene/resources/shaders/vert.spv differ
diff --git a/projects/first_scene/src/main.cpp b/projects/first_scene/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..521818732f7a60eabe9f0c2c080c6d343a71b1d8
--- /dev/null
+++ b/projects/first_scene/src/main.cpp
@@ -0,0 +1,264 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/Logger.hpp>
+
+glm::mat4 arrayTo4x4Matrix(std::array<float,16> array){
+    glm::mat4 matrix;
+    for (int i = 0; i < 4; i++){
+        for (int j = 0; j < 4; j++){
+            matrix[i][j] = array[j * 4 + i];
+        }
+    }
+    return matrix;
+}
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "First Scene";
+
+	uint32_t windowWidth = 800;
+	uint32_t windowHeight = 600;
+
+	vkcv::Window window = vkcv::Window::create(
+		applicationName,
+		windowWidth,
+		windowHeight,
+		true
+	);
+
+	vkcv::camera::CameraManager cameraManager(window);
+	uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -3));
+	cameraManager.getCamera(camIndex0).setNearFar(0.1f, 30.0f);
+	
+	cameraManager.getCamera(camIndex1).setNearFar(0.1f, 30.0f);
+
+	vkcv::Core core = vkcv::Core::create(
+		window,
+		applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eGraphics ,vk::QueueFlagBits::eCompute , vk::QueueFlagBits::eTransfer },
+		{},
+		{ "VK_KHR_swapchain" }
+	);
+
+	vkcv::asset::Scene scene;
+
+	const char* path = argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf";
+	int result = vkcv::asset::loadScene(path, scene);
+
+	if (result == 1) {
+		std::cout << "Mesh loading successful!" << std::endl;
+	}
+	else {
+		std::cout << "Mesh loading failed: " << result << std::endl;
+		return 1;
+	}
+
+	assert(!scene.vertexGroups.empty());
+	std::vector<std::vector<uint8_t>> vBuffers;
+	std::vector<std::vector<uint8_t>> iBuffers;
+
+	std::vector<vkcv::VertexBufferBinding> vBufferBindings;
+	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
+	std::vector<vkcv::asset::VertexAttribute> vAttributes;
+
+	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+
+		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
+		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);
+
+		auto& attributes = scene.vertexGroups[i].vertexBuffer.attributes;
+
+		std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
+			return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
+			});
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> vertexBuffers;
+	for (const vkcv::asset::VertexGroup& group : scene.vertexGroups) {
+		vertexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::VERTEX,
+			group.vertexBuffer.data.size()));
+		vertexBuffers.back().fill(group.vertexBuffer.data);
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> indexBuffers;
+	for (const auto& dataBuffer : iBuffers) {
+		indexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::INDEX,
+			dataBuffer.size()));
+		indexBuffers.back().fill(dataBuffer);
+	}
+
+	int vertexBufferIndex = 0;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
+			vAttributes.push_back(attribute);
+			vBufferBindings.push_back(vkcv::VertexBufferBinding(attribute.offset, vertexBuffers[vertexBufferIndex].getVulkanHandle()));
+		}
+		vertexBufferBindings.push_back(vBufferBindings);
+		vBufferBindings.clear();
+		vertexBufferIndex++;
+	}
+
+	const vkcv::AttachmentDescription present_color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		core.getSwapchain().getFormat()
+	);
+
+	const vkcv::AttachmentDescription depth_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		vk::Format::eD32Sfloat
+	);
+
+	vkcv::PassConfig scenePassDefinition({ present_color_attachment, depth_attachment });
+	vkcv::PassHandle scenePass = core.createPass(scenePassDefinition);
+
+	if (!scenePass) {
+		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::ShaderProgram sceneShaderProgram{};
+	sceneShaderProgram.addShader(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/vert.spv"));
+	sceneShaderProgram.addShader(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/frag.spv"));
+
+	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 vkcv::VertexLayout sceneLayout(bindings);
+
+	uint32_t setID = 0;
+
+	std::vector<vkcv::DescriptorBinding> descriptorBindings = { sceneShaderProgram.getReflectedDescriptors()[setID] };
+
+	vkcv::SamplerHandle sampler = core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::REPEAT
+	);
+
+	std::vector<vkcv::Image> sceneImages;
+	std::vector<vkcv::DescriptorSetHandle> descriptorSets;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		descriptorSets.push_back(core.createDescriptorSet(descriptorBindings));
+
+		const auto& material = scene.materials[vertexGroup.materialIndex];
+
+		int baseColorIndex = material.baseColor;
+		if (baseColorIndex < 0) {
+			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks base color");
+			baseColorIndex = 0;
+		}
+
+		vkcv::asset::Texture& sceneTexture = scene.textures[baseColorIndex];
+
+		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, sceneTexture.w, sceneTexture.h));
+		sceneImages.back().fill(sceneTexture.data.data());
+
+		vkcv::DescriptorWrites setWrites;
+		setWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(0, sceneImages.back().getHandle()) };
+		setWrites.samplerWrites = { vkcv::SamplerDescriptorWrite(1, sampler) };
+		core.writeDescriptorSet(descriptorSets.back(), setWrites);
+	}
+
+	const vkcv::PipelineConfig scenePipelineDefsinition{
+		sceneShaderProgram,
+		UINT32_MAX,
+		UINT32_MAX,
+		scenePass,
+		{sceneLayout},
+		{ core.getDescriptorSet(descriptorSets[0]).layout },
+		true };
+	vkcv::PipelineHandle scenePipeline = core.createGraphicsPipeline(scenePipelineDefsinition);
+	
+	if (!scenePipeline) {
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight).getHandle();
+
+	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+    std::vector<vkcv::DrawcallInfo> drawcalls;
+	for(int i = 0; i < scene.vertexGroups.size(); i++){
+        vkcv::Mesh renderMesh(vertexBufferBindings[i], indexBuffers[i].getVulkanHandle(), scene.vertexGroups[i].numIndices);
+
+        vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSets[i]).vulkanHandle);
+
+	    drawcalls.push_back(vkcv::DrawcallInfo(renderMesh, {descriptorUsage},1));
+	}
+
+	std::vector<glm::mat4> modelMatrices;
+	modelMatrices.resize(scene.vertexGroups.size(), glm::mat4(1.f));
+	for (const auto &mesh : scene.meshes) {
+		const glm::mat4 m = arrayTo4x4Matrix(mesh.modelMatrix);
+		for (const auto &vertexGroupIndex : mesh.vertexGroups) {
+			modelMatrices[vertexGroupIndex] = m;
+		}
+	}
+	std::vector<glm::mat4> mvp;
+
+	auto start = std::chrono::system_clock::now();
+	while (window.isWindowOpen()) {
+        vkcv::Window::pollEvents();
+		
+		if(window.getHeight() == 0 || window.getWidth() == 0)
+			continue;
+		
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+			continue;
+		}
+		
+		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
+			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
+			
+			windowWidth = swapchainWidth;
+			windowHeight = swapchainHeight;
+		}
+  
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
+		
+		start = end;
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+		glm::mat4 vp = cameraManager.getActiveCamera().getMVP();
+
+		mvp.clear();
+        for (const auto& m : modelMatrices) {
+            mvp.push_back(vp * m);
+        }
+
+		vkcv::PushConstantData pushConstantData((void*)mvp.data(), sizeof(glm::mat4));
+
+		const std::vector<vkcv::ImageHandle> renderTargets = { swapchainInput, depthBuffer };
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			scenePass,
+			scenePipeline,
+			pushConstantData,
+			drawcalls,
+			renderTargets);
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		core.endFrame();
+	}
+	
+	return 0;
+}
diff --git a/projects/first_triangle/CMakeLists.txt b/projects/first_triangle/CMakeLists.txt
index e7c8373e085df6497060b8d1d8164cf740dfb01f..ba8c83c06fc804082e6a0a14c3c0414899ef3057 100644
--- a/projects/first_triangle/CMakeLists.txt
+++ b/projects/first_triangle/CMakeLists.txt
@@ -22,7 +22,7 @@ if(MSVC)
 endif()
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(first_triangle SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include})
+target_include_directories(first_triangle SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(first_triangle vkcv vkcv_testing vkcv_camera)
+target_link_libraries(first_triangle vkcv vkcv_testing vkcv_camera vkcv_shader_compiler vkcv_gui)
diff --git a/projects/first_triangle/shaders/shader.frag b/projects/first_triangle/shaders/shader.frag
index d26446a73020111695aa2c86166205796dfa5e44..080678beb011afe4b03aed3bf7ae7148b77932dc 100644
--- a/projects/first_triangle/shaders/shader.frag
+++ b/projects/first_triangle/shaders/shader.frag
@@ -4,6 +4,6 @@
 layout(location = 0) in vec3 fragColor;
 layout(location = 0) out vec4 outColor;
 
-void main()	{
+void main() {
 	outColor = vec4(fragColor, 1.0);
 }
\ No newline at end of file
diff --git a/projects/first_triangle/src/main.cpp b/projects/first_triangle/src/main.cpp
index 62d71d9ce3cab388361ac1163b67281ecc465af5..5bdd55a263f4d81d8f424c056d7d6c0b54ccb1ca 100644
--- a/projects/first_triangle/src/main.cpp
+++ b/projects/first_triangle/src/main.cpp
@@ -4,6 +4,9 @@
 #include <vkcv/camera/CameraManager.hpp>
 #include <chrono>
 
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/gui/GUI.hpp>
+
 int main(int argc, const char** argv) {
 	const char* applicationName = "First Triangle";
 
@@ -24,6 +27,8 @@ int main(int argc, const char** argv) {
 		{},
 		{ "VK_KHR_swapchain" }
 	);
+	
+	vkcv::gui::GUI gui (core, window);
 
 	const auto& context = core.getContext();
 	const vk::Instance& instance = context.getInstance();
@@ -77,7 +82,7 @@ int main(int argc, const char** argv) {
 	const vkcv::AttachmentDescription present_color_attachment(
 		vkcv::AttachmentOperation::STORE,
 		vkcv::AttachmentOperation::CLEAR,
-		core.getSwapchainImageFormat());
+		core.getSwapchain().getFormat());
 
 	vkcv::PassConfig trianglePassDefinition({ present_color_attachment });
 	vkcv::PassHandle trianglePass = core.createPass(trianglePassDefinition);
@@ -88,19 +93,28 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 
-	// Graphics Pipeline
 	vkcv::ShaderProgram triangleShaderProgram{};
-	triangleShaderProgram.addShader(vkcv::ShaderStage::VERTEX, std::filesystem::path("shaders/vert.spv"));
-	triangleShaderProgram.addShader(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("shaders/frag.spv"));
+	vkcv::shader::GLSLCompiler compiler;
+	
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("shaders/shader.vert"),
+					 [&triangleShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		 triangleShaderProgram.addShader(shaderStage, path);
+	});
+	
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("shaders/shader.frag"),
+					 [&triangleShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		triangleShaderProgram.addShader(shaderStage, path);
+	});
 
-	const vkcv::PipelineConfig trianglePipelineDefinition(
+	const vkcv::PipelineConfig trianglePipelineDefinition {
 		triangleShaderProgram,
 		(uint32_t)windowWidth,
 		(uint32_t)windowHeight,
 		trianglePass,
 		{},
 		{},
-		false);
+		false
+	};
 
 	vkcv::PipelineHandle trianglePipeline = core.createGraphicsPipeline(trianglePipelineDefinition);
 
@@ -153,20 +167,22 @@ int main(int argc, const char** argv) {
 	vkcv::ImageHandle swapchainImageHandle = vkcv::ImageHandle::createSwapchainImageHandle();
 
 	const vkcv::Mesh renderMesh({}, triangleIndexBuffer.getVulkanHandle(), 3);
-	vkcv::DrawcallInfo drawcall(renderMesh, {});
+	vkcv::DrawcallInfo drawcall(renderMesh, {},1);
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+    vkcv::camera::CameraManager cameraManager(window);
+    uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+    uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
 	
-    vkcv::camera::CameraManager cameraManager(window, windowWidth, windowHeight);
-    uint32_t camIndex = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
-    uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
-	
-	cameraManager.getCamera(camIndex).setPosition(glm::vec3(0, 0, -2));
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -2));
+    cameraManager.getCamera(camIndex1).setPosition(glm::vec3(0.0f, 0.0f, 0.0f));
+    cameraManager.getCamera(camIndex1).setCenter(glm::vec3(0.0f, 0.0f, -1.0f));
 
 	while (window.isWindowOpen())
 	{
         window.pollEvents();
-		
+
 		uint32_t swapchainWidth, swapchainHeight; // No resizing = No problem
 		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
 			continue;
@@ -202,6 +218,14 @@ int main(int argc, const char** argv) {
 
 		core.prepareSwapchainImageForPresent(cmdStream);
 		core.submitCommandStream(cmdStream);
+		
+		gui.beginGUI();
+		
+		ImGui::Begin("Hello world");
+		ImGui::Text("This is a test!");
+		ImGui::End();
+		
+		gui.endGUI();
 	    
 	    core.endFrame();
 	}
diff --git a/projects/particle_simulation/.gitignore b/projects/particle_simulation/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4964f89e973f38358aa57f564f56d3d4b0c328a9
--- /dev/null
+++ b/projects/particle_simulation/.gitignore
@@ -0,0 +1 @@
+particle_simulation
\ No newline at end of file
diff --git a/projects/particle_simulation/CMakeLists.txt b/projects/particle_simulation/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2a665202c521ac10ae94905cb7580205e897eef9
--- /dev/null
+++ b/projects/particle_simulation/CMakeLists.txt
@@ -0,0 +1,35 @@
+cmake_minimum_required(VERSION 3.16)
+project(particle_simulation)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(particle_simulation 
+		src/main.cpp
+		src/ParticleSystem.hpp 
+		src/ParticleSystem.cpp
+		src/Particle.hpp 
+		src/Particle.cpp
+		src/BloomAndFlares.hpp
+		src/BloomAndFlares.cpp)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(particle_simulation PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(particle_simulation PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+	# in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(particle_simulation PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(particle_simulation SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(particle_simulation vkcv vkcv_testing vkcv_camera vkcv_shader_compiler)
diff --git a/projects/particle_simulation/shaders/bloom/composite.comp b/projects/particle_simulation/shaders/bloom/composite.comp
new file mode 100644
index 0000000000000000000000000000000000000000..87b5ddb975106232d1cd3b6e5b8dc7e623dd0b59
--- /dev/null
+++ b/projects/particle_simulation/shaders/bloom/composite.comp
@@ -0,0 +1,38 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          blurImage;
+layout(set=0, binding=1) uniform texture2D                          lensImage;
+layout(set=0, binding=2) uniform sampler                            linearSampler;
+layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    vec4 composite_color = vec4(0.0f);
+
+    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
+    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
+    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
+
+    // composite blur and lens features
+    float bloom_weight = 0.01f;
+    float lens_weight  = 0.f;
+    float main_weight = 1 - (bloom_weight + lens_weight);
+
+    composite_color.rgb = blur_color * bloom_weight +
+                          lens_color * lens_weight  +
+                          main_color * main_weight;
+
+    imageStore(colorBuffer, pixel_coord, composite_color);
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/downsample.comp b/projects/particle_simulation/shaders/bloom/downsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2ab00c7c92798769153634f3479c5b7f3fb61d94
--- /dev/null
+++ b/projects/particle_simulation/shaders/bloom/downsample.comp
@@ -0,0 +1,76 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inBlurImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+    vec2  UV_offset     = UV + 0.5f * pixel_size;
+
+    vec2 color_fetches[13] = {
+        // center neighbourhood (RED)
+        vec2(-1,  1), // LT
+        vec2(-1, -1), // LB
+        vec2( 1, -1), // RB
+        vec2( 1,  1), // RT
+
+        vec2(-2, 2), // LT
+        vec2( 0, 2), // CT
+        vec2( 2, 2), // RT
+
+        vec2(0 ,-2), // LC
+        vec2(0 , 0), // CC
+        vec2(2,  0), // CR
+
+        vec2(-2, -2), // LB
+        vec2(0 , -2), // CB
+        vec2(2 , -2)  // RB
+    };
+
+    float color_weights[13] = {
+        // 0.5f
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f,
+
+        // 0.25f
+        1.f/16.f,
+        1.f/8.f,
+        1.f/16.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f
+    };
+
+    vec3 sampled_color = vec3(0.0f);
+
+    for(uint i = 0; i < 13; i++)
+    {
+        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
+        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
+        color *= color_weights[i];
+        sampled_color += color;
+    }
+
+    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/lensFlares.comp b/projects/particle_simulation/shaders/bloom/lensFlares.comp
new file mode 100644
index 0000000000000000000000000000000000000000..ce27d8850b709f61332d467914ddc944dc63109f
--- /dev/null
+++ b/projects/particle_simulation/shaders/bloom/lensFlares.comp
@@ -0,0 +1,109 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          blurBuffer;
+layout(set=0, binding=1) uniform sampler                            linearSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+vec3 sampleColorChromaticAberration(vec2 _uv)
+{
+    vec2 toCenter = (vec2(0.5) - _uv);
+
+    vec3    colorScales     = vec3(-1, 0, 1);
+    float   aberrationScale = 0.1;
+    vec3 scaleFactors = colorScales * aberrationScale;
+
+    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
+    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
+    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
+    return vec3(r, g, b);
+}
+
+// _uv assumed to be flipped UV coordinates!
+vec3 ghost_vectors(vec2 _uv)
+{
+    vec2 ghost_vec = (vec2(0.5f) - _uv);
+
+    const uint c_ghost_count = 64;
+    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
+
+    ghost_vec *= c_ghost_spacing;
+
+    vec3 ret_color = vec3(0.0f);
+
+    for (uint i = 0; i < c_ghost_count; ++i)
+    {
+        // sample scene color
+        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
+        vec3 s = sampleColorChromaticAberration(s_uv);
+
+        // tint/weight
+        float d = distance(s_uv, vec2(0.5));
+        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
+        s *= weight;
+
+        ret_color += s;
+    }
+
+    ret_color /= c_ghost_count;
+    return ret_color;
+}
+
+vec3 halo(vec2 _uv)
+{
+    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
+    const float c_radius = 0.6f;
+    const float c_halo_thickness = 0.1f;
+
+    vec2 halo_vec = vec2(0.5) - _uv;
+    //halo_vec.x /= c_aspect_ratio;
+    halo_vec = normalize(halo_vec);
+    //halo_vec.x *= c_aspect_ratio;
+
+
+    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
+    vec2 w_uv = _uv;
+    float d = distance(w_uv, vec2(0.5)); // distance to center
+
+    float distance_to_halo = abs(d - c_radius);
+
+    float halo_weight = 0.0f;
+    if(abs(d - c_radius) <= c_halo_thickness)
+    {
+        float distance_to_border = c_halo_thickness - distance_to_halo;
+        halo_weight = distance_to_border / c_halo_thickness;
+
+        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
+        halo_weight = pow(halo_weight, 2.0f);
+
+
+        //halo_weight = 1.0f;
+    }
+
+    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
+}
+
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    vec2 flipped_UV = vec2(1.0f) - UV;
+
+    vec3 color = vec3(0.0f);
+
+    color += ghost_vectors(flipped_UV);
+    color += halo(UV);
+    color  *= 0.5f;
+
+    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/upsample.comp b/projects/particle_simulation/shaders/bloom/upsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..0ddeedb5b5af9e476dc19012fed6430544006c0e
--- /dev/null
+++ b/projects/particle_simulation/shaders/bloom/upsample.comp
@@ -0,0 +1,45 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
+        return;
+    }
+
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
+    const float gauss_weight = 16.f;
+
+    vec3 sampled_color = vec3(0.f);
+
+    for(int i = -1; i <= 1; i++)
+    {
+        for(int j = -1; j <= 1; j++)
+        {
+            vec2 sample_location = UV + vec2(j, i) * pixel_size;
+            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
+            color *= gauss_kernel[j+1];
+            color *= gauss_kernel[i+1];
+            color /= gauss_weight;
+
+            sampled_color += color;
+        }
+    }
+
+    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
+    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
+    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
+
+    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/particleShading.inc b/projects/particle_simulation/shaders/particleShading.inc
new file mode 100644
index 0000000000000000000000000000000000000000..b2d1832b9ccd6ba05a585b59bdfdedd4729e80f8
--- /dev/null
+++ b/projects/particle_simulation/shaders/particleShading.inc
@@ -0,0 +1,6 @@
+float circleFactor(vec2 triangleCoordinates){
+    // percentage of distance from center to circle edge
+    float p = clamp((0.4 - length(triangleCoordinates)) / 0.4, 0, 1);
+    // remapping for nice falloff
+    return sqrt(p);
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/shader.vert b/projects/particle_simulation/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..0a889b35dbb750dc932de57b611f22acaa1ac3f2
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader.vert
@@ -0,0 +1,45 @@
+#version 460 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 particle;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float padding_2;
+    vec3 reset_velocity;
+    float padding_3;
+};
+
+layout(std430, binding = 2) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    mat4 view;
+    mat4 projection;
+};
+
+layout(location = 0) out vec2 passTriangleCoordinates;
+layout(location = 1) out vec3 passVelocity;
+layout(location = 2) out float passlifeTime;
+
+void main()
+{
+    int id = gl_InstanceIndex;
+    passVelocity = inParticle[id].velocity;
+    passlifeTime = inParticle[id].lifeTime;
+    // particle position in view space
+    vec4 positionView = view * vec4(inParticle[id].position, 1);
+    // by adding the triangle position in view space the mesh is always camera facing
+    positionView.xyz += particle;
+    // multiply with projection matrix for final position
+	gl_Position = projection * positionView;
+    
+    // 0.01 corresponds to vertex position size in main
+    float normalizationDivider  = 0.012;
+    passTriangleCoordinates     = particle.xy / normalizationDivider;
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/shader_gravity.comp b/projects/particle_simulation/shaders/shader_gravity.comp
new file mode 100644
index 0000000000000000000000000000000000000000..77954958c694a3c6c620818dd3b5d999e51b4a42
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_gravity.comp
@@ -0,0 +1,80 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float mass;
+    vec3 reset_velocity;
+    float _padding;
+};
+
+layout(std430, binding = 0) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float deltaTime;
+    float rand;
+};
+
+const int n = 4;
+vec4 gravityPoint[n] = vec4[n](
+    vec4(-0.8, -0.5,  0.0, 3),
+    vec4(-0.4,  0.5,  0.8, 2),
+    vec4( 0.8,  0.8, -0.3, 4),
+    vec4( 0.5, -0.7, -0.5, 1)
+);
+
+const float G = 6.6743015e-11;
+const float sim_d_factor = 10e11;
+const float sim_g_factor = 10e30;
+const float sim_t_factor = 5;
+const float c = 299792458;
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+    inParticle[id].lifeTime -= deltaTime;
+    vec3 pos = inParticle[id].position;
+    vec3 vel = inParticle[id].velocity;
+    float mass = inParticle[id].mass;
+
+    if(inParticle[id].lifeTime < 0.f)
+    {
+        inParticle[id].lifeTime = 5.f * rand;
+        inParticle[id].mass *= rand;
+
+        pos = vec3(0);
+        vel *= rand;
+    }
+
+    for(int i = 0; i < n; i++)
+    {
+        vec3 d = (gravityPoint[i].xyz - pos) * sim_d_factor;
+        float r = length(d);
+        float g = G * (gravityPoint[i].w * sim_g_factor) / (r * r);
+
+        if (r > 0) {
+            vec3 dvel = (deltaTime * sim_t_factor) * g * (d / r);
+
+            vel = (vel + dvel) / (1.0 + dot(vel, dvel) / (c*c));
+        }
+    }
+
+    pos += vel * (deltaTime * sim_t_factor);
+
+    vec3 a_pos = abs(pos);
+
+    if ((a_pos.x > 2.0) || (a_pos.y > 2.0) || (a_pos.z > 2.0))
+    {
+        inParticle[id].lifeTime *= 0.9;
+    }
+
+    inParticle[id].position = pos;
+    inParticle[id].velocity = vel;
+}
diff --git a/projects/particle_simulation/shaders/shader_space.comp b/projects/particle_simulation/shaders/shader_space.comp
new file mode 100644
index 0000000000000000000000000000000000000000..6e25fff8aec8ceab7c1ffdd9be65d9b8fa8f0974
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_space.comp
@@ -0,0 +1,73 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float padding_2;
+    vec3 reset_velocity;
+    float padding_3;
+};
+
+layout(std430, binding = 0) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float deltaTime;
+    float rand;
+};
+
+vec3 attraction(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    const float damp = 0.5;
+    float dDampedDot = dot(delta, delta) + damp;
+    float invDist = 1.0f / sqrt(dDampedDot);
+    float invDistCubed = invDist*invDist*invDist;
+    return delta * invDistCubed * 0.0035;
+}
+
+vec3 repulsion(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    float targetDistance = sqrt(dot(delta, delta));
+    return delta * (1.0 / (targetDistance * targetDistance * targetDistance)) * -0.000035;
+}
+
+
+const int n = 4;
+vec3 gravity = vec3(0,-9.8,0);
+vec3 gravityPoint[n] = vec3[n](vec3(-0.3, .5, -0.6),vec3(-0.2, 0.6, -0.3),vec3(.4, -0.4, 0.6),vec3(-.4, -0.4, -0.6));
+//vec3 gravityPoint[n] = vec3[n](vec3(-0.5, 0.5, 0));
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+    inParticle[id].lifeTime -= deltaTime;
+    vec3 pos = inParticle[id].position;
+    vec3 vel = inParticle[id].velocity;
+    if(inParticle[id].lifeTime < 0.f)
+    {
+        inParticle[id].lifeTime = 5.f;
+        pos = vec3(0);
+    }
+    //    inParticle[id].position += deltaTime * -normalize(max(2 - distance(inParticle[id].position,respawnPos),0.0) * respawnPos - inParticle[id].position);
+
+    for(int i = 0; i < n; i++)
+    {
+        vel += deltaTime * deltaTime * normalize(max(2 - distance(pos,gravityPoint[i]),0.1) * gravityPoint[i] - pos);
+    }
+
+    if((pos.x <= -2.0) || (pos.x > 2.0) || (pos.y <= -2.0) || (pos.y > 2.0)|| (pos.z <= -2.0) || (pos.z > 2.0)){
+        vel = (-vel * 0.1);
+    }
+
+    pos += normalize(vel) * deltaTime;
+    inParticle[id].position = pos;
+    float rand1 = rand;
+    inParticle[id].velocity = vel;
+}
diff --git a/projects/particle_simulation/shaders/shader_space.frag b/projects/particle_simulation/shaders/shader_space.frag
new file mode 100644
index 0000000000000000000000000000000000000000..7f6d22065caa3c4b3ab2b1f697c9545a66d7bd54
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_space.frag
@@ -0,0 +1,46 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particleShading.inc"
+
+layout(location = 0) in vec2 passTriangleCoordinates;
+layout(location = 1) in vec3 passVelocity;
+layout(location = 2) in float passlifeTime;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=0) uniform uColor {
+	vec4 color;
+} Color;
+
+layout(set=0,binding=1) uniform uPosition{
+	vec2 position;
+} Position;
+
+
+void main()
+{
+	vec2 mouse = vec2(Position.position.x, Position.position.y);
+    
+    vec3 c0 = vec3(1, 1, 0.05);
+    vec3 c1 = vec3(1, passlifeTime * 0.5, 0.05);
+    vec3 c2 = vec3(passlifeTime * 0.5,passlifeTime * 0.5,0.05);
+    vec3 c3 = vec3(1, 0.05, 0.05);
+    
+    if(passlifeTime  < 1){
+        outColor = mix(c0, c1, passlifeTime );
+    }
+    else if(passlifeTime  < 2){
+        outColor = mix(c1, c2, passlifeTime  - 1);
+    }
+    else{
+        outColor = mix(c2, c3, clamp((passlifeTime  - 2) * 0.5, 0, 1));
+    }
+   
+   // make the triangle look like a circle
+   outColor *= circleFactor(passTriangleCoordinates);
+   
+   // fade out particle shortly before it dies
+   outColor *= clamp(passlifeTime * 2, 0, 1);
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/shader_water.comp b/projects/particle_simulation/shaders/shader_water.comp
new file mode 100644
index 0000000000000000000000000000000000000000..d1a0e761038b5fb367a33454c746871f1a6a4553
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_water.comp
@@ -0,0 +1,84 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float padding_2;
+    vec3 reset_velocity;
+    float padding_3;
+};
+
+layout(std430, binding = 0) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float deltaTime;
+    float rand;
+};
+
+vec3 attraction(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    const float damp = 0.5;
+    float dDampedDot = dot(delta, delta) + damp;
+    float invDist = 1.0f / sqrt(dDampedDot);
+    float invDistCubed = invDist*invDist*invDist;
+    return delta * invDistCubed * 0.0035;
+}
+
+vec3 repulsion(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    float targetDistance = sqrt(dot(delta, delta));
+    return delta * (1.0 / (targetDistance * targetDistance * targetDistance)) * -0.000035;
+}
+
+
+const int n = 3;
+vec3 gravity = vec3(0,-9.8,0);
+vec3 gravityPoint[n] = vec3[n](vec3(-0.5, 0.5, 0),vec3(0.5, 0.5, 0),vec3(0, -0.5, 0));
+//vec3 gravityPoint[n] = vec3[n](vec3(-0.5, 0.5, 0));
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+    inParticle[id].lifeTime -= deltaTime;
+    vec3 pos = inParticle[id].position;
+    vec3 vel = inParticle[id].velocity;
+    if(inParticle[id].lifeTime < 0.f)
+    {
+        inParticle[id].lifeTime = 7.f;
+        pos = vec3(0);
+        vel = inParticle[id].reset_velocity;
+        inParticle[id].velocity = inParticle[id].reset_velocity;
+    }
+    //    inParticle[id].position += deltaTime * -normalize(max(2 - distance(inParticle[id].position,respawnPos),0.0) * respawnPos - inParticle[id].position);
+
+    for(int i = 0; i < n; i++)
+    {
+        vel += deltaTime * deltaTime * deltaTime * normalize(max(2 - distance(pos,gravityPoint[i]),0.1) * gravityPoint[i] - pos);
+    }
+
+    //vec3 delta = respawnPos - pos;
+    //float targetDistane = sqrt(dot(delta,delta));
+    //vel += repulsion(pos, respawnPos);
+
+    //if((pos.x <= -1.0) || (pos.x > 1.0) || (pos.y <= -1.0) || (pos.y > 1.0)|| (pos.z <= -1.0) || (pos.z > 1.0))
+    vel = (-vel * 0.01);
+
+    if((pos.y <= -1.0) || (pos.y > 1.0)){
+        vel = reflect(vel, vec3(0,1,0));
+    }
+
+    pos += normalize(vel) * deltaTime;
+    inParticle[id].position = pos;
+
+    float weight = 1.0;
+    float rand1 = rand;
+    inParticle[id].velocity = vel;
+}
diff --git a/projects/particle_simulation/shaders/shader_water.frag b/projects/particle_simulation/shaders/shader_water.frag
new file mode 100644
index 0000000000000000000000000000000000000000..b68f9572a91b05e836c3fead9ae9afd7ce16ba8e
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_water.frag
@@ -0,0 +1,46 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particleShading.inc"
+
+layout(location = 0) in vec2 passTriangleCoordinates;
+layout(location = 1) in vec3 passVelocity;
+layout(location = 2) in float passlifeTime;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=0) uniform uColor {
+	vec4 color;
+} Color;
+
+layout(set=0,binding=1) uniform uPosition{
+	vec2 position;
+} Position;
+
+void main()
+{
+	float normlt = 1-normalize(passlifeTime);
+	vec2 mouse = vec2(Position.position.x, Position.position.y);
+    
+    vec3 c0 = vec3(0.2,0.5,1);
+    vec3 c1 = vec3(0.3, 0.7,1);
+    vec3 c2 = vec3(0.5,0.9,1);
+    vec3 c3 = vec3(0.9,1,1);
+    
+    if(passlifeTime  < 1){
+        outColor = mix(c0, c1, passlifeTime );
+    }
+    else if(passlifeTime  < 2){
+        outColor = mix(c1, c2, passlifeTime  - 1);
+    }
+    else{
+        outColor = mix(c2, c3, clamp((passlifeTime  - 2) * 0.5, 0, 1));
+    }
+    
+    // make the triangle look like a circle
+   outColor *= circleFactor(passTriangleCoordinates);
+   
+   // fade out particle shortly before it dies
+   outColor *= clamp(passlifeTime * 2, 0, 1);
+}
diff --git a/projects/particle_simulation/shaders/tonemapping.comp b/projects/particle_simulation/shaders/tonemapping.comp
new file mode 100644
index 0000000000000000000000000000000000000000..26f0232d66e3475afdd1266c0cc6288b47ed1c38
--- /dev/null
+++ b/projects/particle_simulation/shaders/tonemapping.comp
@@ -0,0 +1,19 @@
+#version 440
+
+layout(set=0, binding=0, rgba16f)   uniform image2D inImage;
+layout(set=0, binding=1, rgba8)     uniform image2D outImage;
+
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(inImage)))){
+        return;
+    }
+    ivec2 uv            = ivec2(gl_GlobalInvocationID.xy);
+    vec3 linearColor    = imageLoad(inImage, uv).rgb;
+    vec3 tonemapped     = linearColor / (dot(linearColor, vec3(0.21, 0.71, 0.08)) + 1); // reinhard tonemapping
+    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
+    imageStore(outImage, uv, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..23ace2bc35a2e421613718c62380f9161a408f70
--- /dev/null
+++ b/projects/particle_simulation/src/BloomAndFlares.cpp
@@ -0,0 +1,274 @@
+#include "BloomAndFlares.hpp"
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+BloomAndFlares::BloomAndFlares(
+        vkcv::Core *p_Core,
+        vk::Format colorBufferFormat,
+        uint32_t width,
+        uint32_t height) :
+
+        p_Core(p_Core),
+        m_ColorBufferFormat(colorBufferFormat),
+        m_Width(width),
+        m_Height(height),
+        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
+                                              vkcv::SamplerFilterType::LINEAR,
+                                              vkcv::SamplerMipmapMode::LINEAR,
+                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
+        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
+        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
+{
+    vkcv::shader::GLSLCompiler compiler;
+
+    // DOWNSAMPLE
+    vkcv::ShaderProgram dsProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/downsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         dsProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+		m_DownsampleDescSets.push_back(
+                p_Core->createDescriptorSet(dsProg.getReflectedDescriptors()[0]));
+    }
+    m_DownsamplePipe = p_Core->createComputePipeline(
+            dsProg, { p_Core->getDescriptorSet(m_DownsampleDescSets[0]).layout });
+
+    // UPSAMPLE
+    vkcv::ShaderProgram usProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/upsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         usProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+        m_UpsampleDescSets.push_back(
+                p_Core->createDescriptorSet(usProg.getReflectedDescriptors()[0]));
+    }
+    m_UpsamplePipe = p_Core->createComputePipeline(
+            usProg, { p_Core->getDescriptorSet(m_UpsampleDescSets[0]).layout });
+
+    // LENS FEATURES
+    vkcv::ShaderProgram lensProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/lensFlares.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         lensProg.addShader(shaderStage, path);
+                     });
+    m_LensFlareDescSet = p_Core->createDescriptorSet(lensProg.getReflectedDescriptors()[0]);
+    m_LensFlarePipe = p_Core->createComputePipeline(
+            lensProg, { p_Core->getDescriptorSet(m_LensFlareDescSet).layout });
+
+    // COMPOSITE
+    vkcv::ShaderProgram compProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/composite.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         compProg.addShader(shaderStage, path);
+                     });
+    m_CompositeDescSet = p_Core->createDescriptorSet(compProg.getReflectedDescriptors()[0]);
+    m_CompositePipe = p_Core->createComputePipeline(
+            compProg, { p_Core->getDescriptorSet(m_CompositeDescSet).layout });
+}
+
+void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
+                                        const vkcv::ImageHandle &colorAttachment)
+{
+    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
+    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+    // blur dispatch
+    uint32_t initialDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+
+    // downsample dispatch of original color attachment
+    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    vkcv::DescriptorWrites initialDownsampleWrites;
+    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
+    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
+    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
+
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_DownsamplePipe,
+            initialDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 0));
+
+    // downsample dispatches of blur buffer's mip maps
+    float mipDispatchCountX = dispatchCountX;
+    float mipDispatchCountY = dispatchCountY;
+    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipDescriptorWrites;
+        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
+        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
+        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
+
+        // mip dispatch calculation
+        mipDispatchCountX  /= 2.0f;
+        mipDispatchCountY /= 2.0f;
+
+        uint32_t mipDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
+                1
+        };
+
+        if(mipDispatchCount[0] == 0)
+            mipDispatchCount[0] = 1;
+        if(mipDispatchCount[1] == 0)
+            mipDispatchCount[1] = 1;
+
+        // mip blur dispatch
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_DownsamplePipe,
+                mipDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstantData(nullptr, 0));
+
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // upsample dispatch
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    const uint32_t upsampleMipLevels = std::min(
+    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
+    		static_cast<uint32_t>(3)
+	);
+
+    // upsample dispatch for each mip map
+    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipUpsampleWrites;
+        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
+        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
+        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
+
+        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
+
+        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
+        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
+        upsampleDispatchX /= 8.0f;
+        upsampleDispatchY /= 8.0f;
+
+        const uint32_t upsampleDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
+                1
+        };
+
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_UpsamplePipe,
+                upsampleDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstantData(nullptr, 0)
+        );
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // lens feature generation descriptor writes
+    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
+    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
+
+    vkcv::DescriptorWrites lensFeatureWrites;
+    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
+    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
+    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
+
+    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
+    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+    // lens feature generation dispatch
+    uint32_t lensFeatureDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_LensFlarePipe,
+            lensFeatureDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 0));
+}
+
+void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
+                                       const vkcv::ImageHandle &colorAttachment)
+{
+    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
+    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
+    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
+
+    // bloom composite descriptor write
+    vkcv::DescriptorWrites compositeWrites;
+    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
+                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
+    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
+    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
+    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
+
+    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
+    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+
+    uint32_t compositeDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+
+    // bloom composite dispatch
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_CompositePipe,
+            compositeDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 0));
+}
+
+void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
+                                       const vkcv::ImageHandle &colorAttachment)
+{
+    execDownsamplePipe(cmdStream, colorAttachment);
+    execUpsamplePipe(cmdStream);
+    execLensFeaturePipe(cmdStream);
+    execCompositePipe(cmdStream, colorAttachment);
+}
+
+void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
+{
+    m_Width  = width;
+    m_Height = height;
+
+    p_Core->getContext().getDevice().waitIdle();
+    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
+    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
+}
+
+
diff --git a/projects/particle_simulation/src/BloomAndFlares.hpp b/projects/particle_simulation/src/BloomAndFlares.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..756b1ca154ea5232df04eb09a88bb743c5bd28aa
--- /dev/null
+++ b/projects/particle_simulation/src/BloomAndFlares.hpp
@@ -0,0 +1,47 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include <glm/glm.hpp>
+
+class BloomAndFlares{
+public:
+    BloomAndFlares(vkcv::Core *p_Core,
+                   vk::Format colorBufferFormat,
+                   uint32_t width,
+                   uint32_t height);
+
+    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+
+    void updateImageDimensions(uint32_t width, uint32_t height);
+
+private:
+    vkcv::Core *p_Core;
+
+    vk::Format m_ColorBufferFormat;
+    uint32_t m_Width;
+    uint32_t m_Height;
+
+    vkcv::SamplerHandle m_LinearSampler;
+    vkcv::Image m_Blur;
+    vkcv::Image m_LensFeatures;
+
+
+    vkcv::PipelineHandle                     m_DownsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_DownsampleDescSets; // per mip desc set
+
+    vkcv::PipelineHandle                     m_UpsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_UpsampleDescSets;   // per mip desc set
+
+    vkcv::PipelineHandle                     m_LensFlarePipe;
+    vkcv::DescriptorSetHandle                m_LensFlareDescSet;
+
+    vkcv::PipelineHandle                     m_CompositePipe;
+    vkcv::DescriptorSetHandle                m_CompositeDescSet;
+
+    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+};
+
+
+
diff --git a/projects/particle_simulation/src/Particle.cpp b/projects/particle_simulation/src/Particle.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..387728eb366430e4373282da785bbff47de17e7a
--- /dev/null
+++ b/projects/particle_simulation/src/Particle.cpp
@@ -0,0 +1,41 @@
+
+#include "Particle.hpp"
+
+Particle::Particle(glm::vec3 position, glm::vec3 velocity, float lifeTime)
+: m_position(position),
+m_velocity(velocity),
+m_lifeTime(lifeTime),
+m_reset_velocity(velocity)
+{}
+
+const glm::vec3& Particle::getPosition()const{
+    return m_position;
+}
+
+const bool Particle::isAlive()const{
+    return m_lifeTime > 0.f;
+}
+
+void Particle::setPosition( const glm::vec3 pos ){
+    m_position = pos;
+}
+
+const glm::vec3& Particle::getVelocity()const{
+    return m_velocity;
+}
+
+void Particle::setVelocity( const glm::vec3 vel ){
+    m_velocity = vel;
+}
+
+void Particle::update( const float delta ){
+    m_position += m_velocity * delta;
+}
+
+void Particle::setLifeTime( const float lifeTime ){
+    m_lifeTime = lifeTime;
+}
+
+const float& Particle::getLifeTime()const{
+    return m_lifeTime;
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/src/Particle.hpp b/projects/particle_simulation/src/Particle.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f374218fd8a08f1e1bf367bdc899a71c55ea1b78
--- /dev/null
+++ b/projects/particle_simulation/src/Particle.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <glm/glm.hpp>
+
+class Particle {
+
+public:
+    Particle(glm::vec3 position, glm::vec3 velocity, float lifeTime = 1.f);
+
+    const glm::vec3& getPosition()const;
+
+    void setPosition( const glm::vec3 pos );
+
+    const glm::vec3& getVelocity()const;
+
+    void setVelocity( const glm::vec3 vel );
+
+    void update( const float delta );
+
+    const bool isAlive()const;
+
+    void setLifeTime( const float lifeTime );
+
+    const float& getLifeTime()const;
+
+private:
+    // all properties of the Particle
+    glm::vec3 m_position;
+    float m_lifeTime;
+    glm::vec3 m_velocity;
+    float mass = 1.f;
+    glm::vec3 m_reset_velocity;
+    float padding_3 = 0.f;
+};
diff --git a/projects/particle_simulation/src/ParticleSystem.cpp b/projects/particle_simulation/src/ParticleSystem.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b3162d6bea685640d3949577271affc8b2080407
--- /dev/null
+++ b/projects/particle_simulation/src/ParticleSystem.cpp
@@ -0,0 +1,60 @@
+#include "ParticleSystem.hpp"
+
+ParticleSystem::ParticleSystem(uint32_t particleCount ,glm::vec3 minVelocity , glm::vec3 maxVelocity , glm::vec2 lifeTime )
+{
+    m_rdmVel.resize(3);
+    m_rdmVel[0] = std::uniform_real_distribution<float>(minVelocity.x, maxVelocity.x);
+    m_rdmVel[1] = std::uniform_real_distribution<float>(minVelocity.y, maxVelocity.y);
+    m_rdmVel[2] = std::uniform_real_distribution<float>(minVelocity.z, maxVelocity.z);
+    m_rdmLifeTime = std::uniform_real_distribution<float>(lifeTime.x, lifeTime.y);
+
+    for(uint32_t i = 0; i < particleCount ;i++ ){
+        addParticle(Particle(m_respawnPos, getRandomVelocity(), getRandomLifeTime()));
+    }
+}
+
+const std::vector<Particle>& ParticleSystem::getParticles() const{
+    return m_particles;
+}
+
+void ParticleSystem::addParticle( const Particle particle ){
+    m_particles.push_back(particle);
+}
+void ParticleSystem::addParticles( const std::vector<Particle> particles ){
+    m_particles.insert(m_particles.end(), particles.begin(), particles.end());
+}
+
+void ParticleSystem::updateParticles( const float deltaTime ){
+    for(Particle& particle :m_particles){
+        bool alive = particle.isAlive();
+        particle.setPosition( particle.getPosition() * static_cast<float>(alive) + static_cast<float>(!alive) * m_respawnPos );
+        particle.setVelocity( particle.getVelocity() * static_cast<float>(alive) + static_cast<float>(!alive) *  getRandomVelocity());
+        particle.setLifeTime( (particle.getLifeTime() * alive + !alive * getRandomLifeTime() ) - deltaTime );
+        particle.update(deltaTime);
+    }
+}
+
+glm::vec3 ParticleSystem::getRandomVelocity(){
+    return glm::vec3(m_rdmVel[0](m_rdmEngine), m_rdmVel[1](m_rdmEngine),m_rdmVel[2](m_rdmEngine));
+}
+
+float ParticleSystem::getRandomLifeTime(){
+    return m_rdmLifeTime(m_rdmEngine);
+}
+
+void ParticleSystem::setRespawnPos( const glm::vec3 respawnPos){
+    m_respawnPos = respawnPos;
+}
+void ParticleSystem::setRdmLifeTime( const glm::vec2 lifeTime ){
+    m_rdmLifeTime = std::uniform_real_distribution<float> (lifeTime.x,lifeTime.y);
+}
+
+void ParticleSystem::setRdmVelocity( glm::vec3 minVelocity, glm::vec3 maxVelocity ){
+    m_rdmVel[0] = std::uniform_real_distribution<float> (minVelocity.x,maxVelocity.x);
+    m_rdmVel[1] = std::uniform_real_distribution<float> (minVelocity.y,maxVelocity.y);
+    m_rdmVel[2] = std::uniform_real_distribution<float> (minVelocity.z,maxVelocity.z);
+}
+
+const glm::vec3 ParticleSystem::getRespawnPos() const{
+    return m_respawnPos;
+}
diff --git a/projects/particle_simulation/src/ParticleSystem.hpp b/projects/particle_simulation/src/ParticleSystem.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fe5c99f9b407b9dbdfd414e265e7cd91bbe790b9
--- /dev/null
+++ b/projects/particle_simulation/src/ParticleSystem.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <vector>
+#include "Particle.hpp"
+#include <random>
+#include "vkcv/Buffer.hpp"
+
+class ParticleSystem {
+
+public:
+    ParticleSystem(uint32_t particleCount , glm::vec3 minVelocity = glm::vec3(0.f,0.f,0.f), glm::vec3 maxVelocity = glm::vec3(1.f,1.f,0.f), glm::vec2 lifeTime = glm::vec2(2.f,3.f));
+    const std::vector<Particle> &getParticles() const;
+    void updateParticles( const float deltaTime );
+    void setRespawnPos( const glm::vec3 respawnPos );
+    void setRdmLifeTime( const glm::vec2 lifeTime );
+    void setRdmVelocity( glm::vec3 minVelocity, glm::vec3 maxVelocity );
+    const glm::vec3 getRespawnPos() const;
+
+private:
+
+    void addParticle( const Particle particle );
+    void addParticles( const std::vector<Particle> particles );
+    glm::vec3 getRandomVelocity();
+    float getRandomLifeTime();
+
+    std::vector<Particle> m_particles;
+    glm::vec3 m_respawnPos = glm::vec3(0.f);
+
+    std::vector<std::uniform_real_distribution<float>> m_rdmVel;
+    std::uniform_real_distribution<float> m_rdmLifeTime;
+    std::default_random_engine m_rdmEngine;
+};
\ No newline at end of file
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a22044f0d2588a43a5e7a0f6cba25d9c7460be9f
--- /dev/null
+++ b/projects/particle_simulation/src/main.cpp
@@ -0,0 +1,320 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+#include "ParticleSystem.hpp"
+#include <random>
+#include <glm/gtc/matrix_access.hpp>
+#include <time.h>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include "BloomAndFlares.hpp"
+
+int main(int argc, const char **argv) {
+    const char *applicationName = "Particlesystem";
+
+    uint32_t windowWidth = 800;
+    uint32_t windowHeight = 600;
+    vkcv::Window window = vkcv::Window::create(
+            applicationName,
+            windowWidth,
+            windowHeight,
+            true
+    );
+
+    vkcv::camera::CameraManager cameraManager(window);
+
+    vkcv::Core core = vkcv::Core::create(
+            window,
+            applicationName,
+            VK_MAKE_VERSION(0, 0, 1),
+            {vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
+            {},
+            {"VK_KHR_swapchain"}
+    );
+
+    auto particleIndexBuffer = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3,
+                                                           vkcv::BufferMemoryType::DEVICE_LOCAL);
+    uint16_t indices[3] = {0, 1, 2};
+    particleIndexBuffer.fill(&indices[0], sizeof(indices));
+
+    vk::Format colorFormat = vk::Format::eR16G16B16A16Sfloat;
+    // an example attachment for passes that output to the window
+    const vkcv::AttachmentDescription present_color_attachment(
+            vkcv::AttachmentOperation::STORE,
+            vkcv::AttachmentOperation::CLEAR,
+            colorFormat);
+
+
+    vkcv::PassConfig particlePassDefinition({present_color_attachment});
+    vkcv::PassHandle particlePass = core.createPass(particlePassDefinition);
+
+    vkcv::PassConfig computePassDefinition({});
+    vkcv::PassHandle computePass = core.createPass(computePassDefinition);
+
+    if (!particlePass || !computePass)
+    {
+        std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+        return EXIT_FAILURE;
+    }
+
+    // use space or use water
+    bool useSpace = true;
+
+    vkcv::shader::GLSLCompiler compiler;
+    vkcv::ShaderProgram computeShaderProgram{};
+    compiler.compile(vkcv::ShaderStage::COMPUTE, useSpace ? "shaders/shader_space.comp" : "shaders/shader_water.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        computeShaderProgram.addShader(shaderStage, path);
+    });
+
+    vkcv::DescriptorSetHandle computeDescriptorSet = core.createDescriptorSet(computeShaderProgram.getReflectedDescriptors()[0]);
+
+    const std::vector<vkcv::VertexAttachment> computeVertexAttachments = computeShaderProgram.getVertexAttachments();
+
+    std::vector<vkcv::VertexBinding> computeBindings;
+    for (size_t i = 0; i < computeVertexAttachments.size(); i++) {
+        computeBindings.push_back(vkcv::VertexBinding(i, { computeVertexAttachments[i] }));
+    }
+    const vkcv::VertexLayout computeLayout(computeBindings);
+
+    vkcv::ShaderProgram particleShaderProgram{};
+    compiler.compile(vkcv::ShaderStage::VERTEX, "shaders/shader.vert", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        particleShaderProgram.addShader(shaderStage, path);
+    });
+    compiler.compile(vkcv::ShaderStage::FRAGMENT, useSpace ? "shaders/shader_space.frag" : "shaders/shader_water.frag", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        particleShaderProgram.addShader(shaderStage, path);
+    });
+
+    vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(
+            particleShaderProgram.getReflectedDescriptors()[0]);
+
+    vkcv::Buffer<glm::vec3> vertexBuffer = core.createBuffer<glm::vec3>(
+            vkcv::BufferType::VERTEX,
+            3
+    );
+    const std::vector<vkcv::VertexAttachment> vertexAttachments = particleShaderProgram.getVertexAttachments();
+
+    const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
+            vkcv::VertexBufferBinding(0, vertexBuffer.getVulkanHandle())};
+
+    std::vector<vkcv::VertexBinding> bindings;
+    for (size_t i = 0; i < vertexAttachments.size(); i++) {
+        bindings.push_back(vkcv::VertexBinding(i, {vertexAttachments[i]}));
+    }
+
+    const vkcv::VertexLayout particleLayout(bindings);
+
+    vkcv::PipelineConfig particlePipelineDefinition{
+            particleShaderProgram,
+            UINT32_MAX,
+            UINT32_MAX,
+            particlePass,
+            {particleLayout},
+            {core.getDescriptorSet(descriptorSet).layout},
+            true};
+    particlePipelineDefinition.m_blendMode = vkcv::BlendMode::Additive;
+
+    const std::vector<glm::vec3> vertices = {glm::vec3(-0.012, 0.012, 0),
+                                             glm::vec3(0.012, 0.012, 0),
+                                             glm::vec3(0, -0.012, 0)};
+
+    vertexBuffer.fill(vertices);
+
+    vkcv::PipelineHandle particlePipeline = core.createGraphicsPipeline(particlePipelineDefinition);
+
+    vkcv::PipelineHandle computePipeline = core.createComputePipeline(computeShaderProgram, {core.getDescriptorSet(computeDescriptorSet).layout} );
+
+    vkcv::Buffer<glm::vec4> color = core.createBuffer<glm::vec4>(
+            vkcv::BufferType::UNIFORM,
+            1
+    );
+
+    vkcv::Buffer<glm::vec2> position = core.createBuffer<glm::vec2>(
+            vkcv::BufferType::UNIFORM,
+            1
+    );
+
+    glm::vec3 minVelocity = glm::vec3(-0.1f,-0.1f,-0.1f);
+    glm::vec3 maxVelocity = glm::vec3(0.1f,0.1f,0.1f);
+    glm::vec2 lifeTime = glm::vec2(-1.f,8.f);
+    ParticleSystem particleSystem = ParticleSystem( 100000 , minVelocity, maxVelocity, lifeTime);
+
+    vkcv::Buffer<Particle> particleBuffer = core.createBuffer<Particle>(
+            vkcv::BufferType::STORAGE,
+            particleSystem.getParticles().size()
+    );
+
+    particleBuffer.fill(particleSystem.getParticles());
+
+    vkcv::DescriptorWrites setWrites;
+    setWrites.uniformBufferWrites = {vkcv::UniformBufferDescriptorWrite(0,color.getHandle()),
+                                     vkcv::UniformBufferDescriptorWrite(1,position.getHandle())};
+    setWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(2,particleBuffer.getHandle())};
+    core.writeDescriptorSet(descriptorSet, setWrites);
+
+    vkcv::DescriptorWrites computeWrites;
+    computeWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0,particleBuffer.getHandle())};
+    core.writeDescriptorSet(computeDescriptorSet, computeWrites);
+
+    if (!particlePipeline || !computePipeline)
+    {
+        std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+        return EXIT_FAILURE;
+    }
+
+    const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+    const vkcv::Mesh renderMesh({vertexBufferBindings}, particleIndexBuffer.getVulkanHandle(),
+                                particleIndexBuffer.getCount());
+    vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+    //vkcv::DrawcallInfo drawcalls(renderMesh, {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle)});
+
+    glm::vec2 pos = glm::vec2(0.f);
+    glm::vec3 spawnPosition = glm::vec3(0.f);
+    glm::vec4 tempPosition = glm::vec4(0.f);
+
+    window.e_mouseMove.add([&](double offsetX, double offsetY) {
+        pos = glm::vec2(static_cast<float>(offsetX), static_cast<float>(offsetY));
+//        std::cout << offsetX << " , " << offsetY << std::endl;
+        // borders are assumed to be 0.5
+        //pos = glm::vec2((pos.x -0.5f * static_cast<float>(window.getWidth()))/static_cast<float>(window.getWidth()), (pos.y -0.5f * static_cast<float>(window.getHeight()))/static_cast<float>(window.getHeight()));
+        //borders are assumed to be 1
+        pos.x = (-2 * pos.x + static_cast<float>(window.getWidth())) / static_cast<float>(window.getWidth());
+        pos.y = (-2 * pos.y + static_cast<float>(window.getHeight())) / static_cast<float>(window.getHeight());
+        glm::vec4 row1 = glm::row(cameraManager.getCamera(0).getView(), 0);
+        glm::vec4 row2 = glm::row(cameraManager.getCamera(0).getView(), 1);
+        glm::vec4 row3 = glm::row(cameraManager.getCamera(0).getView(), 2);
+        glm::vec4 camera_pos = glm::column(cameraManager.getCamera(0).getView(), 3);
+//        std::cout << "row1: " << row1.x << ", " << row1.y << ", " << row1.z << std::endl;
+//        std::cout << "row2: " << row2.x << ", " << row2.y << ", " << row2.z << std::endl;
+//        std::cout << "row3: " << row3.x << ", " << row3.y << ", " << row3.z << std::endl;
+//        std::cout << "camerapos: " << camera_pos.x << ", " << camera_pos.y << ", " << camera_pos.z << std::endl;
+//        std::cout << "camerapos: " << camera_pos.x << ", " << camera_pos.y << ", " << camera_pos.z << std::endl;
+        //glm::vec4 view_axis = glm::row(cameraManager.getCamera().getView(), 2);
+        // std::cout << "view_axis: " << view_axis.x << ", " << view_axis.y << ", " << view_axis.z << std::endl;
+        //std::cout << "Front: " << cameraManager.getCamera().getFront().x << ", " << cameraManager.getCamera().getFront().z << ", " << cameraManager.getCamera().getFront().z << std::endl;
+        glm::mat4 viewmat = cameraManager.getCamera(0).getView();
+        spawnPosition = glm::vec3(pos.x, pos.y, 0.f);
+        tempPosition = glm::vec4(spawnPosition, 1.0f);
+        spawnPosition = glm::vec3(tempPosition.x, tempPosition.y, tempPosition.z);
+        particleSystem.setRespawnPos(glm::vec3(-spawnPosition.x, spawnPosition.y, spawnPosition.z));
+//        std::cout << "respawn pos: " << spawnPosition.x << ", " << spawnPosition.y << ", " << spawnPosition.z << std::endl;
+    });
+
+    std::vector<glm::mat4> modelMatrices;
+    std::vector<vkcv::DrawcallInfo> drawcalls;
+    drawcalls.push_back(vkcv::DrawcallInfo(renderMesh, {descriptorUsage}, particleSystem.getParticles().size()));
+
+    auto start = std::chrono::system_clock::now();
+
+    glm::vec4 colorData = glm::vec4(1.0f, 1.0f, 0.0f, 1.0f);
+    uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+    uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+
+    cameraManager.getCamera(camIndex0).setNearFar(0.1, 30);
+    cameraManager.getCamera(camIndex1).setNearFar(0.1, 30);
+
+    cameraManager.setActiveCamera(1);
+
+    cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -2));
+    cameraManager.getCamera(camIndex1).setPosition(glm::vec3(0.0f, 0.0f, -2.0f));
+    cameraManager.getCamera(camIndex1).setCenter(glm::vec3(0.0f, 0.0f, 0.0f));
+
+    vkcv::ImageHandle colorBuffer = core.createImage(colorFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
+    BloomAndFlares bloomAndFlares(&core, colorFormat, windowWidth, windowHeight);
+    window.e_resize.add([&](int width, int height) {
+        windowWidth = width;
+        windowHeight = height;
+        colorBuffer = core.createImage(colorFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
+        bloomAndFlares.updateImageDimensions(width, height);
+    });
+
+    vkcv::ShaderProgram tonemappingShader;
+    compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/tonemapping.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        tonemappingShader.addShader(shaderStage, path);
+    });
+
+    vkcv::DescriptorSetHandle tonemappingDescriptor = core.createDescriptorSet(tonemappingShader.getReflectedDescriptors()[0]);
+    vkcv::PipelineHandle tonemappingPipe = core.createComputePipeline(
+        tonemappingShader, 
+        { core.getDescriptorSet(tonemappingDescriptor).layout });
+
+    std::uniform_real_distribution<float> rdm = std::uniform_real_distribution<float>(0.95f, 1.05f);
+    std::default_random_engine rdmEngine;
+    while (window.isWindowOpen()) {
+        window.pollEvents();
+
+        uint32_t swapchainWidth, swapchainHeight;
+        if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+            continue;
+        }
+
+        color.fill(&colorData);
+        position.fill(&pos);
+
+        auto end = std::chrono::system_clock::now();
+        float deltatime = 0.000001 * static_cast<float>( std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() );
+        start = end;
+//        particleSystem.updateParticles(deltatime);
+
+        cameraManager.update(deltatime);
+
+        // split view and projection to allow for easy billboarding in shader
+        glm::mat4 renderingMatrices[2];
+        renderingMatrices[0] = cameraManager.getActiveCamera().getView();
+        renderingMatrices[1] = cameraManager.getActiveCamera().getProjection();
+
+        auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+        float random = rdm(rdmEngine);
+        glm::vec2 pushData = glm::vec2(deltatime, random);
+
+        vkcv::PushConstantData pushConstantDataCompute( &pushData, sizeof(glm::vec2));
+        uint32_t computeDispatchCount[3] = {static_cast<uint32_t> (std::ceil(particleSystem.getParticles().size()/256.f)),1,1};
+        core.recordComputeDispatchToCmdStream(cmdStream,
+                                              computePipeline,
+                                              computeDispatchCount,
+                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle)},
+                                              pushConstantDataCompute);
+
+        core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
+
+        vkcv::PushConstantData pushConstantDataDraw((void *) &renderingMatrices[0], 2 * sizeof(glm::mat4));
+        core.recordDrawcallsToCmdStream(
+                cmdStream,
+                particlePass,
+                particlePipeline,
+                pushConstantDataDraw,
+                {drawcalls},
+                { colorBuffer });
+
+        bloomAndFlares.execWholePipeline(cmdStream, colorBuffer);
+
+        core.prepareImageForStorage(cmdStream, colorBuffer);
+        core.prepareImageForStorage(cmdStream, swapchainInput);
+
+        vkcv::DescriptorWrites tonemappingDescriptorWrites;
+        tonemappingDescriptorWrites.storageImageWrites = {
+            vkcv::StorageImageDescriptorWrite(0, colorBuffer),
+            vkcv::StorageImageDescriptorWrite(1, swapchainInput)
+        };
+        core.writeDescriptorSet(tonemappingDescriptor, tonemappingDescriptorWrites);
+
+        uint32_t tonemappingDispatchCount[3];
+        tonemappingDispatchCount[0] = std::ceil(windowWidth / 8.f);
+        tonemappingDispatchCount[1] = std::ceil(windowHeight / 8.f);
+        tonemappingDispatchCount[2] = 1;
+
+        core.recordComputeDispatchToCmdStream(
+            cmdStream, 
+            tonemappingPipe, 
+            tonemappingDispatchCount, 
+            {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
+            vkcv::PushConstantData(nullptr, 0));
+
+        core.prepareSwapchainImageForPresent(cmdStream);
+        core.submitCommandStream(cmdStream);
+        core.endFrame();
+    }
+
+    return 0;
+}
diff --git a/projects/voxelization/.gitignore b/projects/voxelization/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..f07a22d4e0641a9114e998212f38ca9cb83a9655
--- /dev/null
+++ b/projects/voxelization/.gitignore
@@ -0,0 +1 @@
+voxelization
\ No newline at end of file
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c962409f2e14994f0c38b923de7b9b1a4d198cab
--- /dev/null
+++ b/projects/voxelization/CMakeLists.txt
@@ -0,0 +1,36 @@
+cmake_minimum_required(VERSION 3.16)
+project(voxelization)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(voxelization src/main.cpp)
+
+target_sources(voxelization PRIVATE
+    src/Voxelization.hpp
+    src/Voxelization.cpp
+    src/ShadowMapping.hpp
+    src/ShadowMapping.cpp
+    src/BloomAndFlares.hpp
+    src/BloomAndFlares.cpp)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(voxelization PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(voxelization 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(voxelization PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# 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})
+
+# 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)
diff --git a/projects/voxelization/resources/RadialLUT.png b/projects/voxelization/resources/RadialLUT.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b7056cf2a35c4d41f142e52bbc48dd1a91e4758
--- /dev/null
+++ b/projects/voxelization/resources/RadialLUT.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70d59d4e9c1ce2a077ed60c19c8c4665bf0723c952612a2ca8ec32c55f9ec498
+size 900
diff --git a/projects/voxelization/resources/Sponza/Sponza.bin b/projects/voxelization/resources/Sponza/Sponza.bin
new file mode 100644
index 0000000000000000000000000000000000000000..eb0523cb55746451c1b20f25fb4ecfed22ef6047
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Sponza.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:232d9c216b72dc0ee6089bc070cf8a12cabecd6a00c1f04aea78ac361da53839
+size 10819832
diff --git a/projects/voxelization/resources/Sponza/Sponza.gltf b/projects/voxelization/resources/Sponza/Sponza.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..18d697f622eab38c3b3089a56c1680ff4a443171
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Sponza.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:748597f56b7228dddbe4c5d1cb84a35d645941933faf89c4c0f81dd9b602291e
+size 73667
diff --git a/projects/voxelization/resources/Sponza/Textures/Arch_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Arch_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..95900bdf9c8d57666b92929109a6750dc04a548b
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Arch_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8b68080eb4c5709536dfb7858d595d51d4bb725003e11b0f383acf76a7e05a3
+size 521947
diff --git a/projects/voxelization/resources/Sponza/Textures/Arch_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Arch_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..28e407a7c96acc4fbc9e75c7c8d6399914d409e3
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Arch_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:86defcb4c007b80b9d23bcc9fdccd994f6a68f710eb75f11396d92faa2fbe368
+size 206244
diff --git a/projects/voxelization/resources/Sponza/Textures/Arch_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Arch_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d1e0df9e5b0613ee476440b7e86d23535d5f4d05
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Arch_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:259b6659cb2681e06ef04becd49298d379f2f25b2cc468acf0c2ade4d190637c
+size 570081
diff --git a/projects/voxelization/resources/Sponza/Textures/Background_Albedo.png b/projects/voxelization/resources/Sponza/Textures/Background_Albedo.png
new file mode 100644
index 0000000000000000000000000000000000000000..668c3b6d389e9675bf8e482a51936fe1dff0c889
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Background_Albedo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a64b98be19acc30b95d071fb9dd1f0eaaf066612a2c757b32f8b7487f4600e9
+size 1653594
diff --git a/projects/voxelization/resources/Sponza/Textures/Background_Normal.png b/projects/voxelization/resources/Sponza/Textures/Background_Normal.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f03475bdcf3d26f1b152c30680998fbb6dec321
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Background_Normal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4e7fe693f80bbf4b543d3afc614390e498bb84664fd0251c3cfb4e99b4885a12
+size 1299002
diff --git a/projects/voxelization/resources/Sponza/Textures/Background_Roughness.png b/projects/voxelization/resources/Sponza/Textures/Background_Roughness.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f47f6a071463d13bb0f719570e3c53c4a65c079
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Background_Roughness.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15339be54f1f86593a48875dfde6947adf4ba49b3b945619ce3b4fbea64afbd1
+size 791544
diff --git a/projects/voxelization/resources/Sponza/Textures/Bricks_A_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Bricks_A_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..cff7cf6ceb272d99378ce4b7facd09ade9f7c61a
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Bricks_A_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9330cf1d51c8f6924b6c9ce09f2dedb21a82e1c7406c37d49ce95beca40bb3be
+size 564801
diff --git a/projects/voxelization/resources/Sponza/Textures/Bricks_A_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Bricks_A_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f83f46eb02cbf045886b8867b2dac93321800bf5
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Bricks_A_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0821ec8dc8226fd5ac70f068e134d014a9952276a7100cc7f0660853471b39f6
+size 339486
diff --git a/projects/voxelization/resources/Sponza/Textures/Bricks_A_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Bricks_A_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..da82e3992c1cdac7b90f294809193f4227da08cf
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Bricks_A_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bee42826d56d3ff719c4908c11ef65584fbe19e08273862fde7ec1408e44ba25
+size 530384
diff --git a/projects/voxelization/resources/Sponza/Textures/Ceiling_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Ceiling_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..20f8871aa9bb13d4c2ccc7a0aa9ddddcd226080b
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Ceiling_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5bbd0cbf2da7416d5620514ebb0c4837b0ccc841b649cf4070281aa333ff8520
+size 520162
diff --git a/projects/voxelization/resources/Sponza/Textures/Ceiling_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Ceiling_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8a8fba3de7e4b68e5d5a095cb30442fb30c53be6
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Ceiling_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e309f1e6310dc0bb85df0a5f3b139207876f067378796a7aa666b0bb169587ae
+size 919134
diff --git a/projects/voxelization/resources/Sponza/Textures/Ceiling_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Ceiling_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..59ec4fabc2df618dce40d73a1e71c23e780b0422
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Ceiling_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81b21b9352f1fb9ccfeaf237f8547b985ddbf0cfd7ade7a4a8670b73a72b2cb5
+size 608687
diff --git a/projects/voxelization/resources/Sponza/Textures/Chain_Diff.png b/projects/voxelization/resources/Sponza/Textures/Chain_Diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..9629f2f279c8b2d338724b96538f2858a9f3f4ff
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Chain_Diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d6df943a0dcf535d9dfb009c2833e0c7dc21939cfe3a2c10316c552b3dc3441b
+size 1077432
diff --git a/projects/voxelization/resources/Sponza/Textures/Chain_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Chain_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..127484f4ff3aa31597262c8e675c9d1f4dc979f1
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Chain_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:763740af8d22f0bfc6a43bd46050ef24bb661833bcace92f1f3326b9d5d7dc16
+size 137384
diff --git a/projects/voxelization/resources/Sponza/Textures/Cloth1_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Cloth1_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..cd117615246364139418024db5474e3e29ccc40b
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Cloth1_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3af8848a1a952ad8cc83199bbda0c095f9ed136be839659aa668f72afac91508
+size 1034060
diff --git a/projects/voxelization/resources/Sponza/Textures/Cloth1_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Cloth1_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e505337ce76a797c4b385c89c25f9486eccab9a8
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Cloth1_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:21bb124f5b5d4d5e9c8d9773cd31dedfc41b30eb302af36e7257cf930152908e
+size 486634
diff --git a/projects/voxelization/resources/Sponza/Textures/Cloth2_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Cloth2_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..35885a182fc5cf119afbebbeb2cc060850970731
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Cloth2_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:231d919288db7c977fda3091549195c1bd5197e9e63c039e972da8764f5435c1
+size 844768
diff --git a/projects/voxelization/resources/Sponza/Textures/Cloth2_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Cloth2_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..982536602c4633893fa19c27c0b0e08a8ea0aec8
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Cloth2_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8ad0e8a9ca6de602225a527361bc718881de8741bc48927e27259a8803e7fed7
+size 623144
diff --git a/projects/voxelization/resources/Sponza/Textures/ClothBlue1_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/ClothBlue1_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ae58b3bb674bec1f07a3f7de7eb52c8b4c23f4be
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/ClothBlue1_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:005b774a968c1f6af00e4bf97afd668a6a045126a94f524df722ba19edc77d61
+size 1151448
diff --git a/projects/voxelization/resources/Sponza/Textures/ClothBlue2_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/ClothBlue2_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e8597404fcae24e14967eb2220faa1d5cd4d8170
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/ClothBlue2_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:751606c94a5fe5bda3bf6709e7f6a74363bd7902bf2353d54e55ef3b50007487
+size 785678
diff --git a/projects/voxelization/resources/Sponza/Textures/ClothBlue2_Diff_jpg.jpg b/projects/voxelization/resources/Sponza/Textures/ClothBlue2_Diff_jpg.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ae58b3bb674bec1f07a3f7de7eb52c8b4c23f4be
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/ClothBlue2_Diff_jpg.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:005b774a968c1f6af00e4bf97afd668a6a045126a94f524df722ba19edc77d61
+size 1151448
diff --git a/projects/voxelization/resources/Sponza/Textures/ClothGreen1_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/ClothGreen1_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..833de50475dc9d4db7c36e0447bd8e17a5de41f5
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/ClothGreen1_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f55814430f289e89c7d675c3db925eee904fbc566ff83f4b49904de53138e98
+size 1044330
diff --git a/projects/voxelization/resources/Sponza/Textures/ClothGreen2_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/ClothGreen2_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..63ca00ba7ca17e80054fdf35b87aad4227edadef
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/ClothGreen2_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9d5fd79619a52db0232edb2287f5f05c0124c304355e3b035a4e48c0871090bf
+size 764529
diff --git a/projects/voxelization/resources/Sponza/Textures/ClothRed1_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/ClothRed1_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..406d620bf21db33765c00f87f3750f73c8df2de0
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/ClothRed1_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:23c32277c33d8eac091cd73daf81561ebe3640eede49b037546ed5c244a01347
+size 1079152
diff --git a/projects/voxelization/resources/Sponza/Textures/ClothRed2_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/ClothRed2_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f5b06c8dcee807f739981bfc061e4cd3d8f0a291
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/ClothRed2_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:88861c8c7f10517c1d7942f316aae3b74dc5b8ae2faa2021d9dfe9d2c09fb24f
+size 780166
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_B_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Column_B_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ba6158fe4718103e6f37ee0f1e482e4e5dfc25d8
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_B_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:170f6c637ebd71a101244eb4d80c3dcf7063caf89e42053d3114d8aeca755cdf
+size 643517
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_B_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Column_B_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..af99619ce8ff2b71246c00a79d94999c89f40e2e
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_B_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:46ffc9123ee3ad94b2595843b4446fd67dbf5a2f79dc1e3b4e637f5ea4578630
+size 652476
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_B_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Column_B_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..52c8e41dbc885800512ccc8527932be0f6964c63
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_B_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:93d495e33d754c5723c910e322ac94108fc753b65af082a19be5252905aa741b
+size 388462
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_C_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Column_C_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..53c725db4dece66f1458bc93dfb95ab20f8927cb
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_C_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9edc7c1c73661658cd8ed7733728002d45fecd17cb37a962fb654ff1d8c1933d
+size 665796
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_C_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Column_C_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9d2c5f3fe9792c825c50743e80969cb3bca660a9
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_C_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:58942b73c8f209ab9cc6ea174375adfde9c1875cbfe093f993086a0ab6570724
+size 698410
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_C_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Column_C_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..cf4a888d7023a9d23248a68d6217e5e1317c9b46
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_C_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:06c6480e207ce64f9e7c08df3f7f21638da7da43f77682657f4c823acd8c81a5
+size 193077
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Column_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f6e1fc5dfdbc52d8bf80bd736b6c7881f260b10d
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c39c9f304c2605be791b4c0a58cd0f8eb20de2dd6a4a4af893c4eb112d158353
+size 518310
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Column_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..549b3a7e290e79a4275124ce339c2d7a6b308cb2
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d54c520da7e60e2a62630e9cf946faf139648a7ab831c4531272238e4ade8fa8
+size 557865
diff --git a/projects/voxelization/resources/Sponza/Textures/Column_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Column_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..5c3770a5faebbe1e98df4b39367a87246b0161cc
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Column_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f06c93f8e9f90be44a2e78847e57acec82bd793f58f7be7f86b088150730b88
+size 260119
diff --git a/projects/voxelization/resources/Sponza/Textures/Detail_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Detail_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8ede3e8fff423456faf51cb1419261a33ab708d0
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Detail_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:98e4c4a091fea684cd693f3ae7ebe83e491969b501b4cd6e1fdd6a3cc1fa5cab
+size 321375
diff --git a/projects/voxelization/resources/Sponza/Textures/Detail_norm.jpg b/projects/voxelization/resources/Sponza/Textures/Detail_norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..6dad5879bba40f4273658e28f6d7fa3e3d8a663b
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Detail_norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9552937c9fa0481a07dc66768958b4be382544add46d1d66a687af54c51ab6da
+size 547553
diff --git a/projects/voxelization/resources/Sponza/Textures/Detail_spec.jpg b/projects/voxelization/resources/Sponza/Textures/Detail_spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d3918da21cdc2bcb27db235749791c45b1fb88f4
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Detail_spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:967005ccfbf4d38f674df41aebb3e58ac1fdcd91f554ead7acfcc132372f8ec6
+size 551432
diff --git a/projects/voxelization/resources/Sponza/Textures/Fill_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Fill_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b7dfccb5c93c5d215f3466b9352b6e77f4034555
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Fill_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5562f0e76ca213d72295e48b8a14a59ee1971f58a47f95e5c022a4e78371c5aa
+size 12575
diff --git a/projects/voxelization/resources/Sponza/Textures/Flagpole_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Flagpole_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..43e93c048438d4d57501c198f45d5888d5a478fd
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Flagpole_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fbdb7bcced57a005b84b4c3bbf744bae6309804cca51dabd9237e03a72fbc505
+size 399027
diff --git a/projects/voxelization/resources/Sponza/Textures/Flagpole_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Flagpole_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8a8c2891f2c44d175806df7d1451dea97649154a
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Flagpole_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:62a52f200082c02a0fbfa25c0bc40ea7d105a21f10d5aa0ec9408c4f45523794
+size 772201
diff --git a/projects/voxelization/resources/Sponza/Textures/Flagpole_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Flagpole_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..145dc9d42327723970227151f945048b34471287
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Flagpole_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d939d2e8bf11c2cf375154129f861b982c786b83db79cb9fde1a2b94b46caf62
+size 682743
diff --git a/projects/voxelization/resources/Sponza/Textures/Floor_A_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Floor_A_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d1c351ca22197a2556ff0acc08c73912f63fdac5
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Floor_A_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:34b464edddbd2295b0ee23fa7a7a440a19456888c586acdc47032f841b302abd
+size 573157
diff --git a/projects/voxelization/resources/Sponza/Textures/Floor_A_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Floor_A_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..0304c61dbae476fb226b151d44612fca91892e44
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Floor_A_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3a2c2aeb87a490a9bd37a8aa912a00e5f5c13974879fcb4f4f0ef39b17c52a7a
+size 718904
diff --git a/projects/voxelization/resources/Sponza/Textures/Floor_A_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Floor_A_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..90621f970579c5c8e268be5d415cab1162cf5910
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Floor_A_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9158388312a76ba25a40d3b57978e1ace224882e53198a766494d98f9118aeba
+size 313460
diff --git a/projects/voxelization/resources/Sponza/Textures/Flower_Diff.png b/projects/voxelization/resources/Sponza/Textures/Flower_Diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f297bbb9581dddb4bc61f8e29c14537a3415736
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Flower_Diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:75328d1aac87f8539be2456e6a5392cef7c6d08476d75585eafcd334dbeee0c5
+size 1225157
diff --git a/projects/voxelization/resources/Sponza/Textures/Flower_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Flower_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..23b8b1d0bc30d90e0dbf2964b7591e3a4c90c215
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Flower_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81ede4ce51045540817c1efb83dbb65a9a9701f1abd78fa130def51d227162fc
+size 421442
diff --git a/projects/voxelization/resources/Sponza/Textures/Flower_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Flower_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..fc60f6df8ee0a01601294062d585377ec14f28f3
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Flower_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d81efcb42b3a044595ca984cb46d941b159064bddae9a43ed8d9582db68d6b47
+size 280745
diff --git a/projects/voxelization/resources/Sponza/Textures/Lion_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Lion_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..51286b49a1eb9d1e52ad599f1d1a4abccc53d7f7
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Lion_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1ea4f9bc86354d363ea8153df9b7593c3bd3da153c53b9624c1fd8a960cb3ebc
+size 599959
diff --git a/projects/voxelization/resources/Sponza/Textures/Lion_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Lion_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..49132519e6e14b0a0964575ad92c86f805500421
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Lion_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f296e43c0d4c92953d4899ea0c38e8af992ba471acc87feb10f08e29a94aaef6
+size 512035
diff --git a/projects/voxelization/resources/Sponza/Textures/Lion_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Lion_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3058c29c325e5768bc3189a8c61548d44d88db55
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Lion_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8c26ae9523cfae7a062fd074f74e0ddbb61630772f9a2130489e1d6e4724c38b
+size 281980
diff --git a/projects/voxelization/resources/Sponza/Textures/Roof_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Roof_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1e9b6f6f9a00a99b4563229f76997abf402bcc7f
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Roof_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dfa17f2dee2cd2d850585673d19d5cda477a63e21659b346d8730e80edd3b347
+size 822373
diff --git a/projects/voxelization/resources/Sponza/Textures/Roof_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Roof_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..88ee02405bb090eed4ea2b658d0adcb93b1a8ed6
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Roof_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e5166a5fba339e44a9d197dedc168683971f8acb2156034248a80674cbba0dd5
+size 1607263
diff --git a/projects/voxelization/resources/Sponza/Textures/Roof_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Roof_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..dccdb78a2b439240d6cb3d79ce1694e53f83217f
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Roof_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8e90e94cadf80528a3df3496035d3c77900eba24d88b1c1d31c9dce77990fc44
+size 872255
diff --git a/projects/voxelization/resources/Sponza/Textures/Shield_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Shield_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..da701b850fd9bb49714263dba7c0d2681808ad5a
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Shield_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70d85bff3bc017177124942ae187db117ba2588e490746f440f2cef085852956
+size 357112
diff --git a/projects/voxelization/resources/Sponza/Textures/Shield_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Shield_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..c918da16e61180632dae222a622bd8d479ff9031
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Shield_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9cbd61fcb8047f1c21450eff05207b29d71824edd75483267b5fbc2f7b21378c
+size 329124
diff --git a/projects/voxelization/resources/Sponza/Textures/Shield_diff.jpg b/projects/voxelization/resources/Sponza/Textures/Shield_diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..90aa71962d62e0370f1d3550fc35781c54a466bf
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Shield_diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:73a94a12fb0a9ab4c75e40996c9a2d49ac45ac18a8c8ae81d5ceb1c8ae52d6dc
+size 362437
diff --git a/projects/voxelization/resources/Sponza/Textures/Thorn_Diff.png b/projects/voxelization/resources/Sponza/Textures/Thorn_Diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd52793a05b4f0827299d86c7446315ce67c1c68
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Thorn_Diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6fbdbf67d3501831ad3a4e2adbf3d27e4c3d13ba1d655e5861c3f39b5f899f65
+size 2427921
diff --git a/projects/voxelization/resources/Sponza/Textures/Thorn_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Thorn_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d85686c7aa40b03a94d00fa86dcd1f41681f1e1f
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Thorn_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dc5bf5f3091b548f1dfe40410b1aa1e2e55d82e2d77ba57dbd1f92955a0d8ff8
+size 413837
diff --git a/projects/voxelization/resources/Sponza/Textures/Thorn_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Thorn_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..dc414f0ea13433f7fe9832f4a64d0514ae9a2239
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Thorn_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da2a0b308b4d6d57ffbcf4bb7509d4afe91156d5d86d0396e83d7cac883a4857
+size 668962
diff --git a/projects/voxelization/resources/Sponza/Textures/VaseRound_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/VaseRound_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..74e6d243f6da2015398ef9a08de663c59f32c329
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/VaseRound_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:430758549ba2189d94f06abeaae092bfc0a1d8c22afc56f5caf3f99c5ee7407c
+size 572245
diff --git a/projects/voxelization/resources/Sponza/Textures/VaseRound_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/VaseRound_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..154b1fa17729990b54b368febb55a259b7a47292
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/VaseRound_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8af8fb0810bf3493242491da44f7ae14f7c0bfa6cbacea4be01f6f4261db4559
+size 618837
diff --git a/projects/voxelization/resources/Sponza/Textures/VaseRound_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/VaseRound_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..97c19f41117e9a3aaec02885b236349c8ba9c6ca
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/VaseRound_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12e1f2ea3c2714a6ec455fa8e623a702e33ef95765ebd09f7874a68fa55fd104
+size 399963
diff --git a/projects/voxelization/resources/Sponza/Textures/Vase_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Vase_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..4d53ec1a796184e24893aff99f53652d08bd4f47
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Vase_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1da7664b7b88ae58386a6e6b5eca2829537818061d704f002356d8fc483c1a28
+size 531156
diff --git a/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Diff.jpg b/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Diff.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..149a0f1c47e15ebfc2a9de474e8dda46ed3ab2de
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Diff.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f849f0510ca0df66ba1cba2380a6a1b481444d9e64b68da6b2ce202b54d9cec
+size 302363
diff --git a/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..bcdd65e4b718c0dc7e74f03aa50cac22f10febea
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b93291121303b5a24a488a3e29e0645a24bb471413cfd25c0917d306dff1d038
+size 208352
diff --git a/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Spec.jpg b/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8976a203dfe463d121d48e743b3ecb662ac52d09
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Vase_Hanging_Spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c288a37ed8373d1132644873624b096a72ba81d03cda601e050a5de24ba13c39
+size 104626
diff --git a/projects/voxelization/resources/Sponza/Textures/Vase_Norm.jpg b/projects/voxelization/resources/Sponza/Textures/Vase_Norm.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..de5758090ab8ab0aaa374110aa08df6c7f53d599
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Vase_Norm.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:728dd0fe3b570103e51dbfcc1eac64ad09916a7a31603c62a4090a2f2afaa6f8
+size 876227
diff --git a/projects/voxelization/resources/Sponza/Textures/Vase_spec.jpg b/projects/voxelization/resources/Sponza/Textures/Vase_spec.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..0cce3ebf638a3ca932bca8b28c31b8474f123349
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/Vase_spec.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb09708202859d08764601acc9c9ea3b5254b2c4fd39dcc6b6055768c77aee9b
+size 370061
diff --git a/projects/voxelization/resources/Sponza/Textures/white.png b/projects/voxelization/resources/Sponza/Textures/white.png
new file mode 100644
index 0000000000000000000000000000000000000000..98e867371da9926f451d5754604bc97b3186296a
--- /dev/null
+++ b/projects/voxelization/resources/Sponza/Textures/white.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e2ab2939dda535ad28779e41c20c98368f630f29c5824a0a5a430f47b3b6da12
+size 951
diff --git a/projects/voxelization/resources/cube/boards2_vcyc_jpg.jpg b/projects/voxelization/resources/cube/boards2_vcyc_jpg.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..2636039e272289c0fba3fa2d88a060b857501248
--- /dev/null
+++ b/projects/voxelization/resources/cube/boards2_vcyc_jpg.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cca33a6e58ddd1b37a6e6853a9aa0e7b15ca678937119194752393dd2a0a0564
+size 1192476
diff --git a/projects/cmd_sync_test/resources/cube/cube.bin b/projects/voxelization/resources/cube/cube.bin
similarity index 100%
rename from projects/cmd_sync_test/resources/cube/cube.bin
rename to projects/voxelization/resources/cube/cube.bin
diff --git a/projects/cmd_sync_test/resources/cube/cube.blend b/projects/voxelization/resources/cube/cube.blend
similarity index 100%
rename from projects/cmd_sync_test/resources/cube/cube.blend
rename to projects/voxelization/resources/cube/cube.blend
diff --git a/projects/cmd_sync_test/resources/cube/cube.blend1 b/projects/voxelization/resources/cube/cube.blend1
similarity index 100%
rename from projects/cmd_sync_test/resources/cube/cube.blend1
rename to projects/voxelization/resources/cube/cube.blend1
diff --git a/projects/cmd_sync_test/resources/cube/cube.glb b/projects/voxelization/resources/cube/cube.glb
similarity index 100%
rename from projects/cmd_sync_test/resources/cube/cube.glb
rename to projects/voxelization/resources/cube/cube.glb
diff --git a/projects/cmd_sync_test/resources/cube/cube.gltf b/projects/voxelization/resources/cube/cube.gltf
similarity index 100%
rename from projects/cmd_sync_test/resources/cube/cube.gltf
rename to projects/voxelization/resources/cube/cube.gltf
diff --git a/projects/voxelization/resources/lensDirt.jpg b/projects/voxelization/resources/lensDirt.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f941567527fe92f23aa6955e15ba95dc46881dad
--- /dev/null
+++ b/projects/voxelization/resources/lensDirt.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:95982351ecf3d4d129d17612b40a8092944a285c0bffdd74d4886dd8489695ca
+size 107603
diff --git a/projects/voxelization/resources/shaders/bloomDownsample.comp b/projects/voxelization/resources/shaders/bloomDownsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2ab00c7c92798769153634f3479c5b7f3fb61d94
--- /dev/null
+++ b/projects/voxelization/resources/shaders/bloomDownsample.comp
@@ -0,0 +1,76 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inBlurImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+    vec2  UV_offset     = UV + 0.5f * pixel_size;
+
+    vec2 color_fetches[13] = {
+        // center neighbourhood (RED)
+        vec2(-1,  1), // LT
+        vec2(-1, -1), // LB
+        vec2( 1, -1), // RB
+        vec2( 1,  1), // RT
+
+        vec2(-2, 2), // LT
+        vec2( 0, 2), // CT
+        vec2( 2, 2), // RT
+
+        vec2(0 ,-2), // LC
+        vec2(0 , 0), // CC
+        vec2(2,  0), // CR
+
+        vec2(-2, -2), // LB
+        vec2(0 , -2), // CB
+        vec2(2 , -2)  // RB
+    };
+
+    float color_weights[13] = {
+        // 0.5f
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f,
+
+        // 0.25f
+        1.f/16.f,
+        1.f/8.f,
+        1.f/16.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f
+    };
+
+    vec3 sampled_color = vec3(0.0f);
+
+    for(uint i = 0; i < 13; i++)
+    {
+        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
+        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
+        color *= color_weights[i];
+        sampled_color += color;
+    }
+
+    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/bloomFlaresComposite.comp b/projects/voxelization/resources/shaders/bloomFlaresComposite.comp
new file mode 100644
index 0000000000000000000000000000000000000000..57174b73ae3b58023d01defd26f636e13cb4709c
--- /dev/null
+++ b/projects/voxelization/resources/shaders/bloomFlaresComposite.comp
@@ -0,0 +1,89 @@
+#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(set=0, binding=4) uniform texture2D                          radialLUT;
+layout(set=0, binding=5) uniform sampler                            radialLUTSampler;
+layout(set=0, binding=6) uniform texture2D                          dirtTexture;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    vec3 cameraForward;
+};
+
+float starburst(vec2 uv){
+    vec2 toCenter   = vec2(0.5) - uv;
+    float d2        = dot(toCenter, toCenter);
+    float falloff   = clamp(pow(d2 * 2, 2.5), 0, 1);
+    
+    float cosTheta  = acos(normalize(toCenter).x) * sign(toCenter.y);
+    cosTheta        *= 4;
+    
+    float thetaOffset   = cameraForward.x + cameraForward.y;
+    thetaOffset         *= 10;
+    cosTheta            += thetaOffset;
+    
+    float burst     = texture(sampler2D(radialLUT, radialLUTSampler), vec2(cosTheta, 0.5)).r;
+    burst           = pow(burst, 2);
+    return mix(1, burst, falloff);
+}
+
+float getLensDirtWeight(vec2 uv){
+    vec2    targetTextureRes    = imageSize(colorBuffer);
+    float   targetAspectRatio   = targetTextureRes.x / targetTextureRes.y;
+    
+    vec2    dirtTextureRes    = textureSize(sampler2D(dirtTexture, linearSampler), 0);
+    float   dirtAspectRatio   = dirtTextureRes.x / dirtTextureRes.y;
+    
+    uv.x                        *= targetAspectRatio / dirtAspectRatio;
+    float   dirt                = texture(sampler2D(dirtTexture, radialLUTSampler), uv).r;
+    float   dirtStrength        = 0.4f;
+    
+    // manually looked up in gimp, must be adjusted when changing dirt texture
+    float dirtMean = 0.132;
+    // make sure no energy is lost
+    // otherwise bloom is darkened when the dirt increases
+    dirt /= dirtMean;   
+    
+    return mix(1, dirt, dirtStrength);
+}
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(colorBuffer);
+    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.06f;
+    float lens_weight  = 0.02f;
+    float main_weight = 1 - (bloom_weight + lens_weight);
+
+    lens_color *= starburst(UV);
+    
+    float lensDirtWeight = getLensDirtWeight(UV);
+    bloom_weight        *= lensDirtWeight;
+    lens_weight         *= lensDirtWeight;
+    
+    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/resources/shaders/bloomUpsample.comp b/projects/voxelization/resources/shaders/bloomUpsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..0ddeedb5b5af9e476dc19012fed6430544006c0e
--- /dev/null
+++ b/projects/voxelization/resources/shaders/bloomUpsample.comp
@@ -0,0 +1,45 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
+        return;
+    }
+
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
+    const float gauss_weight = 16.f;
+
+    vec3 sampled_color = vec3(0.f);
+
+    for(int i = -1; i <= 1; i++)
+    {
+        for(int j = -1; j <= 1; j++)
+        {
+            vec2 sample_location = UV + vec2(j, i) * pixel_size;
+            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
+            color *= gauss_kernel[j+1];
+            color *= gauss_kernel[i+1];
+            color /= gauss_weight;
+
+            sampled_color += color;
+        }
+    }
+
+    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
+    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
+    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
+
+    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/brdf.inc b/projects/voxelization/resources/shaders/brdf.inc
new file mode 100644
index 0000000000000000000000000000000000000000..4cf334eaceedd18815ab928aed38d5f8d3f51c1e
--- /dev/null
+++ b/projects/voxelization/resources/shaders/brdf.inc
@@ -0,0 +1,31 @@
+#ifndef BRDF_INC
+#define BRDF_INC
+
+const float pi = 3.1415; 
+
+vec3 lambertBRDF(vec3 albedo){
+    return albedo / pi;
+}
+
+vec3 fresnelSchlick(float cosTheta, vec3 f0){
+    return f0 + (vec3(1) - f0) * pow(1 - cosTheta, 5);
+}
+
+float GGXDistribution(float r, float NoH){
+    float r2    = r * r;
+    float denom = pi * pow(NoH * NoH * (r2 - 1) + 1, 2);
+    return r2 / max(denom, 0.00001);
+}
+
+float GGXSmithShadowingPart(float r, float cosTheta){
+    float nom   = cosTheta * 2;
+    float r2    = r * r;
+    float denom = cosTheta + sqrt(r2 + (1 - r2) * cosTheta * cosTheta);
+    return nom / max(denom, 0.00001);
+}
+
+float GGXSmithShadowing(float r, float NoV, float NoL){
+    return GGXSmithShadowingPart(r, NoV) * GGXSmithShadowingPart(r, NoL);
+}
+
+#endif // #ifndef BRDF_INC
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/depthPrepass.frag b/projects/voxelization/resources/shaders/depthPrepass.frag
new file mode 100644
index 0000000000000000000000000000000000000000..5e2f7a092ca300af40cc039608a44d080c28730f
--- /dev/null
+++ b/projects/voxelization/resources/shaders/depthPrepass.frag
@@ -0,0 +1,20 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "perMeshResources.inc"
+
+layout(location = 0) in vec2 passUV;
+
+layout(location = 0) out vec4 outColor; // only used for alpha to coverage, not actually written to
+
+// coverage to alpha techniques explained in: https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f
+void main()	{
+    float alpha         = texture(sampler2D(albedoTexture, textureSampler), passUV).a;
+    float alphaCutoff   = 0.5;
+    
+    // scale alpha to one pixel width
+    alpha               = (alpha - alphaCutoff) / max(fwidth(alpha), 0.0001) + 0.5;
+    
+    outColor.a          = alpha;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/depthPrepass.vert b/projects/voxelization/resources/shaders/depthPrepass.vert
new file mode 100644
index 0000000000000000000000000000000000000000..4bb3500eb59214e30fce84862e181fd7e24b7340
--- /dev/null
+++ b/projects/voxelization/resources/shaders/depthPrepass.vert
@@ -0,0 +1,18 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+#extension GL_GOOGLE_include_directive : enable
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 2) in vec2 inUV;
+
+layout(location = 0) out vec2 passUV;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+    passUV = inUV;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/depthToMoments.comp b/projects/voxelization/resources/shaders/depthToMoments.comp
new file mode 100644
index 0000000000000000000000000000000000000000..5a78d0cb9b748187d12057708fcd0de7658a61ed
--- /dev/null
+++ b/projects/voxelization/resources/shaders/depthToMoments.comp
@@ -0,0 +1,36 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_texture_multisample : enable
+
+#include "shadowMapping.inc"
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout(set=0, binding=0)                    uniform texture2DMS srcTexture;
+layout(set=0, binding=1)                    uniform sampler     depthSampler;                
+layout(set=0, binding=2, rgba16)            uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    int msaaCount;
+};
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
+        return;
+    }
+    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
+
+    float z = 0;
+    for(int i = 0; i < msaaCount; i++){
+        z += texelFetch(sampler2DMS(srcTexture, depthSampler), uv, i).r;
+    }
+    z /= msaaCount;
+    
+    float   z2                  = z*z;   
+    vec4    moments             = vec4(z, z2, z2*z, z2*z2);
+    vec4    momentsQuantized    = quantizeMoments(moments);
+    imageStore(outImage, uv, momentsQuantized);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/lensFlares.comp b/projects/voxelization/resources/shaders/lensFlares.comp
new file mode 100644
index 0000000000000000000000000000000000000000..afcad375c1cd3e8f547ad2386b6f1d7bdfdfa85a
--- /dev/null
+++ b/projects/voxelization/resources/shaders/lensFlares.comp
@@ -0,0 +1,106 @@
+#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.15;
+    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 = 8;
+    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)
+{
+    float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
+    c_aspect_ratio *= 0.55;
+    const float c_radius = 0.5f;
+    const float c_halo_thickness = 0.15f;
+
+    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 = pow(clamp(halo_weight + 0.1, 0, 1), 2);
+    }
+
+    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
+}
+
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    vec2 flipped_UV = vec2(1.0f) - UV;
+
+    vec3 color = vec3(0.0f);
+
+    color += ghost_vectors(flipped_UV);
+    color += halo(UV);
+    color  *= 0.5f;
+
+    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/lightInfo.inc b/projects/voxelization/resources/shaders/lightInfo.inc
new file mode 100644
index 0000000000000000000000000000000000000000..a87f9ce7bebc1db1688dd20dd80608e99925755a
--- /dev/null
+++ b/projects/voxelization/resources/shaders/lightInfo.inc
@@ -0,0 +1,12 @@
+#ifndef LIGHT_INFO_INC
+#define LIGHT_INFO_INC
+
+struct LightInfo{
+    vec3    L;             
+    float   padding;
+    vec3    sunColor;      
+    float   sunStrength;
+    mat4    lightMatrix;
+};
+
+#endif // #ifndef LIGHT_INFO_INC
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/luma.inc b/projects/voxelization/resources/shaders/luma.inc
new file mode 100644
index 0000000000000000000000000000000000000000..17b3b282830ab155ce62e9b1394c0985ceccecd9
--- /dev/null
+++ b/projects/voxelization/resources/shaders/luma.inc
@@ -0,0 +1,8 @@
+#ifndef LUMA_INC
+#define LUMA_INC
+
+float computeLuma(vec3 c){
+    return dot(c, vec3(0.21, 0.72, 0.07));
+}
+
+#endif // #ifndef LUMA_INC
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/msaa4XResolve.comp b/projects/voxelization/resources/shaders/msaa4XResolve.comp
new file mode 100644
index 0000000000000000000000000000000000000000..8bb1a946e3ba43f4e80f21f6bd730e020276f2d8
--- /dev/null
+++ b/projects/voxelization/resources/shaders/msaa4XResolve.comp
@@ -0,0 +1,83 @@
+#version 450
+#extension GL_ARB_texture_multisample : enable
+#extension GL_GOOGLE_include_directive : enable
+
+layout(set=0, binding=0)                    uniform texture2DMS     srcTexture;
+layout(set=0, binding=1)                    uniform sampler         MSAASampler;                
+layout(set=0, binding=2, r11f_g11f_b10f)    uniform image2D         outImage;
+
+#include "luma.inc"
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+vec3 tonemap(vec3 c){
+    return c / (1 + computeLuma(c));
+}
+
+vec3 tonemapReverse(vec3 c){
+    return c / (1 - computeLuma(c));
+}
+
+float reconstructionFilter(float d){
+    // gauß filter, tuned so that distance of one has weight around 20%
+    float a = 1.6; 
+    return exp(-a * d*d);
+}
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
+        return;
+    }
+    ivec2 uv            = ivec2(gl_GlobalInvocationID.xy);
+    
+    vec2 samplePositions[4] = {
+     vec2(0.375, 0.125),
+     vec2(0.875, 0.375),
+     vec2(0.125, 0.625),
+     vec2(0.625, 0.875)};
+    
+    vec3    color   = vec3(0);
+    float   wTotal  = 0;
+    
+    // four samples from main pixel
+    for(int i = 0; i < 4; i++){
+        vec3 msaaSample = texelFetch(sampler2DMS(srcTexture, MSAASampler), uv, i).rgb;
+        float d         = distance(vec2(0.5), samplePositions[i]);
+        float w         = reconstructionFilter(d);
+        color           += tonemap(msaaSample) * w;
+        wTotal          += w;
+    }
+    
+    ivec2 neighbourOffsets[4] = {
+        ivec2( 1,  0),   // right
+        ivec2(-1,  0),   // left
+        ivec2( 0,  1),   // top
+        ivec2( 0, -1)    // bot
+    };
+    
+    int neighbourSampleIndices[8] = {
+        0, 2, // left  samples of right neighbour
+        1, 3, // right samples of left  neighbour
+        2, 3, // bot   samples of top   neighbour
+        0, 1  // top   samples of bot   neighbour
+    };
+    
+    // two additional samples from each neighbour
+    for(int neighbour = 0; neighbour < 4; neighbour++){
+        for(int i = 0; i < 2; i++){
+            int     sampleIndex = neighbourSampleIndices[neighbour * 2 + i];
+            ivec2   pixelOffset = neighbourOffsets[neighbour];
+            ivec2   pixelUV     = uv + pixelOffset;
+            vec3    msaaSample  = texelFetch(sampler2DMS(srcTexture, MSAASampler), pixelUV, sampleIndex).rgb;
+            float   d           = distance(vec2(0.5), samplePositions[sampleIndex] + pixelOffset);
+            float   w           = reconstructionFilter(d);
+            color               += tonemap(msaaSample) * w;
+            wTotal              += w;
+        }
+    }    
+    color /= wTotal;
+    color = tonemapReverse(color);
+    
+    imageStore(outImage, uv, vec4(color, 0.f));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/perMeshResources.inc b/projects/voxelization/resources/shaders/perMeshResources.inc
new file mode 100644
index 0000000000000000000000000000000000000000..b1523713cf2040f672f74be3f47f9bf43996c614
--- /dev/null
+++ b/projects/voxelization/resources/shaders/perMeshResources.inc
@@ -0,0 +1,4 @@
+layout(set=1, binding=0) uniform texture2D  albedoTexture;
+layout(set=1, binding=1) uniform sampler    textureSampler;
+layout(set=1, binding=2) uniform texture2D  normalTexture;
+layout(set=1, binding=3) uniform texture2D  specularTexture;
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shader.frag b/projects/voxelization/resources/shaders/shader.frag
new file mode 100644
index 0000000000000000000000000000000000000000..25ec69acb77bace1134920bbcee56deb40bb936b
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shader.frag
@@ -0,0 +1,170 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "perMeshResources.inc"
+#include "lightInfo.inc"
+#include "shadowMapping.inc"
+#include "brdf.inc"
+#include "voxel.inc"
+
+layout(location = 0) in vec3 passNormal;
+layout(location = 1) in vec2 passUV;
+layout(location = 2) in vec3 passPos;
+layout(location = 3) in vec4 passTangent;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=0) uniform sunBuffer {
+    LightInfo lightInfo;
+};
+layout(set=0, binding=1) uniform texture2D  shadowMap;
+layout(set=0, binding=2) uniform sampler    shadowMapSampler;
+
+layout(set=0, binding=3) uniform cameraBuffer {
+    vec3 cameraPos;
+};
+
+layout(set=0, binding=4) uniform texture3D  voxelTexture;
+layout(set=0, binding=5) uniform sampler    voxelSampler;
+
+layout(set=0, binding=6) uniform VoxelInfoBuffer{
+    VoxelInfo voxelInfo;
+};
+
+layout(set=0, binding=7) uniform VolumetricSettings {
+    vec3    scatteringCoefficient;
+    float   volumetricAmbientLight;
+    vec3    absorptionCoefficient;
+};
+
+
+vec3 cookTorrance(vec3 f0, float r, vec3 N, vec3 V, vec3 L){
+    
+    vec3 H  = normalize(L + V);
+    
+    float NoH = clamp(dot(N, H), 0, 1);
+    float NoL = clamp(dot(N, L), 0, 1);
+    float NoV = clamp(abs(dot(N, V)), 0, 1);    // abs to account for wrong visibility caused by normal mapping
+    
+    vec3    F = fresnelSchlick(NoH, f0);
+    float   D = GGXDistribution(r, NoH);
+    float   G = GGXSmithShadowing(r, NoV, NoL);
+    
+    return (F * D * G) / max(4 * NoV * NoL, 0.00001);
+}
+
+float roughnessToConeAngleDegree(float r){
+    return mix(degreeToRadian(3), degreeToRadian(60), r);
+}
+
+// from: "Next Generation Post Processing in Call Of Duty Advanced Warfare" slide page 123
+float interleavedGradientNoise(vec2 uv){
+    vec3 magic = vec3(0.06711056, 0.00583715, 62.9829189);
+    return fract(magic.z * fract(dot(uv, magic.xy)));
+}
+
+// from: https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
+vec3 EnvBRDFApprox(vec3 SpecularColor, float Roughness, float NoV )
+{
+	const vec4 c0 = { -1, -0.0275, -0.572, 0.022 };
+	const vec4 c1 = { 1, 0.0425, 1.04, -0.04 };
+	vec4 r = Roughness * c0 + c1;
+	float a004 = min( r.x * r.x, exp2( -9.28 * NoV ) ) * r.x + r.y;
+	vec2 AB = vec2( -1.04, 1.04 ) * a004 + r.zw;
+	return SpecularColor * AB.x + AB.y;
+}
+
+float isotropicPhase(){
+    return 1 / (4 * pi);
+}
+
+vec3 volumetricLighting(vec3 colorIn, vec3 V, vec3 pos, float d){
+    vec3 color      = colorIn;
+    
+    int sampleCount = 20;
+    float stepSize  = d / sampleCount;
+    
+    vec3 extinctionCoefficient = scatteringCoefficient + absorptionCoefficient;
+    
+    float   noise           = 2 * pi * interleavedGradientNoise(gl_FragCoord.xy);
+    vec2    shadowOffset    = 3.f * vec2(sin(noise), cos(noise)) / textureSize(sampler2D(shadowMap, shadowMapSampler), 0);
+    
+    float noiseScale    = 0.1f;
+    pos                 += V * noiseScale * interleavedGradientNoise(gl_FragCoord.xy);
+    
+    for(int i = 0; i < sampleCount; i++){
+        vec3    samplePoint = pos + V * i * stepSize;
+        float   phase       = isotropicPhase();
+        vec3    light       = lightInfo.sunColor * lightInfo.sunStrength;
+        float   shadow      = shadowTest(samplePoint, lightInfo, shadowMap, shadowMapSampler, shadowOffset);
+        light               *= shadow;
+        light               += volumetricAmbientLight;
+        
+        color               += phase * light * scatteringCoefficient * stepSize;
+        color               *= exp(-stepSize * extinctionCoefficient);
+    }
+    return color;
+}
+
+void main()	{
+
+    vec3 albedoTexel    = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
+    vec3 normalTexel    = texture(sampler2D(normalTexture, textureSampler), passUV).rgb;
+    vec3 specularTexel  = texture(sampler2D(specularTexture, textureSampler), passUV).rgb;
+    
+    float r             = specularTexel.g;
+    
+    float metal         = specularTexel.b;
+    vec3 albedo         = mix(albedoTexel, vec3(0), metal);
+    vec3 f0_dielectric  = vec3(0.04f);
+    vec3 f0             = mix(f0_dielectric, albedoTexel, metal);
+    
+    vec3 T      = normalize(passTangent.xyz);
+    vec3 N_geo  = normalize(passNormal);
+    vec3 B      = cross(N_geo, T) * passTangent.w;
+    mat3 TBN    = mat3(T, B, N_geo);
+    normalTexel = normalTexel * 2 - 1;
+
+    vec3 N  = normalize(TBN * normalTexel);
+    vec3 L  = lightInfo.L;
+    vec3 V  = normalize(cameraPos - passPos);
+    
+    float NoL = clamp(dot(N, L), 0, 1);    
+    float NoV = clamp(abs(dot(N, V)), 0, 1);
+    
+    vec3 sunSpecular    = cookTorrance(f0, r, N, V, L);
+    vec3 sun            = lightInfo.sunStrength * lightInfo.sunColor * NoL;
+    
+    float   noise           = 2 * pi * interleavedGradientNoise(gl_FragCoord.xy);
+    vec2    shadowOffset    = 0.05f * vec2(sin(noise), cos(noise)) / textureSize(sampler2D(shadowMap, shadowMapSampler), 0);
+    float   shadow          = shadowTest(passPos, lightInfo, shadowMap, shadowMapSampler, shadowOffset);
+    sun                     *= shadow;
+    
+    vec3 F_in       = fresnelSchlick(NoL, f0);
+    vec3 F_out      = fresnelSchlick(NoV, f0);
+    vec3 diffuse    = lambertBRDF(albedo) * (1 - F_in) * (1 - F_out);
+    
+    vec3 up         = abs(N_geo.y) >= 0.99 ? vec3(1, 0, 0) : vec3(0, 1, 0);
+    vec3 right      = normalize(cross(up, N));
+    up              = cross(N, right); 
+    mat3 toSurface  = mat3(right, up, N);
+    
+    vec3 diffuseTrace = diffuseVoxelTraceHemisphere(toSurface, passPos, voxelTexture, voxelSampler, voxelInfo);
+    
+    vec3 R                      = reflect(-V, N);
+    float reflectionConeAngle   = roughnessToConeAngleDegree(r);
+    vec3 offsetTraceStart       = passPos + N_geo * 0.1f;
+    offsetTraceStart            += R * interleavedGradientNoise(gl_FragCoord.xy) * 0.5;
+    vec3 specularTrace          = voxelConeTrace(R, offsetTraceStart, reflectionConeAngle, voxelTexture, voxelSampler, voxelInfo);
+    specularTrace               *= clamp(dot(N, R), 0, 1);
+    vec3 reflectionBRDF         = EnvBRDFApprox(f0, r, NoV); 
+    
+	outColor = 
+        (diffuse + sunSpecular) * sun + 
+        lambertBRDF(albedo) * diffuseTrace + 
+        reflectionBRDF * specularTrace;
+        
+    float d     = distance(cameraPos, passPos);
+    outColor    = volumetricLighting(outColor, V, passPos, d);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shader.vert b/projects/voxelization/resources/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..e3873f98a308347592725e794d6b7102cbbe3e5c
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shader.vert
@@ -0,0 +1,25 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+layout(location = 2) in vec2 inUV;
+layout(location = 3) in vec4 inTangent;
+
+layout(location = 0) out vec3 passNormal;
+layout(location = 1) out vec2 passUV;
+layout(location = 2) out vec3 passPos;
+layout(location = 3) out vec4 passTangent;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    mat4 model;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+	passNormal  = mat3(model) * inNormal;    // assuming no weird stuff like shearing or non-uniform scaling
+    passUV      = inUV;
+    passPos     = (model * vec4(inPosition, 1)).xyz;
+    passTangent = vec4(mat3(model) * inTangent.xyz, inTangent.w);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shadow.frag b/projects/voxelization/resources/shaders/shadow.frag
new file mode 100644
index 0000000000000000000000000000000000000000..65592d2cfe161b8522de1a0c3e68fa1d6afa80be
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shadow.frag
@@ -0,0 +1,7 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+void main()	{
+
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shadow.vert b/projects/voxelization/resources/shaders/shadow.vert
new file mode 100644
index 0000000000000000000000000000000000000000..d800c547368c4f2126c880534276a3be3cf336f5
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shadow.vert
@@ -0,0 +1,14 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+#extension GL_GOOGLE_include_directive : enable
+
+layout(location = 0) in vec3 inPosition;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shadowBlur.inc b/projects/voxelization/resources/shaders/shadowBlur.inc
new file mode 100644
index 0000000000000000000000000000000000000000..06147415f118dca9badd15813b431a68682ce0b0
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shadowBlur.inc
@@ -0,0 +1,27 @@
+#ifndef SHADOW_BLUR_INC
+#define SHADOW_BLUR_INC
+
+vec4 blurMomentShadowMap1D(ivec2 coord, ivec2 blurDirection, texture2D srcTexture, sampler depthSampler){
+    
+    int blurRadius  = 9;
+    int minOffset   = -(blurRadius-1) / 2;
+    int maxOffset   = -minOffset;
+    
+    vec2 pixelSize = vec2(1) / textureSize(sampler2D(srcTexture, depthSampler), 0);
+    
+    float wTotal = 0;
+    vec4 moments = vec4(0);
+    
+    float weights1D[4] = { 0.5, 0.25, 0.125, 0.0625 };    // gaussian
+    
+    for(int i = minOffset; i <= maxOffset; i++){
+        vec2 uv = (coord + i * blurDirection) * pixelSize;
+        uv      += 0.5 * pixelSize * blurDirection * sign(i); // half pixel shift to take advantage of bilinear filtering
+        float w = weights1D[abs(i)];
+        moments += w * texture(sampler2D(srcTexture, depthSampler), uv);
+        wTotal  += w;
+    }
+    return moments / wTotal;
+}
+
+#endif // #ifndef SHADOW_BLUR_INC
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shadowBlurX.comp b/projects/voxelization/resources/shaders/shadowBlurX.comp
new file mode 100644
index 0000000000000000000000000000000000000000..45b91aad71673347dbf607fecef92463ef1c3c88
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shadowBlurX.comp
@@ -0,0 +1,23 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+#include "shadowBlur.inc"
+
+layout(set=0, binding=0)            uniform texture2D   srcTexture;
+layout(set=0, binding=1)            uniform sampler     depthSampler;                
+layout(set=0, binding=2, rgba16)    uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
+        return;
+    }
+    ivec2 coord = ivec2(gl_GlobalInvocationID.xy);    
+    vec4 moments = blurMomentShadowMap1D(coord, ivec2(1, 0), srcTexture, depthSampler);
+    
+    imageStore(outImage, coord, moments);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shadowBlurY.comp b/projects/voxelization/resources/shaders/shadowBlurY.comp
new file mode 100644
index 0000000000000000000000000000000000000000..51d4df054b0d99e54149863a5967143518f61dd2
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shadowBlurY.comp
@@ -0,0 +1,25 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+#include "shadowBlur.inc"
+
+layout(set=0, binding=0)            uniform texture2D   srcTexture;
+layout(set=0, binding=1)            uniform sampler     depthSampler;                
+layout(set=0, binding=2, rgba16)    uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
+        return;
+    }
+    ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
+    vec2 pixelSize = vec2(1) / textureSize(sampler2D(srcTexture, depthSampler), 0);
+    
+    vec4 moments = blurMomentShadowMap1D(coord, ivec2(0, 1), srcTexture, depthSampler);
+    
+    imageStore(outImage, coord, moments);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shadowMapping.inc b/projects/voxelization/resources/shaders/shadowMapping.inc
new file mode 100644
index 0000000000000000000000000000000000000000..c56ae8985c5c5fcef780b622d8b888f1081af74c
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shadowMapping.inc
@@ -0,0 +1,95 @@
+#ifndef SHADOW_MAPPING_INC
+#define SHADOW_MAPPING_INC
+
+#include "lightInfo.inc"
+
+// nice math blob from the moment shadow mapping presentation
+float ComputeMSMShadowIntensity(vec4 _4Moments, float FragmentDepth, float DepthBias, float MomentBias)
+{
+    vec4 b=mix(_4Moments, vec4(0.5),MomentBias);
+    vec3 z;
+    z[0]=FragmentDepth-DepthBias;
+    float L32D22=fma(-b[0], b[1], b[2]);
+    float D22=fma(-b[0], b[0], b[1]);
+    float SquaredDepthVariance=fma(-b[1], b[1], b[3]);
+    float D33D22=dot(vec2(SquaredDepthVariance,-L32D22),
+                     vec2(D22,                  L32D22));
+                     
+    float InvD22=1.0/D22;
+    float L32=L32D22*InvD22;
+    vec3 c=vec3(1.0,z[0],z[0]*z[0]);
+    c[1]-=b.x;
+    c[2]-=b.y+L32*c[1];
+    c[1]*=InvD22;
+    c[2]*=D22/D33D22;
+    c[1]-=L32*c[2];
+    c[0]-=dot(c.yz,b.xy);
+    float p=c[1]/c[2];
+    float q=c[0]/c[2];
+    float r=sqrt((p*p*0.25)-q);
+    z[1]=-p*0.5-r;
+    z[2]=-p*0.5+r;
+    vec4 Switch=
+    	(z[2]<z[0])?vec4(z[1],z[0],1.0,1.0):(
+    	(z[1]<z[0])?vec4(z[0],z[1],0.0,1.0):
+    	vec4(0.0));
+    float Quotient=(Switch[0]*z[2]-b[0]*(Switch[0]+z[2])+b[1])
+                  /((z[2]-Switch[1])*(z[0]-z[1]));
+    return 1-clamp(Switch[2]+Switch[3]*Quotient, 0, 1);
+}
+
+vec4 quantizeMoments(vec4 moments){
+    mat4 T = mat4(
+        -2.07224649,     13.7948857237,   0.105877704,   9.7924062118,
+         32.23703778,   -59.4683975703, -1.9077466311, -33.7652110555,
+        -68.571074599,   82.0359750338,  9.3496555107,  47.9456096605,
+         39.3703274134, -35.364903257,  -6.6543490743, -23.9728048165);
+    vec4 quantized = T * moments;
+    quantized[0] += 0.0359558848;
+    return quantized;
+}
+
+vec4 unquantizeMoments(vec4 moments){
+    moments[0] -= 0.0359558848;
+    mat4 T = mat4(
+        0.2227744146,  0.1549679261,  0.1451988946,  0.163127443,
+        0.0771972861,  0.1394629426,  0.2120202157,  0.2591432266,
+        0.7926986636,  0.7963415838,  0.7258694464,  0.6539092497,
+        0.0319417555,  -0.1722823173, -0.2758014811, -0.3376131734);
+    return T * moments;
+}
+
+float rescaleRange(float a, float b, float v)
+{
+    return clamp((v - a) / (b - a), 0, 1);
+}
+
+float reduceLightBleeding(float shadow, float amount)
+{
+   return rescaleRange(amount, 1.0f, shadow);
+}
+
+float shadowTest(vec3 worldPos, LightInfo lightInfo, texture2D shadowMap, sampler shadowMapSampler, vec2 offset){
+    vec4 lightPos   = lightInfo.lightMatrix * vec4(worldPos, 1);
+    lightPos        /= lightPos.w;
+    lightPos.xy     = lightPos.xy * 0.5 + 0.5;
+    lightPos.xy     += offset;
+    
+    if(any(lessThan(lightPos.xy, vec2(0))) || any(greaterThan(lightPos.xy, vec2(1)))){
+        return 1;
+    }
+    
+    lightPos.z = clamp(lightPos.z, 0, 1);
+
+    vec4 shadowMapSample = texture(sampler2D(shadowMap, shadowMapSampler), lightPos.xy);
+    
+    shadowMapSample = unquantizeMoments(shadowMapSample);
+    
+    float depthBias     = 0.f;
+    float momentBias    = 0.0003;
+    
+    float shadow = ComputeMSMShadowIntensity(shadowMapSample, lightPos.z, depthBias, momentBias);
+    return reduceLightBleeding(shadow, 0.1f);
+}
+
+#endif // #ifndef SHADOW_MAPPING_INC
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/sky.frag b/projects/voxelization/resources/shaders/sky.frag
new file mode 100644
index 0000000000000000000000000000000000000000..2a3b2ad03e1936641a565b2f3fbd1f19f186ff7a
--- /dev/null
+++ b/projects/voxelization/resources/shaders/sky.frag
@@ -0,0 +1,13 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) out vec3 outColor;
+
+layout( push_constant ) uniform constants{
+    vec3 skyColor;
+    float skyStrength;
+};
+
+void main()	{
+    outColor = skyColor * skyStrength;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/sky.vert b/projects/voxelization/resources/shaders/sky.vert
new file mode 100644
index 0000000000000000000000000000000000000000..686e6f352e9bb1054656f58340a9cfc9b55fcff4
--- /dev/null
+++ b/projects/voxelization/resources/shaders/sky.vert
@@ -0,0 +1,12 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+const vec2 positions[3] = {
+    vec2(-1, -1),
+    vec2(-1, 4),
+    vec2(4, -1)
+};
+
+void main()	{
+	gl_Position = vec4(positions[gl_VertexIndex], 1, 1);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/tonemapping.comp b/projects/voxelization/resources/shaders/tonemapping.comp
new file mode 100644
index 0000000000000000000000000000000000000000..8fa07d39ebb56eab857cdccb755a6558f5ae1ec3
--- /dev/null
+++ b/projects/voxelization/resources/shaders/tonemapping.comp
@@ -0,0 +1,149 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "luma.inc"
+
+layout(set=0, binding=0)        uniform texture2D   inTexture;
+layout(set=0, binding=1)        uniform sampler     textureSampler;
+layout(set=0, binding=2, rgba8) uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    float time;
+};
+
+// from: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
+vec3 ACESFilm(vec3 x)
+{
+    float a = 2.51f;
+    float b = 0.03f;
+    float c = 2.43f;
+    float d = 0.59f;
+    float e = 0.14f;
+    return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0, 1);
+}
+
+// From Dave Hoskins: https://www.shadertoy.com/view/4djSRW.
+float hash(vec3 p3){
+    p3 = fract(p3 * 0.1031);
+    p3 += dot(p3,p3.yzx + 19.19);
+    return fract((p3.x + p3.y) * p3.z);
+}
+
+// From iq: https://www.shadertoy.com/view/4sfGzS.
+float noise(vec3 x){
+    vec3 i = floor(x);
+    vec3 f = fract(x);
+    f = f*f*(3.0-2.0*f);
+    return mix(mix(mix(hash(i+vec3(0, 0, 0)), 
+                       hash(i+vec3(1, 0, 0)),f.x),
+                   mix(hash(i+vec3(0, 1, 0)), 
+                       hash(i+vec3(1, 1, 0)),f.x),f.y),
+               mix(mix(hash(i+vec3(0, 0, 1)), 
+                       hash(i+vec3(1, 0, 1)),f.x),
+                   mix(hash(i+vec3(0, 1, 1)), 
+                       hash(i+vec3(1, 1, 1)),f.x),f.y),f.z);
+}
+
+// From: https://www.shadertoy.com/view/3sGSWVF
+// Slightly high-passed continuous value-noise.
+float grainSource(vec3 x, float strength, float pitch){
+    float center = noise(x);
+	float v1 = center - noise(vec3( 1, 0, 0)/pitch + x) + 0.5;
+	float v2 = center - noise(vec3( 0, 1, 0)/pitch + x) + 0.5;
+	float v3 = center - noise(vec3(-1, 0, 0)/pitch + x) + 0.5;
+	float v4 = center - noise(vec3( 0,-1, 0)/pitch + x) + 0.5;
+    
+	float total = (v1 + v2 + v3 + v4) / 4.0;
+	return mix(1, 0.5 + total, strength);
+}
+
+vec3 applyGrain(ivec2 uv, vec3 c){
+    float grainLift     = 0.6;
+    float grainStrength = 0.4;
+    float grainTimeFactor = 0.1;
+    
+    float timeColorOffset = 1.2;
+    vec3 grain = vec3(
+        grainSource(vec3(uv, floor(grainTimeFactor*time)),                   grainStrength, grainLift),
+        grainSource(vec3(uv, floor(grainTimeFactor*time + timeColorOffset)), grainStrength, grainLift),
+        grainSource(vec3(uv, floor(grainTimeFactor*time - timeColorOffset)), grainStrength, grainLift));
+    
+    return c * grain;
+}
+
+vec2 computeDistortedUV(vec2 uv, float aspectRatio){
+    uv          = uv * 2 - 1;
+    float   r2  = dot(uv, uv);
+    float   k1  = 0.02f;
+    
+    float maxR2     = dot(vec2(1), vec2(1));
+    float maxFactor = maxR2 * k1;
+    
+    // correction only needed for pincushion distortion
+    maxFactor       = min(maxFactor, 0);
+    
+    uv /= 1 + r2*k1;
+    
+    // correction to avoid going out of [-1, 1] range when using barrel distortion 
+    uv *= 1 + maxFactor;
+    
+    return uv * 0.5 + 0.5;
+}
+
+float computeLocalContrast(vec2 uv){
+    float lumaMin = 100;
+    float lumaMax = 0;
+    
+    vec2 pixelSize = vec2(1) / textureSize(sampler2D(inTexture, textureSampler), 0);
+    
+    for(int x = -1; x <= 1; x++){
+        for(int y = -1; y <= 1; y++){
+            vec3 c = texture(sampler2D(inTexture, textureSampler), uv + vec2(x, y) * pixelSize).rgb;
+            float luma  = computeLuma(c);
+            lumaMin     = min(lumaMin, luma);
+            lumaMax     = max(lumaMax, luma);
+        }
+    }
+    
+    return lumaMax - lumaMin;
+}
+
+vec3 computeChromaticAberrationScale(vec2 uv){
+    float   localContrast   = computeLocalContrast(uv);
+    vec3    colorScales     = vec3(-1, 0, 1);
+    float   aberrationScale = 0.004;
+    vec3    maxScaleFactors = colorScales * aberrationScale;
+    float   factor          = clamp(localContrast, 0, 1);
+    return mix(vec3(0), maxScaleFactors, factor);
+}
+
+vec3 sampleColorChromaticAberration(vec2 uv){
+    vec2 toCenter       = (vec2(0.5) - uv);
+    
+    vec3 scaleFactors = computeChromaticAberrationScale(uv);
+    
+    float r = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.r).r;
+    float g = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.g).g;
+    float b = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.b).b;
+    return vec3(r, g, b);
+}
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
+        return;
+    }
+    ivec2   textureRes  = textureSize(sampler2D(inTexture, textureSampler), 0);
+    ivec2   coord       = ivec2(gl_GlobalInvocationID.xy);
+    vec2    uv          = vec2(coord) / textureRes;
+    float   aspectRatio = float(textureRes.x) / textureRes.y;
+    uv                  = computeDistortedUV(uv, aspectRatio);
+    vec3 linearColor    = sampleColorChromaticAberration(uv);
+    vec3 tonemapped     = ACESFilm(linearColor);
+    tonemapped          = applyGrain(coord, tonemapped);
+    
+    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
+    imageStore(outImage, coord, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxel.inc b/projects/voxelization/resources/shaders/voxel.inc
new file mode 100644
index 0000000000000000000000000000000000000000..6133ca7cfc52ca77cb70fb8c2cc0e83ef6da4016
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxel.inc
@@ -0,0 +1,179 @@
+#include "brdf.inc"
+
+struct VoxelInfo{
+    vec3 offset;
+    float extent;
+};
+
+struct PackedVoxelData{
+    uint color;
+    uint normal;
+    uint albedo;
+};
+
+uint flattenVoxelUVToIndex(ivec3 UV, ivec3 voxelImageSize){
+    return UV.x + UV.y * voxelImageSize.x + UV.z *  voxelImageSize.x*  voxelImageSize.y;
+}
+
+vec3 worldToVoxelCoordinates(vec3 world, VoxelInfo info){
+    return (world - info.offset) / info.extent + 0.5f;
+}
+
+ivec3 voxelCoordinatesToUV(vec3 voxelCoordinates, ivec3 voxelImageResolution){
+    return ivec3(voxelCoordinates * voxelImageResolution);
+}
+
+vec3 voxelCoordinatesToWorldPosition(ivec3 coord, int voxelResolution, VoxelInfo voxelInfo, float voxelHalfSize){
+    return (vec3(coord) / voxelResolution - 0.5) * voxelInfo.extent + voxelHalfSize + voxelInfo.offset;
+}
+
+// packed voxel data: 
+// 1 bit opacity
+// 7 bit exposure
+// 8 bit blue
+// 8 bit green
+// 8 bit red
+float maxExposure = 16.f;
+
+uint packVoxelColor(vec3 color){
+    
+    color               = clamp(color, vec3(0), vec3(maxExposure));
+    float maxComponent  = max(max(max(color.r, color.g), color.b), 1.f);
+    color               /= maxComponent;
+    
+    uint opaqueBit      = 1 << 31;
+    uint exposureBits   = (0x0000007F & uint(maxComponent / maxExposure * 127)) << 24;
+    uint redBits        = (0x000000FF & uint(color.r * 255)) << 0;
+    uint greenBits      = (0x000000FF & uint(color.g * 255)) << 8;
+    uint blueBits       = (0x000000FF & uint(color.b * 255)) << 16;
+    return opaqueBit | exposureBits | blueBits | greenBits | redBits;
+}
+
+vec4 unpackVoxelColor(uint packed){
+    vec4 rgba;
+    rgba.r = (packed >> 0  & 0x000000FF) / 255.f;
+    rgba.g = (packed >> 8  & 0x000000FF) / 255.f;
+    rgba.b = (packed >> 16 & 0x000000FF) / 255.f;
+    rgba.a =  packed  >> 31; 
+    
+    rgba.rgb *= (packed >> 24 & 0x0000007F) / 127.f * maxExposure; 
+    
+    return rgba;
+}
+
+uint packSNormInto9Bits(float x){
+    uint lengthBits = 0x000000FF & uint(abs(x) * 255.f);
+    uint signBits   = (x < 0 ? 1 : 0)  << 8;
+    return lengthBits | signBits;
+}
+
+float unpack9LowBitsIntoSNorm(uint bits){
+    bits = (0x000001FF & bits);
+    float length    = bits / 255.f;
+    float sign      = (bits >> 8) == 0 ? 1 : -1;
+    return sign * length;
+}
+
+// normals are packed with 9 bits each, 8 for length and 1 for sign
+uint packVoxelNormal(vec3 N){
+    N           = clamp(N, vec3(0), vec3(1));
+    uint xBits  = packSNormInto9Bits(N.x) << 0;
+    uint yBits  = packSNormInto9Bits(N.y) << 9;
+    uint zBits  = packSNormInto9Bits(N.z) << 18;
+    return zBits | yBits | xBits;
+}
+
+vec3 unpackVoxelNormal(uint packed){
+    vec3 N;
+    N.x  = unpack9LowBitsIntoSNorm(packed >> 0);
+    N.y  = unpack9LowBitsIntoSNorm(packed >> 9);
+    N.z  = unpack9LowBitsIntoSNorm(packed >> 18);
+    return normalize(N);
+}
+
+uint packUNormInto8Bits(float x){
+    return 0x000000FF & uint(abs(x) * 255.f);
+}
+
+float unpack8LowBitsIntoUNorm(uint bits){
+    bits = (0x000000FF & bits);
+    return bits / 255.f;
+}
+
+// albedo is packed with 8 bits each
+uint packVoxelAlbedo(vec3 albedo){
+    albedo      = clamp(albedo, vec3(0), vec3(1));
+    uint rBits  = packUNormInto8Bits(albedo.r) << 0;
+    uint gBits  = packUNormInto8Bits(albedo.g) << 8;
+    uint bBits  = packUNormInto8Bits(albedo.b) << 16;
+    return bBits | gBits | rBits;
+}
+
+vec3 unpackVoxelAlbedo(uint packed){
+    vec3 albedo;
+    albedo.r  = unpack8LowBitsIntoUNorm(packed >> 0);
+    albedo.g  = unpack8LowBitsIntoUNorm(packed >> 8);
+    albedo.b  = unpack8LowBitsIntoUNorm(packed >> 16);
+    return albedo;
+}
+
+vec3 voxelConeTrace(vec3 direction, vec3 startPosition, float coneAngleRadian, texture3D voxelTexture, sampler voxelSampler, VoxelInfo voxelInfo){
+
+    int voxelResolution =  textureSize(sampler3D(voxelTexture, voxelSampler), 0).x;
+    float voxelSize     = voxelInfo.extent / voxelResolution;
+    float maxMip        = float(log2(voxelResolution));
+    float maxStableMip  = 4;    // must be the same as in Voxelization::voxelizeMeshes
+    maxMip              = min(maxMip, maxStableMip);
+    float d             = 2 * sqrt(3 * pow(voxelSize, 2));
+    vec3 color          = vec3(0);
+    float a             = 0;
+    
+    float coneAngleHalf = coneAngleRadian * 0.5f;
+    
+    int maxSamples = 16;
+    for(int i = 0; i < maxSamples; i++){
+        
+        vec3 samplePos      = startPosition + d * direction;
+        vec3 sampleUV       = worldToVoxelCoordinates(samplePos, voxelInfo);
+        
+        if(a >= 0.95 || any(lessThan(sampleUV, vec3(0))) || any(greaterThan(sampleUV, vec3(1)))){
+            break;
+        }
+        
+        float coneDiameter  = 2 * tan(coneAngleHalf) * d;
+        float mip           = log2(coneDiameter / voxelSize);
+        mip                 = min(mip, maxMip);
+    
+        vec4 voxelSample    = textureLod(sampler3D(voxelTexture, voxelSampler), sampleUV , mip);
+        
+        color               += (1 - a) * voxelSample.rgb;
+        a                   += (1 - a) * voxelSample.a;
+        
+        float minStepSize   = 1.f;
+        d                   += max(coneDiameter, minStepSize);
+    }
+    return color;
+}
+
+float degreeToRadian(float d){
+    return d / 180.f * pi;
+}
+
+vec3 diffuseVoxelTraceHemisphere(mat3 toSurface, vec3 position, texture3D voxelTexture, sampler voxelSampler, VoxelInfo voxelInfo){
+    float coneAngle = degreeToRadian(60.f);
+    vec3 diffuseTrace = vec3(0);
+    {
+        vec3 sampleDirection    = toSurface * vec3(0, 0, 1);
+        float weight            = pi / 4.f;
+        diffuseTrace            += weight * voxelConeTrace(sampleDirection, position, coneAngle, voxelTexture, voxelSampler, voxelInfo);
+    }
+    for(int i = 0; i < 6;i++){
+        float theta             = 2 * pi / i;
+        float phi               = pi / 3;   // 60 degrees
+        vec3 sampleDirection    = toSurface * vec3(cos(theta) * sin(phi), sin(theta) * sin(phi), cos(phi));
+        float weight            = pi * (3.f / 4.f) / 6;
+        vec3 trace              = voxelConeTrace(sampleDirection, position, coneAngle, voxelTexture, voxelSampler, voxelInfo);
+        diffuseTrace            += weight * trace;
+    }
+    return diffuseTrace;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelBufferToImage.comp b/projects/voxelization/resources/shaders/voxelBufferToImage.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2c2cffe856c8b0fc7db07202572aaa35e8445603
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelBufferToImage.comp
@@ -0,0 +1,33 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+#include "voxel.inc"
+
+layout(set=0, binding=0, std430) buffer voxelBuffer{
+    PackedVoxelData packedVoxelData[];
+};
+
+layout(set=0, binding=1, rgba16f) uniform image3D voxelImage;
+
+layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
+
+void main(){
+
+    ivec3 voxelImageSize = imageSize(voxelImage);
+    if(any(greaterThanEqual(gl_GlobalInvocationID, voxelImageSize))){
+        return;
+    }
+    ivec3 UV = ivec3(gl_GlobalInvocationID);
+    uint flatIndex = flattenVoxelUVToIndex(UV, voxelImageSize);
+    
+    vec4 color = unpackVoxelColor(packedVoxelData[flatIndex].color);
+    
+    // for proper visualisation voxel secondary bounce should be disabled, otherwise it adds color
+    
+    // for debugging: write normal into image, so voxel visualisation draws normal
+    // color = vec4(unpackVoxelNormal(packedVoxelData[flatIndex].normal), color.a); 
+    
+    // for debugging: write albedo into image, so voxel visualisation draws albedo
+    // color = vec4(unpackVoxelAlbedo(packedVoxelData[flatIndex].albedo), color.a); 
+    
+    imageStore(voxelImage, UV, vec4(color));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelReset.comp b/projects/voxelization/resources/shaders/voxelReset.comp
new file mode 100644
index 0000000000000000000000000000000000000000..79eda9ec95e703d39af57bc3b29044f0ad6c1bf9
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelReset.comp
@@ -0,0 +1,23 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+#include "voxel.inc"
+
+layout(set=0, binding=0) buffer voxelizationBuffer{
+    PackedVoxelData packedVoxelData[];
+};
+
+layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    uint voxelCount;
+};
+
+void main(){
+
+    if(gl_GlobalInvocationID.x> voxelCount){
+        return;
+    }  
+    packedVoxelData[gl_GlobalInvocationID.x].color     = 0;
+    packedVoxelData[gl_GlobalInvocationID.x].normal    = 0;
+    packedVoxelData[gl_GlobalInvocationID.x].albedo    = 0;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelSecondaryBounce.comp b/projects/voxelization/resources/shaders/voxelSecondaryBounce.comp
new file mode 100644
index 0000000000000000000000000000000000000000..29026e7052861ab190200b23d9f860bc609d1550
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelSecondaryBounce.comp
@@ -0,0 +1,46 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+#include "voxel.inc"
+#include "brdf.inc"
+
+layout(set=0, binding=0, std430) buffer voxelBuffer{
+    PackedVoxelData packedVoxelData[];
+};
+layout(set=0, binding=1) uniform texture3D          voxelImageIn;
+layout(set=0, binding=2) uniform sampler            voxelSampler;
+layout(set=0, binding=3, rgba16f) uniform image3D   voxelImageOut;
+layout(set=0, binding=4) uniform voxelizationInfo{
+    VoxelInfo voxelInfo;
+};
+
+layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
+
+void main(){
+
+    ivec3 voxelImageSize = imageSize(voxelImageOut);
+    if(any(greaterThanEqual(gl_GlobalInvocationID, voxelImageSize))){
+        return;
+    }
+    ivec3 UV = ivec3(gl_GlobalInvocationID);
+    
+    vec4 color = texelFetch(sampler3D(voxelImageIn, voxelSampler), UV, 0);
+    
+    if(color.a > 0){
+        uint flatIndex  = flattenVoxelUVToIndex(UV, voxelImageSize);
+        vec3 N          = unpackVoxelNormal(packedVoxelData[flatIndex].normal);
+        
+        float halfVoxelSize = voxelInfo.extent / float(voxelImageSize.x) * 0.5f;
+        vec3 pos            = voxelCoordinatesToWorldPosition(UV, voxelImageSize.x, voxelInfo, halfVoxelSize);
+        
+        vec3 up         = abs(N.y) >= 0.99 ? vec3(1, 0, 0) : vec3(0, 1, 0);
+        vec3 right      = normalize(cross(up, N));
+        up              = cross(N, right); 
+        mat3 toSurface  = mat3(right, up, N);
+    
+        vec3 secondaryBounce    = diffuseVoxelTraceHemisphere(toSurface, pos, voxelImageIn, voxelSampler, voxelInfo);
+        vec3 albedo             = unpackVoxelAlbedo(packedVoxelData[flatIndex].albedo);
+        color.rgb               += lambertBRDF(albedo) * secondaryBounce;
+    }
+    
+    imageStore(voxelImageOut, UV, color);
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelVisualisation.frag b/projects/voxelization/resources/shaders/voxelVisualisation.frag
new file mode 100644
index 0000000000000000000000000000000000000000..0b02beb7e848ab20cda4b012f77d1fa664b6ab53
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelVisualisation.frag
@@ -0,0 +1,9 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location=0) in vec3 passColorToFrag;
+layout(location=0) out vec3 outColor;
+
+void main()	{
+    outColor = passColorToFrag;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelVisualisation.geom b/projects/voxelization/resources/shaders/voxelVisualisation.geom
new file mode 100644
index 0000000000000000000000000000000000000000..e98076fcc83a69a903df454cb00267da84e3f223
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelVisualisation.geom
@@ -0,0 +1,104 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(points) in;
+layout (triangle_strip, max_vertices = 24) out;
+
+layout( push_constant ) uniform constants{
+    mat4 viewProjection;
+};
+
+layout(location = 0) in float passCubeHalf[1];
+layout(location = 1) in vec3 passColorToGeom[1];
+
+
+layout(location = 0) out vec3 passColorToFrag;
+
+void main()	{
+    float cubeHalf = passCubeHalf[0];
+    // right
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1, 1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1, 1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1, -1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1, -1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    EndPrimitive();
+    // left
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, 1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, 1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, -1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, -1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    EndPrimitive();
+    // back
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,   1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();                                                                  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,  -1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();                                                                  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1,  1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();                                                                  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, -1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    EndPrimitive();
+    // front
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,   1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();                                                                  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,  -1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();                                                                  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1,  1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();                                                                  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, -1, 1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    EndPrimitive();
+    // bot
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,  1,  1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex(); 
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,  1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, 1,  1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, 1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    EndPrimitive();
+    // top
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,  -1,  1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();   
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(1,  -1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();  
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, -1,  1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();   
+    gl_Position = viewProjection * vec4(gl_in[0].gl_Position.xyz + cubeHalf * vec3(-1, -1, -1), 1);
+    passColorToFrag = passColorToGeom[0];
+    EmitVertex();
+    EndPrimitive();
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelVisualisation.vert b/projects/voxelization/resources/shaders/voxelVisualisation.vert
new file mode 100644
index 0000000000000000000000000000000000000000..e26e2209ffb9bd3e62103fa9e7eeccce13d7d602
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelVisualisation.vert
@@ -0,0 +1,36 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "voxel.inc"
+
+layout(location = 0) out float passCubeHalf;
+layout(location = 1) out vec3 passColorToGeom;
+
+layout( push_constant ) uniform constants{
+    mat4 viewProjection;
+};
+
+layout(set=0, binding=0, rgba16f) uniform image3D  voxelImage;
+layout(set=0, binding=1) uniform voxelizationInfo{
+    VoxelInfo voxelInfo;
+};
+
+
+void main()	{
+    passCubeHalf        = voxelInfo.extent / float(imageSize(voxelImage).x) * 0.5f;
+    int voxelResolution = imageSize(voxelImage).x;
+    int slicePixelCount = voxelResolution * voxelResolution;
+    int z               = gl_VertexIndex / slicePixelCount;
+    int index2D         = gl_VertexIndex % slicePixelCount;
+    int y               = index2D / voxelResolution;
+    int x               = index2D % voxelResolution;
+    vec3 position       = voxelCoordinatesToWorldPosition(ivec3(x, y, z), voxelResolution, voxelInfo, passCubeHalf);
+	gl_Position         = vec4(position, 1.0);
+    
+    vec4 voxelColor = imageLoad(voxelImage, ivec3(x,y,z));
+    if(voxelColor.a == 0){
+        gl_Position.x /= 0; // clip
+    }
+    passColorToGeom = voxelColor.rgb;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelization.frag b/projects/voxelization/resources/shaders/voxelization.frag
new file mode 100644
index 0000000000000000000000000000000000000000..0bbd26bff249db1390399b26f2f4b5a139195fef
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelization.frag
@@ -0,0 +1,52 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "voxel.inc"
+#include "perMeshResources.inc"
+#include "lightInfo.inc"
+#include "shadowMapping.inc"
+#include "brdf.inc"
+
+layout(location = 0) in vec3 passPos;
+layout(location = 1) in vec2 passUV;
+layout(location = 2) in vec3 passN;
+
+layout(set=0, binding=0, std430) buffer voxelizationBuffer{
+    PackedVoxelData packedVoxelData[];
+};
+
+layout(set=0, binding=1) uniform voxelizationInfo{
+    VoxelInfo voxelInfo;
+};
+
+layout(set=0, binding=2, r8) uniform image3D voxelImage;
+
+layout(set=0, binding=3) uniform sunBuffer {
+    LightInfo lightInfo;
+};
+
+layout(set=0, binding=4) uniform texture2D  shadowMap;
+layout(set=0, binding=5) uniform sampler    shadowMapSampler;
+
+void main()	{
+    vec3 voxelCoordinates = worldToVoxelCoordinates(passPos, voxelInfo);
+    ivec3 voxelImageSize = imageSize(voxelImage);
+    ivec3 UV = voxelCoordinatesToUV(voxelCoordinates, voxelImageSize);
+    if(any(lessThan(UV, ivec3(0))) || any(greaterThanEqual(UV, voxelImageSize))){
+        return;
+    }
+    uint flatIndex = flattenVoxelUVToIndex(UV, voxelImageSize);
+
+    vec3 albedo = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
+    
+    vec3 N      = normalize(passN);
+    float NoL   = clamp(dot(N, lightInfo.L), 0, 1);
+    vec3 sun    = lightInfo.sunStrength * lightInfo.sunColor * NoL * shadowTest(passPos, lightInfo, shadowMap, shadowMapSampler, vec2(0));
+    vec3 color  = albedo * sun;
+    color       = lambertBRDF(albedo) * sun;
+    
+    atomicMax(packedVoxelData[flatIndex].color, packVoxelColor(color));
+    atomicMax(packedVoxelData[flatIndex].normal, packVoxelNormal(N));
+    atomicMax(packedVoxelData[flatIndex].albedo, packVoxelAlbedo(albedo));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelization.geom b/projects/voxelization/resources/shaders/voxelization.geom
new file mode 100644
index 0000000000000000000000000000000000000000..56542d960d65db6ca12c5f84837cb0c0a9ff0ded
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelization.geom
@@ -0,0 +1,38 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(triangles) in;
+layout (triangle_strip, max_vertices = 3) out;
+
+layout(location = 0) in vec3 passPosIn[3];
+layout(location = 1) in vec2 passUVIn[3];
+layout(location = 2) in vec3 passNIn[3];
+
+layout(location = 0) out vec3 passPos;
+layout(location = 1) out vec2 passUV;
+layout(location = 2) out vec3 passN;
+
+void main()	{
+    // compute geometric normal, no normalization necessary
+    vec3 N = cross(passPosIn[0] - passPosIn[1], passPosIn[0] - passPosIn[2]);
+    N = abs(N); // only interested in the magnitude
+    
+    for(int i = 0; i < 3; i++){
+        // swizzle position, so biggest side is rasterized
+        if(N.z > N.x && N.z > N.y){
+            gl_Position = gl_in[i].gl_Position.xyzw;
+        }
+        else if(N.x > N.y){
+            gl_Position = gl_in[i].gl_Position.yzxw;
+        }
+        else{
+            gl_Position = gl_in[i].gl_Position.xzyw;
+        }
+        gl_Position.z = gl_Position.z * 0.5 + 0.5;  // xyz are kept in NDC range [-1, 1] so swizzling works, but vulkan needs final z in range [0, 1]
+        passPos = passPosIn[i];
+        passUV  = passUVIn[i];
+        passN   = passNIn[i];
+        EmitVertex();
+    }
+    EndPrimitive();
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelization.vert b/projects/voxelization/resources/shaders/voxelization.vert
new file mode 100644
index 0000000000000000000000000000000000000000..221d0f6d189cfe1d6fb8e9e8e2fc9c04884c40c1
--- /dev/null
+++ b/projects/voxelization/resources/shaders/voxelization.vert
@@ -0,0 +1,22 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+layout(location = 2) in vec2 inUV;
+
+layout(location = 0) out vec3 passPos;
+layout(location = 1) out vec2 passUV;
+layout(location = 2) out vec3 passN;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    mat4 model;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+    passPos     = (model * vec4(inPosition, 1)).xyz;
+    passUV      = inUV;
+    passN       = mat3(model) * inNormal;
+}
\ No newline at end of file
diff --git a/projects/cmd_sync_test/resources/triangle/Triangle.bin b/projects/voxelization/resources/triangle/Triangle.bin
similarity index 100%
rename from projects/cmd_sync_test/resources/triangle/Triangle.bin
rename to projects/voxelization/resources/triangle/Triangle.bin
diff --git a/projects/cmd_sync_test/resources/triangle/Triangle.blend b/projects/voxelization/resources/triangle/Triangle.blend
similarity index 100%
rename from projects/cmd_sync_test/resources/triangle/Triangle.blend
rename to projects/voxelization/resources/triangle/Triangle.blend
diff --git a/projects/cmd_sync_test/resources/triangle/Triangle.glb b/projects/voxelization/resources/triangle/Triangle.glb
similarity index 100%
rename from projects/cmd_sync_test/resources/triangle/Triangle.glb
rename to projects/voxelization/resources/triangle/Triangle.glb
diff --git a/projects/cmd_sync_test/resources/triangle/Triangle.gltf b/projects/voxelization/resources/triangle/Triangle.gltf
similarity index 100%
rename from projects/cmd_sync_test/resources/triangle/Triangle.gltf
rename to projects/voxelization/resources/triangle/Triangle.gltf
diff --git a/projects/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fac57735a6544c197f880f78e1f512382607d048
--- /dev/null
+++ b/projects/voxelization/src/BloomAndFlares.cpp
@@ -0,0 +1,341 @@
+#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("resources/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,
+                     "resources/shaders/bloomDownsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         dsProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+		m_DownsampleDescSets.push_back(
+                p_Core->createDescriptorSet(dsProg.getReflectedDescriptors()[0]));
+    }
+    m_DownsamplePipe = p_Core->createComputePipeline(
+            dsProg, { p_Core->getDescriptorSet(m_DownsampleDescSets[0]).layout });
+
+    // UPSAMPLE
+    vkcv::ShaderProgram usProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "resources/shaders/bloomUpsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         usProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+        m_UpsampleDescSets.push_back(
+                p_Core->createDescriptorSet(usProg.getReflectedDescriptors()[0]));
+    }
+    for (uint32_t mipLevel = 0; mipLevel < m_LensFeatures.getMipCount(); mipLevel++) {
+        m_UpsampleLensFlareDescSets.push_back(
+            p_Core->createDescriptorSet(usProg.getReflectedDescriptors()[0]));
+    }
+
+    m_UpsamplePipe = p_Core->createComputePipeline(
+            usProg, { p_Core->getDescriptorSet(m_UpsampleDescSets[0]).layout });
+
+    // LENS FEATURES
+    vkcv::ShaderProgram lensProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "resources/shaders/lensFlares.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         lensProg.addShader(shaderStage, path);
+                     });
+    m_LensFlareDescSet = p_Core->createDescriptorSet(lensProg.getReflectedDescriptors()[0]);
+    m_LensFlarePipe = p_Core->createComputePipeline(
+            lensProg, { p_Core->getDescriptorSet(m_LensFlareDescSet).layout });
+
+    // COMPOSITE
+    vkcv::ShaderProgram compProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "resources/shaders/bloomFlaresComposite.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         compProg.addShader(shaderStage, path);
+                     });
+    m_CompositeDescSet = p_Core->createDescriptorSet(compProg.getReflectedDescriptors()[0]);
+    m_CompositePipe = p_Core->createComputePipeline(
+            compProg, { p_Core->getDescriptorSet(m_CompositeDescSet).layout });
+
+    // radial LUT
+    const auto texture = vkcv::asset::loadTexture("resources/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
+    };
+
+    // downsample dispatch of original color attachment
+    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    vkcv::DescriptorWrites initialDownsampleWrites;
+    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
+    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
+    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
+
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_DownsamplePipe,
+            initialDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 0));
+
+    // downsample dispatches of blur buffer's mip maps
+    float mipDispatchCountX = dispatchCountX;
+    float mipDispatchCountY = dispatchCountY;
+    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipDescriptorWrites;
+        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
+        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
+        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
+
+        // mip dispatch calculation
+        mipDispatchCountX  /= 2.0f;
+        mipDispatchCountY /= 2.0f;
+
+        uint32_t mipDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
+                1
+        };
+
+        if(mipDispatchCount[0] == 0)
+            mipDispatchCount[0] = 1;
+        if(mipDispatchCount[1] == 0)
+            mipDispatchCount[1] = 1;
+
+        // mip blur dispatch
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_DownsamplePipe,
+                mipDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstantData(nullptr, 0));
+
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // upsample dispatch
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    const uint32_t upsampleMipLevels = std::min(
+    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
+    		static_cast<uint32_t>(5)
+	);
+
+    // upsample dispatch for each mip map
+    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipUpsampleWrites;
+        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
+        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
+        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
+
+        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
+
+        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
+        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
+        upsampleDispatchX /= 8.0f;
+        upsampleDispatchY /= 8.0f;
+
+        const uint32_t upsampleDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
+                1
+        };
+
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_UpsamplePipe,
+                upsampleDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstantData(nullptr, 0)
+        );
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // lens feature generation descriptor writes
+    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
+    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
+
+    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, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            vkcv::PushConstantData(nullptr, 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, p_Core->getDescriptorSet(m_UpsampleLensFlareDescSets[i]).vulkanHandle) },
+            vkcv::PushConstantData(nullptr, 0)
+        );
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_LensFeatures.getHandle());
+    }
+}
+
+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->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
+    };
+
+    // bloom composite dispatch
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_CompositePipe,
+            compositeDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            vkcv::PushConstantData((void*)&cameraForward, sizeof(cameraForward)));
+}
+
+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);
+}
+
+
diff --git a/projects/voxelization/src/BloomAndFlares.hpp b/projects/voxelization/src/BloomAndFlares.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2b410e5b256c5820d908372d2e23fd495853274a
--- /dev/null
+++ b/projects/voxelization/src/BloomAndFlares.hpp
@@ -0,0 +1,53 @@
+#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::PipelineHandle                     m_DownsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_DownsampleDescSets; // per mip desc set
+    std::vector<vkcv::DescriptorSetHandle>   m_UpsampleLensFlareDescSets; // per mip desc set
+
+    vkcv::PipelineHandle                     m_UpsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_UpsampleDescSets;   // per mip desc set
+
+    vkcv::PipelineHandle                     m_LensFlarePipe;
+    vkcv::DescriptorSetHandle                m_LensFlareDescSet;
+
+    vkcv::PipelineHandle                     m_CompositePipe;
+    vkcv::DescriptorSetHandle                m_CompositeDescSet;
+
+    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment, 
+        const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward);
+};
+
+
+
diff --git a/projects/voxelization/src/ShadowMapping.cpp b/projects/voxelization/src/ShadowMapping.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a330394b7bd7ff2a4b8c347bd79e676dbc70f846
--- /dev/null
+++ b/projects/voxelization/src/ShadowMapping.cpp
@@ -0,0 +1,321 @@
+#include "ShadowMapping.hpp"
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+const vk::Format            shadowMapFormat         = vk::Format::eR16G16B16A16Unorm;
+const vk::Format            shadowMapDepthFormat    = vk::Format::eD32Sfloat;
+const uint32_t              shadowMapResolution     = 1024;
+const vkcv::Multisampling   msaa                    = vkcv::Multisampling::MSAA8X;
+
+vkcv::ShaderProgram loadShadowShader() {
+	vkcv::ShaderProgram shader;
+	vkcv::shader::GLSLCompiler compiler;
+	compiler.compile(vkcv::ShaderStage::VERTEX, "resources/shaders/shadow.vert",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, "resources/shaders/shadow.frag",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+vkcv::ShaderProgram loadDepthToMomentsShader() {
+	vkcv::ShaderProgram shader;
+	vkcv::shader::GLSLCompiler compiler;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/depthToMoments.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+vkcv::ShaderProgram loadShadowBlurXShader() {
+	vkcv::ShaderProgram shader;
+	vkcv::shader::GLSLCompiler compiler;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/shadowBlurX.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+vkcv::ShaderProgram loadShadowBlurYShader() {
+	vkcv::ShaderProgram shader;
+	vkcv::shader::GLSLCompiler compiler;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/shadowBlurY.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+glm::mat4 computeShadowViewProjectionMatrix(
+	const glm::vec3&            lightDirection, 
+	const vkcv::camera::Camera& camera, 
+	float                       maxShadowDistance,
+	const glm::vec3&            voxelVolumeOffset,
+	float                       voxelVolumeExtent) {
+
+	const glm::vec3 cameraPos   = camera.getPosition();
+	const glm::vec3 forward     = glm::normalize(camera.getFront());
+	glm::vec3 up                = glm::normalize(camera.getUp());
+	const glm::vec3 right       = glm::normalize(glm::cross(forward, up));
+	up = glm::cross(right, forward);
+
+	const float fov         = camera.getFov();
+	const float aspectRatio = camera.getRatio();
+
+	float near;
+	float far;
+	camera.getNearFar(near, far);
+	far = std::min(maxShadowDistance, far);
+
+	const glm::vec3 nearCenter  = cameraPos + forward * near;
+	const float nearUp          = near * tan(fov * 0.5);
+	const float nearRight       = nearUp * aspectRatio;
+	
+	const glm::vec3 farCenter   = cameraPos + forward * far;
+	const float farUp           = far * tan(fov * 0.5);
+	const float farRight        = farUp * aspectRatio;
+
+	std::array<glm::vec3, 8> viewFrustumCorners = {
+		nearCenter + right * nearRight + nearUp * up,
+		nearCenter + right * nearRight - nearUp * up,
+		nearCenter - right * nearRight + nearUp * up,
+		nearCenter - right * nearRight - nearUp * up,
+
+		farCenter + right * farRight + farUp * up,
+		farCenter + right * farRight - farUp * up,
+		farCenter - right * farRight + farUp * up,
+		farCenter - right * farRight - farUp * up
+	};
+
+	std::array<glm::vec3, 8> voxelVolumeCorners = {
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(1, 1, 1),
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(1, 1, -1),
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(1, -1, 1),
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(1, -1, -1),
+
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(-1, 1, 1),
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(-1, 1, -1),
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(-1, -1, 1),
+		voxelVolumeOffset + voxelVolumeExtent * glm::vec3(-1, -1, -1),
+	};
+
+	glm::vec3 minView(std::numeric_limits<float>::max());
+	glm::vec3 maxView(std::numeric_limits<float>::lowest());
+
+	const glm::mat4 view = glm::lookAt(glm::vec3(0), -lightDirection, glm::vec3(0, -1, 0));
+
+	auto getMinMaxView = [&](std::array<glm::vec3, 8> points) {
+		for (const glm::vec3& p : points) {
+			const auto& pView = glm::vec3(view * glm::vec4(p, 1));
+			minView = glm::min(minView, pView);
+			maxView = glm::max(maxView, pView);
+		}
+	};
+
+	getMinMaxView(viewFrustumCorners);
+	getMinMaxView(voxelVolumeCorners);
+
+	// rotationaly invariant to avoid shadow  swimming when moving camera
+	// could potentially be wasteful, but guarantees stability, regardless of camera and voxel volume
+	 glm::vec3 scale = glm::vec3(1.f / glm::max(far, voxelVolumeExtent));
+
+	glm::vec3 offset = -0.5f * (maxView + minView) * scale;
+
+	// snap to texel to avoid shadow swimming when moving
+	glm::vec2 offset2D = glm::vec2(offset);
+	glm::vec2 frustumExtent2D = glm::vec2(1) / glm::vec2(scale);
+	glm::vec2 texelSize = glm::vec2(frustumExtent2D / static_cast<float>(shadowMapResolution));
+	offset2D = glm::ceil(offset2D / texelSize) * texelSize;
+	offset.x = offset2D.x;
+	offset.y = offset2D.y;
+
+	glm::mat4 crop(1);
+	crop[0][0] = scale.x;
+	crop[1][1] = scale.y;
+	crop[2][2] = scale.z;
+
+	crop[3][0] = offset.x;
+	crop[3][1] = offset.y;
+	crop[3][2] = offset.z;
+
+	glm::mat4 vulkanCorrectionMatrix(1.f);
+	vulkanCorrectionMatrix[2][2] = 0.5;
+	vulkanCorrectionMatrix[3][2] = 0.5;
+
+	return vulkanCorrectionMatrix * crop * view;
+}
+
+ShadowMapping::ShadowMapping(vkcv::Core* corePtr, const vkcv::VertexLayout& vertexLayout) : 
+	m_corePtr(corePtr),
+	m_shadowMap(corePtr->createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1, true, true)),
+	m_shadowMapIntermediate(corePtr->createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1, false, true)),
+	m_shadowMapDepth(corePtr->createImage(shadowMapDepthFormat, shadowMapResolution, shadowMapResolution, 1, false, false, false, msaa)),
+	m_lightInfoBuffer(corePtr->createBuffer<LightInfo>(vkcv::BufferType::UNIFORM, sizeof(glm::vec3))){
+
+	vkcv::ShaderProgram shadowShader = loadShadowShader();
+
+	// pass
+	const std::vector<vkcv::AttachmentDescription> shadowAttachments = {
+		vkcv::AttachmentDescription(vkcv::AttachmentOperation::STORE, vkcv::AttachmentOperation::CLEAR, shadowMapDepthFormat)
+	};
+	vkcv::PassConfig shadowPassConfig(shadowAttachments, msaa);
+	m_shadowMapPass = corePtr->createPass(shadowPassConfig);
+
+	// pipeline
+	vkcv::PipelineConfig shadowPipeConfig{
+		shadowShader,
+		shadowMapResolution,
+		shadowMapResolution,
+		m_shadowMapPass,
+		vertexLayout,
+		{},
+		false
+	};
+	shadowPipeConfig.m_multisampling        = msaa;
+	shadowPipeConfig.m_EnableDepthClamping  = true;
+	shadowPipeConfig.m_culling              = vkcv::CullMode::Front;
+	m_shadowMapPipe                         = corePtr->createGraphicsPipeline(shadowPipeConfig);
+
+	m_shadowSampler = corePtr->createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
+
+	// depth to moments
+	vkcv::ShaderProgram depthToMomentsShader    = loadDepthToMomentsShader();
+	m_depthToMomentsDescriptorSet               = corePtr->createDescriptorSet(depthToMomentsShader.getReflectedDescriptors()[0]);
+	m_depthToMomentsPipe                        = corePtr->createComputePipeline(depthToMomentsShader, { corePtr->getDescriptorSet(m_depthToMomentsDescriptorSet).layout });
+
+	vkcv::DescriptorWrites depthToMomentDescriptorWrites;
+	depthToMomentDescriptorWrites.sampledImageWrites    = { vkcv::SampledImageDescriptorWrite(0, m_shadowMapDepth.getHandle()) };
+	depthToMomentDescriptorWrites.samplerWrites         = { vkcv::SamplerDescriptorWrite(1, m_shadowSampler) };
+	depthToMomentDescriptorWrites.storageImageWrites    = { vkcv::StorageImageDescriptorWrite(2, m_shadowMap.getHandle()) };
+	corePtr->writeDescriptorSet(m_depthToMomentsDescriptorSet, depthToMomentDescriptorWrites);
+
+	// shadow blur X
+	vkcv::ShaderProgram shadowBlurXShader    = loadShadowBlurXShader();
+	m_shadowBlurXDescriptorSet              = corePtr->createDescriptorSet(shadowBlurXShader.getReflectedDescriptors()[0]);
+	m_shadowBlurXPipe                       = corePtr->createComputePipeline(shadowBlurXShader, { corePtr->getDescriptorSet(m_shadowBlurXDescriptorSet).layout });
+
+	vkcv::DescriptorWrites shadowBlurXDescriptorWrites;
+	shadowBlurXDescriptorWrites.sampledImageWrites   = { vkcv::SampledImageDescriptorWrite(0, m_shadowMap.getHandle()) };
+	shadowBlurXDescriptorWrites.samplerWrites        = { vkcv::SamplerDescriptorWrite(1, m_shadowSampler) };
+	shadowBlurXDescriptorWrites.storageImageWrites   = { vkcv::StorageImageDescriptorWrite(2, m_shadowMapIntermediate.getHandle()) };
+	corePtr->writeDescriptorSet(m_shadowBlurXDescriptorSet, shadowBlurXDescriptorWrites);
+
+	// shadow blur Y
+	vkcv::ShaderProgram shadowBlurYShader = loadShadowBlurYShader();
+	m_shadowBlurYDescriptorSet = corePtr->createDescriptorSet(shadowBlurYShader.getReflectedDescriptors()[0]);
+	m_shadowBlurYPipe = corePtr->createComputePipeline(shadowBlurYShader, { corePtr->getDescriptorSet(m_shadowBlurYDescriptorSet).layout });
+
+	vkcv::DescriptorWrites shadowBlurYDescriptorWrites;
+	shadowBlurYDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, m_shadowMapIntermediate.getHandle()) };
+	shadowBlurYDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, m_shadowSampler) };
+	shadowBlurYDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, m_shadowMap.getHandle()) };
+	corePtr->writeDescriptorSet(m_shadowBlurYDescriptorSet, shadowBlurYDescriptorWrites);
+}
+
+void ShadowMapping::recordShadowMapRendering(
+	const vkcv::CommandStreamHandle&    cmdStream,
+	const glm::vec2&                    lightAngleRadian,
+	const glm::vec3&                    lightColor,
+	float                               lightStrength,
+	float                               maxShadowDistance,
+	const std::vector<vkcv::Mesh>&      meshes,
+	const std::vector<glm::mat4>&       modelMatrices,
+	const vkcv::camera::Camera&         camera,
+	const glm::vec3&                    voxelVolumeOffset,
+	float                               voxelVolumeExtent) {
+
+	LightInfo lightInfo;
+	lightInfo.sunColor = lightColor;
+	lightInfo.sunStrength = lightStrength;
+	lightInfo.direction = glm::normalize(glm::vec3(
+		std::cos(lightAngleRadian.x) * std::cos(lightAngleRadian.y),
+		std::sin(lightAngleRadian.x),
+		std::cos(lightAngleRadian.x) * std::sin(lightAngleRadian.y)));
+
+	lightInfo.lightMatrix = computeShadowViewProjectionMatrix(
+		lightInfo.direction,
+		camera,
+		maxShadowDistance,
+		voxelVolumeOffset,
+		voxelVolumeExtent);
+	m_lightInfoBuffer.fill({ lightInfo });
+
+	std::vector<glm::mat4> mvpLight;
+	for (const auto& m : modelMatrices) {
+		mvpLight.push_back(lightInfo.lightMatrix * m);
+	}
+	const vkcv::PushConstantData shadowPushConstantData((void*)mvpLight.data(), sizeof(glm::mat4));
+
+	std::vector<vkcv::DrawcallInfo> drawcalls;
+	for (const auto& mesh : meshes) {
+		drawcalls.push_back(vkcv::DrawcallInfo(mesh, {}));
+	}
+
+	m_corePtr->recordDrawcallsToCmdStream(
+		cmdStream,
+		m_shadowMapPass,
+		m_shadowMapPipe,
+		shadowPushConstantData,
+		drawcalls,
+		{ m_shadowMapDepth.getHandle() });
+	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMapDepth.getHandle());
+
+	// depth to moments
+	uint32_t dispatchCount[3];
+	dispatchCount[0] = static_cast<uint32_t>(std::ceil(shadowMapResolution / 8.f));
+	dispatchCount[1] = static_cast<uint32_t>(std::ceil(shadowMapResolution / 8.f));
+	dispatchCount[2] = 1;
+
+	const uint32_t msaaSampleCount = msaaToSampleCount(msaa);
+
+	m_corePtr->prepareImageForStorage(cmdStream, m_shadowMap.getHandle());
+	m_corePtr->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_depthToMomentsPipe,
+		dispatchCount,
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_depthToMomentsDescriptorSet).vulkanHandle) },
+		vkcv::PushConstantData((void*)&msaaSampleCount, sizeof(msaaSampleCount)));
+	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
+
+	// blur X
+	m_corePtr->prepareImageForStorage(cmdStream, m_shadowMapIntermediate.getHandle());
+	m_corePtr->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_shadowBlurXPipe,
+		dispatchCount,
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurXDescriptorSet).vulkanHandle) },
+		vkcv::PushConstantData(nullptr, 0));
+	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMapIntermediate.getHandle());
+
+	// blur Y
+	m_corePtr->prepareImageForStorage(cmdStream, m_shadowMap.getHandle());
+	m_corePtr->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_shadowBlurYPipe,
+		dispatchCount,
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurYDescriptorSet).vulkanHandle) },
+		vkcv::PushConstantData(nullptr, 0));
+	m_shadowMap.recordMipChainGeneration(cmdStream);
+	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
+}
+
+vkcv::ImageHandle ShadowMapping::getShadowMap() {
+	return m_shadowMap.getHandle();
+}
+
+vkcv::SamplerHandle ShadowMapping::getShadowSampler() {
+	return m_shadowSampler;
+}
+
+vkcv::BufferHandle ShadowMapping::getLightInfoBuffer() {
+	return m_lightInfoBuffer.getHandle();
+}
\ No newline at end of file
diff --git a/projects/voxelization/src/ShadowMapping.hpp b/projects/voxelization/src/ShadowMapping.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8066d5bdc90a66c0823be4dc23cf6a12729e32c7
--- /dev/null
+++ b/projects/voxelization/src/ShadowMapping.hpp
@@ -0,0 +1,56 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include <vkcv/camera/Camera.hpp>
+
+#include <glm/glm.hpp>
+#include <glm/gtx/transform.hpp>
+
+struct LightInfo {
+	glm::vec3   direction;
+	float       padding;
+	glm::vec3   sunColor;
+	float       sunStrength;
+	glm::mat4   lightMatrix;
+};
+
+class ShadowMapping {
+public:
+	ShadowMapping(vkcv::Core* corePtr, const vkcv::VertexLayout& vertexLayout);
+
+	void recordShadowMapRendering(
+		const vkcv::CommandStreamHandle&    cmdStream,
+		const glm::vec2&                    lightAngleRadian,
+		const glm::vec3&                    lightColor,
+		float                               lightStrength,
+		float                               maxShadowDistance,
+		const std::vector<vkcv::Mesh>&      meshes,
+		const std::vector<glm::mat4>&       modelMatrices,
+		const vkcv::camera::Camera&         camera,
+		const glm::vec3&                    voxelVolumeOffset,
+		float                               voxelVolumeExtent);
+
+	vkcv::ImageHandle   getShadowMap();
+	vkcv::SamplerHandle getShadowSampler();
+	vkcv::BufferHandle  getLightInfoBuffer();
+
+private:
+	vkcv::Core* m_corePtr;
+
+	vkcv::Image                 m_shadowMap;
+	vkcv::Image                 m_shadowMapIntermediate;
+	vkcv::Image                 m_shadowMapDepth;
+	vkcv::SamplerHandle         m_shadowSampler;
+	vkcv::Buffer<LightInfo>     m_lightInfoBuffer;
+
+	vkcv::PassHandle            m_shadowMapPass;
+	vkcv::PipelineHandle        m_shadowMapPipe;
+
+	vkcv::PipelineHandle        m_depthToMomentsPipe;
+	vkcv::DescriptorSetHandle   m_depthToMomentsDescriptorSet;
+
+	vkcv::PipelineHandle        m_shadowBlurXPipe;
+	vkcv::DescriptorSetHandle   m_shadowBlurXDescriptorSet;
+
+	vkcv::PipelineHandle        m_shadowBlurYPipe;
+	vkcv::DescriptorSetHandle   m_shadowBlurYDescriptorSet;
+};
\ No newline at end of file
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c117b4b9e6b896fbf51aae83343f30281061be9f
--- /dev/null
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -0,0 +1,379 @@
+#include "Voxelization.hpp"
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <algorithm>
+
+vkcv::ShaderProgram loadVoxelizationShader() {
+	vkcv::shader::GLSLCompiler compiler;
+	vkcv::ShaderProgram shader;
+	compiler.compile(vkcv::ShaderStage::VERTEX, "resources/shaders/voxelization.vert",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::GEOMETRY, "resources/shaders/voxelization.geom",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, "resources/shaders/voxelization.frag",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+vkcv::ShaderProgram loadVoxelVisualisationShader() {
+	vkcv::shader::GLSLCompiler compiler;
+	vkcv::ShaderProgram shader;
+	compiler.compile(vkcv::ShaderStage::VERTEX, "resources/shaders/voxelVisualisation.vert",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::GEOMETRY, "resources/shaders/voxelVisualisation.geom",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, "resources/shaders/voxelVisualisation.frag",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+vkcv::ShaderProgram loadVoxelResetShader() {
+	vkcv::shader::GLSLCompiler compiler;
+	vkcv::ShaderProgram shader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/voxelReset.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+vkcv::ShaderProgram loadVoxelBufferToImageShader() {
+	vkcv::shader::GLSLCompiler compiler;
+	vkcv::ShaderProgram shader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/voxelBufferToImage.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+vkcv::ShaderProgram loadSecondaryBounceShader() {
+	vkcv::shader::GLSLCompiler compiler;
+	vkcv::ShaderProgram shader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/voxelSecondaryBounce.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shader.addShader(shaderStage, path);
+	});
+	return shader;
+}
+
+const uint32_t      voxelResolution = 128;
+uint32_t            voxelCount = voxelResolution * voxelResolution * voxelResolution;
+const vk::Format    voxelizationDummyFormat = vk::Format::eR8Unorm;
+const int           maxStableMip = 4;	// must be the same as in voxelConeTrace shader function
+
+Voxelization::Voxelization(
+	vkcv::Core* corePtr,
+	const Dependencies& dependencies,
+	vkcv::BufferHandle  lightInfoBuffer,
+	vkcv::ImageHandle   shadowMap,
+	vkcv::SamplerHandle shadowSampler,
+	vkcv::SamplerHandle voxelSampler,
+	vkcv::Multisampling msaa)
+	:
+	m_corePtr(corePtr), 
+	m_voxelImage(m_corePtr->createImage(vk::Format::eR16G16B16A16Sfloat, voxelResolution, voxelResolution, voxelResolution, true, true)),
+	m_voxelImageIntermediate(m_corePtr->createImage(vk::Format::eR16G16B16A16Sfloat, voxelResolution, voxelResolution, voxelResolution, true, true)),
+	m_dummyRenderTarget(m_corePtr->createImage(voxelizationDummyFormat, voxelResolution, voxelResolution, 1, false, false, true)),
+	m_voxelInfoBuffer(m_corePtr->createBuffer<VoxelizationInfo>(vkcv::BufferType::UNIFORM, 1)),
+	m_voxelBuffer(m_corePtr->createBuffer<VoxelBufferContent>(vkcv::BufferType::STORAGE, voxelCount)){
+
+	const vkcv::ShaderProgram voxelizationShader = loadVoxelizationShader();
+
+	const vkcv::PassConfig voxelizationPassConfig({vkcv::AttachmentDescription(
+		vkcv::AttachmentOperation::DONT_CARE, 
+		vkcv::AttachmentOperation::DONT_CARE, 
+		voxelizationDummyFormat) });
+	m_voxelizationPass = m_corePtr->createPass(voxelizationPassConfig);
+
+	std::vector<vkcv::DescriptorBinding> voxelizationDescriptorBindings = 
+	{ voxelizationShader.getReflectedDescriptors()[0] };
+	m_voxelizationDescriptorSet = m_corePtr->createDescriptorSet(voxelizationDescriptorBindings);
+
+	vkcv::DescriptorSetHandle dummyPerMeshDescriptorSet =
+		m_corePtr->createDescriptorSet({ voxelizationShader.getReflectedDescriptors()[1] });
+
+	const vkcv::PipelineConfig voxelizationPipeConfig{
+		voxelizationShader,
+		voxelResolution,
+		voxelResolution,
+		m_voxelizationPass,
+		dependencies.vertexLayout,
+		{ 
+			m_corePtr->getDescriptorSet(m_voxelizationDescriptorSet).layout,
+			m_corePtr->getDescriptorSet(dummyPerMeshDescriptorSet).layout},
+		false,
+		true };
+	m_voxelizationPipe = m_corePtr->createGraphicsPipeline(voxelizationPipeConfig);
+
+	vkcv::DescriptorWrites voxelizationDescriptorWrites;
+	voxelizationDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	voxelizationDescriptorWrites.uniformBufferWrites = { 
+		vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()),
+		vkcv::UniformBufferDescriptorWrite(3, lightInfoBuffer)
+	};
+	voxelizationDescriptorWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(4, shadowMap) };
+	voxelizationDescriptorWrites.samplerWrites      = { vkcv::SamplerDescriptorWrite(5, shadowSampler) };
+	voxelizationDescriptorWrites.storageImageWrites = { vkcv::StorageImageDescriptorWrite(2, m_voxelImageIntermediate.getHandle()) };
+	m_corePtr->writeDescriptorSet(m_voxelizationDescriptorSet, voxelizationDescriptorWrites);
+
+	vkcv::ShaderProgram voxelVisualisationShader = loadVoxelVisualisationShader();
+
+	const std::vector<vkcv::DescriptorBinding> voxelVisualisationDescriptorBindings = 
+		{ voxelVisualisationShader.getReflectedDescriptors()[0] };
+	m_visualisationDescriptorSet = m_corePtr->createDescriptorSet(voxelVisualisationDescriptorBindings);
+
+	const vkcv::AttachmentDescription voxelVisualisationColorAttachments(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		dependencies.colorBufferFormat
+	);
+
+	const vkcv::AttachmentDescription voxelVisualisationDepthAttachments(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		dependencies.depthBufferFormat
+	);
+
+	vkcv::PassConfig voxelVisualisationPassDefinition(
+		{ voxelVisualisationColorAttachments, voxelVisualisationDepthAttachments });
+	voxelVisualisationPassDefinition.msaa = msaa;
+	m_visualisationPass = m_corePtr->createPass(voxelVisualisationPassDefinition);
+
+	vkcv::PipelineConfig voxelVisualisationPipeConfig{
+		voxelVisualisationShader,
+		0,
+		0,
+		m_visualisationPass,
+		{},
+		{ m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).layout },
+		true,
+		false,
+		vkcv::PrimitiveTopology::PointList };	// points are extended to cubes in the geometry shader
+	voxelVisualisationPipeConfig.m_multisampling = msaa;
+	m_visualisationPipe = m_corePtr->createGraphicsPipeline(voxelVisualisationPipeConfig);
+
+	std::vector<uint16_t> voxelIndexData;
+	for (int i = 0; i < voxelCount; i++) {
+		voxelIndexData.push_back(i);
+	}
+
+	const vkcv::DescriptorSetUsage voxelizationDescriptorUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle);
+
+	vkcv::ShaderProgram resetVoxelShader = loadVoxelResetShader();
+
+	m_voxelResetDescriptorSet = m_corePtr->createDescriptorSet(resetVoxelShader.getReflectedDescriptors()[0]);
+	m_voxelResetPipe = m_corePtr->createComputePipeline(
+		resetVoxelShader,
+		{ m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).layout });
+
+	vkcv::DescriptorWrites resetVoxelWrites;
+	resetVoxelWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	m_corePtr->writeDescriptorSet(m_voxelResetDescriptorSet, resetVoxelWrites);
+
+	// buffer to image
+	vkcv::ShaderProgram bufferToImageShader = loadVoxelBufferToImageShader();
+
+	m_bufferToImageDescriptorSet = m_corePtr->createDescriptorSet(bufferToImageShader.getReflectedDescriptors()[0]);
+	m_bufferToImagePipe = m_corePtr->createComputePipeline(
+		bufferToImageShader,
+		{ m_corePtr->getDescriptorSet(m_bufferToImageDescriptorSet).layout });
+
+	vkcv::DescriptorWrites bufferToImageDescriptorWrites;
+	bufferToImageDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	bufferToImageDescriptorWrites.storageImageWrites = { vkcv::StorageImageDescriptorWrite(1, m_voxelImageIntermediate.getHandle()) };
+	m_corePtr->writeDescriptorSet(m_bufferToImageDescriptorSet, bufferToImageDescriptorWrites);
+
+	// secondary bounce
+	vkcv::ShaderProgram secondaryBounceShader = loadSecondaryBounceShader();
+
+	m_secondaryBounceDescriptorSet = m_corePtr->createDescriptorSet(secondaryBounceShader.getReflectedDescriptors()[0]);
+	m_secondaryBouncePipe = m_corePtr->createComputePipeline(
+		secondaryBounceShader,
+		{ m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).layout });
+
+	vkcv::DescriptorWrites secondaryBounceDescriptorWrites;
+	secondaryBounceDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	secondaryBounceDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(1, m_voxelImageIntermediate.getHandle()) };
+	secondaryBounceDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(2, voxelSampler) };
+	secondaryBounceDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(3, m_voxelImage.getHandle()) };
+	secondaryBounceDescriptorWrites.uniformBufferWrites = { vkcv::UniformBufferDescriptorWrite(4, m_voxelInfoBuffer.getHandle()) };
+	m_corePtr->writeDescriptorSet(m_secondaryBounceDescriptorSet, secondaryBounceDescriptorWrites);
+}
+
+void Voxelization::voxelizeMeshes(
+	vkcv::CommandStreamHandle                       cmdStream,
+	const std::vector<vkcv::Mesh>&                  meshes,
+	const std::vector<glm::mat4>&                   modelMatrices,
+	const std::vector<vkcv::DescriptorSetHandle>&   perMeshDescriptorSets) {
+
+	m_voxelInfoBuffer.fill({ m_voxelInfo });
+
+	const float voxelizationHalfExtent = 0.5f * m_voxelInfo.extent;
+	const glm::mat4 voxelizationProjection = glm::ortho(
+		-voxelizationHalfExtent,
+		voxelizationHalfExtent,
+		-voxelizationHalfExtent,
+		voxelizationHalfExtent,
+		-voxelizationHalfExtent,
+		voxelizationHalfExtent);
+
+	const glm::mat4 voxelizationView = glm::translate(glm::mat4(1.f), -m_voxelInfo.offset);
+	const glm::mat4 voxelizationViewProjection = voxelizationProjection * voxelizationView;
+
+	std::vector<std::array<glm::mat4, 2>> voxelizationMatrices;
+	for (const auto& m : modelMatrices) {
+		voxelizationMatrices.push_back({ voxelizationViewProjection * m, m });
+	}
+
+	const vkcv::PushConstantData voxelizationPushConstantData((void*)voxelizationMatrices.data(), 2 * sizeof(glm::mat4));
+
+	// reset voxels
+	const uint32_t resetVoxelGroupSize = 64;
+	uint32_t resetVoxelDispatchCount[3];
+	resetVoxelDispatchCount[0] = glm::ceil(voxelCount / float(resetVoxelGroupSize));
+	resetVoxelDispatchCount[1] = 1;
+	resetVoxelDispatchCount[2] = 1;
+
+	m_corePtr->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_voxelResetPipe,
+		resetVoxelDispatchCount,
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).vulkanHandle) },
+		vkcv::PushConstantData(&voxelCount, sizeof(voxelCount)));
+	m_corePtr->recordBufferMemoryBarrier(cmdStream, m_voxelBuffer.getHandle());
+
+	// voxelization
+	std::vector<vkcv::DrawcallInfo> drawcalls;
+	for (int i = 0; i < meshes.size(); i++) {
+		drawcalls.push_back(vkcv::DrawcallInfo(
+			meshes[i], 
+			{ 
+				vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelizationDescriptorSet).vulkanHandle),
+				vkcv::DescriptorSetUsage(1, m_corePtr->getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) 
+			},1));
+	}
+
+	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImageIntermediate.getHandle());
+	m_corePtr->recordDrawcallsToCmdStream(
+		cmdStream,
+		m_voxelizationPass,
+		m_voxelizationPipe,
+		voxelizationPushConstantData,
+		drawcalls,
+		{ m_dummyRenderTarget.getHandle() });
+
+	// buffer to image
+	const uint32_t bufferToImageGroupSize[3] = { 4, 4, 4 };
+	uint32_t bufferToImageDispatchCount[3];
+	for (int i = 0; i < 3; i++) {
+		bufferToImageDispatchCount[i] = glm::ceil(voxelResolution / float(bufferToImageGroupSize[i]));
+	}
+
+	m_corePtr->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_bufferToImagePipe,
+		bufferToImageDispatchCount,
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_bufferToImageDescriptorSet).vulkanHandle) },
+		vkcv::PushConstantData(nullptr, 0));
+
+	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImageIntermediate.getHandle());
+
+	// intermediate image mipchain
+	m_voxelImageIntermediate.recordMipChainGeneration(cmdStream);
+	m_corePtr->prepareImageForSampling(cmdStream, m_voxelImageIntermediate.getHandle());
+
+	// secondary bounce
+	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImage.getHandle());
+
+	m_corePtr->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_secondaryBouncePipe,
+		bufferToImageDispatchCount,
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).vulkanHandle) },
+		vkcv::PushConstantData(nullptr, 0));
+	m_voxelImage.recordMipChainGeneration(cmdStream);
+
+	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImage.getHandle());
+
+	// final image mipchain
+	m_voxelImage.recordMipChainGeneration(cmdStream);
+	m_corePtr->prepareImageForSampling(cmdStream, m_voxelImage.getHandle());
+}
+
+void Voxelization::renderVoxelVisualisation(
+	vkcv::CommandStreamHandle               cmdStream, 
+	const glm::mat4&                        viewProjectin,
+	const std::vector<vkcv::ImageHandle>&   renderTargets,
+	uint32_t                                mipLevel) {
+
+	const vkcv::PushConstantData voxelVisualisationPushConstantData((void*)&viewProjectin, sizeof(glm::mat4));
+
+	mipLevel = std::clamp(mipLevel, (uint32_t)0, m_voxelImage.getMipCount()-1);
+
+	// write descriptor set
+	vkcv::DescriptorWrites voxelVisualisationDescriptorWrite;
+	voxelVisualisationDescriptorWrite.storageImageWrites =
+	{ vkcv::StorageImageDescriptorWrite(0, m_voxelImage.getHandle(), mipLevel) };
+	voxelVisualisationDescriptorWrite.uniformBufferWrites =
+	{ vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()) };
+	m_corePtr->writeDescriptorSet(m_visualisationDescriptorSet, voxelVisualisationDescriptorWrite);
+
+	uint32_t drawVoxelCount = voxelCount / exp2(mipLevel);
+
+	const auto drawcall = vkcv::DrawcallInfo(
+		vkcv::Mesh({}, nullptr, drawVoxelCount),
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle) },1);
+
+	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImage.getHandle());
+	m_corePtr->recordDrawcallsToCmdStream(
+		cmdStream,
+		m_visualisationPass,
+		m_visualisationPipe,
+		voxelVisualisationPushConstantData,
+		{ drawcall },
+		renderTargets);
+}
+
+void Voxelization::updateVoxelOffset(const vkcv::camera::Camera& camera) {
+
+	// move voxel offset with camera in voxel sized steps
+	const float voxelSize   = m_voxelInfo.extent / voxelResolution;
+	const float snapSize    = voxelSize * exp2(maxStableMip);
+
+	glm::vec3 voxelVolumeCenter = camera.getPosition() + (1.f / 3.f) * m_voxelInfo.extent * glm::normalize(camera.getFront());
+	voxelVolumeCenter.y         = camera.getPosition().y;
+	m_voxelInfo.offset          = glm::floor(voxelVolumeCenter / snapSize) * snapSize;
+}
+
+void Voxelization::setVoxelExtent(float extent) {
+	m_voxelInfo.extent = extent;
+}
+
+vkcv::ImageHandle Voxelization::getVoxelImageHandle() const {
+	return m_voxelImage.getHandle();
+}
+
+vkcv::BufferHandle Voxelization::getVoxelInfoBufferHandle() const {
+	return m_voxelInfoBuffer.getHandle();
+}
+
+glm::vec3 Voxelization::getVoxelOffset() const{
+	return m_voxelInfo.offset;
+}
+
+float Voxelization::getVoxelExtent() const {
+	return m_voxelInfo.extent;
+}
diff --git a/projects/voxelization/src/Voxelization.hpp b/projects/voxelization/src/Voxelization.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..66c87acb3c13c0d950a28dc33e4084d728da5947
--- /dev/null
+++ b/projects/voxelization/src/Voxelization.hpp
@@ -0,0 +1,82 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include <glm/glm.hpp>
+#include <vkcv/camera/Camera.hpp>
+
+class Voxelization{
+public:
+	struct Dependencies {
+		vkcv::VertexLayout  vertexLayout;
+		vk::Format          colorBufferFormat;
+		vk::Format          depthBufferFormat;
+	};
+	Voxelization(
+		vkcv::Core*         corePtr, 
+		const Dependencies& dependencies, 
+		vkcv::BufferHandle  lightInfoBuffer,
+		vkcv::ImageHandle   shadowMap,
+		vkcv::SamplerHandle shadowSampler,
+		vkcv::SamplerHandle voxelSampler,
+		vkcv::Multisampling msaa);
+
+	void voxelizeMeshes(
+		vkcv::CommandStreamHandle                       cmdStream,
+		const std::vector<vkcv::Mesh>&                  meshes,
+		const std::vector<glm::mat4>&                   modelMatrices,
+		const std::vector<vkcv::DescriptorSetHandle>&   perMeshDescriptorSets);
+
+	void renderVoxelVisualisation(
+		vkcv::CommandStreamHandle               cmdStream,
+		const glm::mat4&                        viewProjectin,
+		const std::vector<vkcv::ImageHandle>&   renderTargets,
+		uint32_t                                mipLevel);
+
+	void updateVoxelOffset(const vkcv::camera::Camera& camera);
+	void setVoxelExtent(float extent);
+
+	vkcv::ImageHandle   getVoxelImageHandle() const;
+	vkcv::BufferHandle  getVoxelInfoBufferHandle() const;
+
+	glm::vec3   getVoxelOffset() const;
+	float       getVoxelExtent() const;
+
+private:
+	vkcv::Core* m_corePtr;
+
+	struct VoxelBufferContent{
+		uint32_t lightEncoded;
+		uint32_t normalEncoded;
+		uint32_t albedoEncoded;
+	};
+
+	vkcv::Image                         m_voxelImageIntermediate;
+	vkcv::Image                         m_voxelImage;
+	vkcv::Buffer<VoxelBufferContent>    m_voxelBuffer;
+
+	vkcv::Image                 m_dummyRenderTarget;
+	vkcv::PassHandle            m_voxelizationPass;
+	vkcv::PipelineHandle        m_voxelizationPipe;
+	vkcv::DescriptorSetHandle   m_voxelizationDescriptorSet;
+
+	vkcv::PipelineHandle        m_voxelResetPipe;
+	vkcv::DescriptorSetHandle   m_voxelResetDescriptorSet;
+
+	vkcv::PipelineHandle        m_bufferToImagePipe;
+	vkcv::DescriptorSetHandle   m_bufferToImageDescriptorSet;
+
+	vkcv::PassHandle            m_visualisationPass;
+	vkcv::PipelineHandle        m_visualisationPipe;
+
+	vkcv::PipelineHandle        m_secondaryBouncePipe;
+	vkcv::DescriptorSetHandle   m_secondaryBounceDescriptorSet;
+
+	vkcv::DescriptorSetHandle   m_visualisationDescriptorSet;
+
+	struct VoxelizationInfo {
+		glm::vec3 offset;
+		float extent;
+	};
+	vkcv::Buffer<VoxelizationInfo> m_voxelInfoBuffer;
+
+	VoxelizationInfo m_voxelInfo;
+};
\ No newline at end of file
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..edc50c554b6c73bd2f06914eba6dd7adf9e43483
--- /dev/null
+++ b/projects/voxelization/src/main.cpp
@@ -0,0 +1,795 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/Logger.hpp>
+#include "Voxelization.hpp"
+#include <glm/glm.hpp>
+#include "vkcv/gui/GUI.hpp"
+#include "ShadowMapping.hpp"
+#include "BloomAndFlares.hpp"
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "Voxelization";
+
+	uint32_t windowWidth = 1280;
+	uint32_t windowHeight = 720;
+	const vkcv::Multisampling   msaa        = vkcv::Multisampling::MSAA4X;
+	const bool                  usingMsaa   = msaa != vkcv::Multisampling::None;
+	
+	vkcv::Window window = vkcv::Window::create(
+		applicationName,
+		windowWidth,
+		windowHeight,
+		true
+	);
+
+	bool    isFullscreen            = false;
+	int     windowedWidthBackup     = windowWidth;
+	int     windowedHeightBackup    = windowHeight;
+	int     windowedPosXBackup;
+	int     windowedPosYBackup;
+    glfwGetWindowPos(window.getWindow(), &windowedPosXBackup, &windowedPosYBackup);
+
+	window.e_key.add([&](int key, int scancode, int action, int mods) {
+		if (key == GLFW_KEY_F11 && action == GLFW_PRESS) {
+			if (isFullscreen) {
+				glfwSetWindowMonitor(
+					window.getWindow(),
+					nullptr,
+					windowedPosXBackup,
+					windowedPosYBackup,
+					windowedWidthBackup,
+					windowedHeightBackup,
+					GLFW_DONT_CARE);
+			}
+			else {
+				windowedWidthBackup     = windowWidth;
+				windowedHeightBackup    = windowHeight;
+
+				glfwGetWindowPos(window.getWindow(), &windowedPosXBackup, &windowedPosYBackup);
+
+				GLFWmonitor*        monitor     = glfwGetPrimaryMonitor();
+				const GLFWvidmode*  videoMode   = glfwGetVideoMode(monitor);
+
+				glfwSetWindowMonitor(
+					window.getWindow(),
+					glfwGetPrimaryMonitor(),
+					0,
+					0,
+					videoMode->width,
+					videoMode->height,
+					videoMode->refreshRate);
+			}
+			isFullscreen = !isFullscreen;
+		}
+	});
+
+	vkcv::camera::CameraManager cameraManager(window);
+	uint32_t camIndex  = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+
+	cameraManager.getCamera(camIndex).setPosition(glm::vec3(0.f, 0.f, 3.f));
+	cameraManager.getCamera(camIndex).setNearFar(0.1f, 30.0f);
+	cameraManager.getCamera(camIndex).setYaw(180.0f);
+	cameraManager.getCamera(camIndex).setFov(glm::radians(37.8));	// fov of a 35mm lens
+	
+	cameraManager.getCamera(camIndex2).setNearFar(0.1f, 30.0f);
+
+	vkcv::Core core = vkcv::Core::create(
+		window,
+		applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
+		{},
+		{ "VK_KHR_swapchain" }
+	);
+
+	vkcv::asset::Scene mesh;
+
+	const char* path = argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf";
+	vkcv::asset::Scene scene;
+	int result = vkcv::asset::loadScene(path, scene);
+
+	if (result == 1) {
+		std::cout << "Scene loading successful!" << std::endl;
+	}
+	else {
+		std::cout << "Scene loading failed: " << result << std::endl;
+		return 1;
+	}
+
+	// build index and vertex buffers
+	assert(!scene.vertexGroups.empty());
+	std::vector<std::vector<uint8_t>> vBuffers;
+	std::vector<std::vector<uint8_t>> iBuffers;
+
+	std::vector<vkcv::VertexBufferBinding> vBufferBindings;
+	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
+	std::vector<vkcv::asset::VertexAttribute> vAttributes;
+
+	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+
+		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
+		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);
+
+		auto& attributes = scene.vertexGroups[i].vertexBuffer.attributes;
+
+		std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
+			return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
+		});
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> vertexBuffers;
+	for (const vkcv::asset::VertexGroup& group : scene.vertexGroups) {
+		vertexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::VERTEX,
+			group.vertexBuffer.data.size()));
+		vertexBuffers.back().fill(group.vertexBuffer.data);
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> indexBuffers;
+	for (const auto& dataBuffer : iBuffers) {
+		indexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::INDEX,
+			dataBuffer.size()));
+		indexBuffers.back().fill(dataBuffer);
+	}
+
+	int vertexBufferIndex = 0;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
+			vAttributes.push_back(attribute);
+			vBufferBindings.push_back(vkcv::VertexBufferBinding(attribute.offset, vertexBuffers[vertexBufferIndex].getVulkanHandle()));
+		}
+		vertexBufferBindings.push_back(vBufferBindings);
+		vBufferBindings.clear();
+		vertexBufferIndex++;
+	}
+
+	const vk::Format colorBufferFormat = vk::Format::eB10G11R11UfloatPack32;
+	const vkcv::AttachmentDescription color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		colorBufferFormat
+	);
+	
+	const vk::Format depthBufferFormat = vk::Format::eD32Sfloat;
+	const vkcv::AttachmentDescription depth_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		depthBufferFormat
+	);
+	
+	// forward shading config
+	vkcv::PassConfig forwardPassDefinition({ color_attachment, depth_attachment }, msaa);
+	vkcv::PassHandle forwardPass = core.createPass(forwardPassDefinition);
+
+	vkcv::shader::GLSLCompiler compiler;
+
+	vkcv::ShaderProgram forwardProgram;
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"), 
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		forwardProgram.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		forwardProgram.addShader(shaderStage, path);
+	});
+
+	const std::vector<vkcv::VertexAttachment> vertexAttachments = forwardProgram.getVertexAttachments();
+
+	std::vector<vkcv::VertexBinding> vertexBindings;
+	for (size_t i = 0; i < vertexAttachments.size(); i++) {
+		vertexBindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+	}
+	const vkcv::VertexLayout vertexLayout (vertexBindings);
+
+	vkcv::DescriptorSetHandle forwardShadingDescriptorSet = 
+		core.createDescriptorSet({ forwardProgram.getReflectedDescriptors()[0] });
+
+	// depth prepass config
+	vkcv::ShaderProgram depthPrepassShader;
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/depthPrepass.vert"),
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		depthPrepassShader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/depthPrepass.frag"),
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		depthPrepassShader.addShader(shaderStage, path);
+	});
+
+	const std::vector<vkcv::VertexAttachment> prepassVertexAttachments = depthPrepassShader.getVertexAttachments();
+
+	std::vector<vkcv::VertexBinding> prepassVertexBindings;
+	for (size_t i = 0; i < prepassVertexAttachments.size(); i++) {
+		prepassVertexBindings.push_back(vkcv::VertexBinding(i, { prepassVertexAttachments[i] }));
+	}
+	const vkcv::VertexLayout prepassVertexLayout(prepassVertexBindings);
+
+	const vkcv::AttachmentDescription prepassAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		depthBufferFormat);
+
+	vkcv::PassConfig prepassPassDefinition({ prepassAttachment }, msaa);
+	vkcv::PassHandle prepassPass = core.createPass(prepassPassDefinition);
+
+	// create descriptor sets
+	vkcv::SamplerHandle colorSampler = core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::REPEAT
+	);
+
+	std::vector<vkcv::DescriptorSetHandle> materialDescriptorSets;
+	std::vector<vkcv::Image> sceneImages;
+
+	for (const auto& material : scene.materials) {
+		int albedoIndex     = material.baseColor;
+		int normalIndex     = material.normal;
+		int specularIndex   = material.metalRough;
+
+		if (albedoIndex < 0) {
+			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks albedo");
+			albedoIndex = 0;
+		}
+		if (normalIndex < 0) {
+			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks normal");
+			normalIndex = 0;
+		}
+		if (specularIndex < 0) {
+			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks specular");
+			specularIndex = 0;
+		}
+
+		materialDescriptorSets.push_back(core.createDescriptorSet(forwardProgram.getReflectedDescriptors()[1]));
+
+		vkcv::asset::Texture& albedoTexture     = scene.textures[albedoIndex];
+		vkcv::asset::Texture& normalTexture     = scene.textures[normalIndex];
+		vkcv::asset::Texture& specularTexture   = scene.textures[specularIndex];
+
+		// albedo texture
+		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, albedoTexture.w, albedoTexture.h, 1, true));
+		sceneImages.back().fill(albedoTexture.data.data());
+		sceneImages.back().generateMipChainImmediate();
+		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+		const vkcv::ImageHandle albedoHandle = sceneImages.back().getHandle();
+
+		// normal texture
+		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Unorm, normalTexture.w, normalTexture.h, 1, true));
+		sceneImages.back().fill(normalTexture.data.data());
+		sceneImages.back().generateMipChainImmediate();
+		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+		const vkcv::ImageHandle normalHandle = sceneImages.back().getHandle();
+
+		// specular texture
+		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Unorm, specularTexture.w, specularTexture.h, 1, true));
+		sceneImages.back().fill(specularTexture.data.data());
+		sceneImages.back().generateMipChainImmediate();
+		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+		const vkcv::ImageHandle specularHandle = sceneImages.back().getHandle();
+
+		vkcv::DescriptorWrites setWrites;
+		setWrites.sampledImageWrites = {
+			vkcv::SampledImageDescriptorWrite(0, albedoHandle),
+			vkcv::SampledImageDescriptorWrite(2, normalHandle),
+			vkcv::SampledImageDescriptorWrite(3, specularHandle)
+		};
+		setWrites.samplerWrites = {
+			vkcv::SamplerDescriptorWrite(1, colorSampler),
+		};
+		core.writeDescriptorSet(materialDescriptorSets.back(), setWrites);
+	}
+
+	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		perMeshDescriptorSets.push_back(materialDescriptorSets[vertexGroup.materialIndex]);
+	}
+
+	// prepass pipeline
+	vkcv::DescriptorSetHandle prepassDescriptorSet = core.createDescriptorSet(std::vector<vkcv::DescriptorBinding>());
+
+	vkcv::PipelineConfig prepassPipelineConfig{
+		depthPrepassShader,
+		windowWidth,
+		windowHeight,
+		prepassPass,
+		vertexLayout,
+		{ 
+			core.getDescriptorSet(prepassDescriptorSet).layout,
+			core.getDescriptorSet(perMeshDescriptorSets[0]).layout },
+		true };
+	prepassPipelineConfig.m_culling         = vkcv::CullMode::Back;
+	prepassPipelineConfig.m_multisampling   = msaa;
+	prepassPipelineConfig.m_depthTest       = vkcv::DepthTest::LessEqual;
+	prepassPipelineConfig.m_alphaToCoverage = true;
+
+	vkcv::PipelineHandle prepassPipeline = core.createGraphicsPipeline(prepassPipelineConfig);
+
+	// forward pipeline
+	vkcv::PipelineConfig forwardPipelineConfig {
+		forwardProgram,
+		windowWidth,
+		windowHeight,
+		forwardPass,
+		vertexLayout,
+		{	
+			core.getDescriptorSet(forwardShadingDescriptorSet).layout, 
+			core.getDescriptorSet(perMeshDescriptorSets[0]).layout },
+		true
+	};
+    forwardPipelineConfig.m_culling         = vkcv::CullMode::Back;
+	forwardPipelineConfig.m_multisampling   = msaa;
+	forwardPipelineConfig.m_depthTest       = vkcv::DepthTest::Equal;
+	forwardPipelineConfig.m_depthWrite      = false;
+	
+	vkcv::PipelineHandle forwardPipeline = core.createGraphicsPipeline(forwardPipelineConfig);
+	
+	if (!forwardPipeline) {
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	// sky
+	struct SkySettings {
+		glm::vec3   color;
+		float       strength;
+	};
+	SkySettings skySettings;
+	skySettings.color       = glm::vec3(0.15, 0.65, 1);
+	skySettings.strength    = 5;
+
+	const vkcv::AttachmentDescription skyColorAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		colorBufferFormat);
+
+	const vkcv::AttachmentDescription skyDepthAttachments(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		depthBufferFormat);
+
+	vkcv::PassConfig skyPassConfig({ skyColorAttachment, skyDepthAttachments }, msaa);
+	vkcv::PassHandle skyPass = core.createPass(skyPassConfig);
+
+	vkcv::ShaderProgram skyShader;
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/sky.vert"),
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		skyShader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/sky.frag"),
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		skyShader.addShader(shaderStage, path);
+	});
+
+	vkcv::PipelineConfig skyPipeConfig;
+	skyPipeConfig.m_ShaderProgram       = skyShader;
+	skyPipeConfig.m_Width               = windowWidth;
+	skyPipeConfig.m_Height              = windowHeight;
+	skyPipeConfig.m_PassHandle          = skyPass;
+	skyPipeConfig.m_VertexLayout        = vkcv::VertexLayout();
+	skyPipeConfig.m_DescriptorLayouts   = {};
+	skyPipeConfig.m_UseDynamicViewport  = true;
+	skyPipeConfig.m_multisampling       = msaa;
+	skyPipeConfig.m_depthWrite          = false;
+
+	vkcv::PipelineHandle skyPipe = core.createGraphicsPipeline(skyPipeConfig);
+
+	// render targets
+	vkcv::ImageHandle depthBuffer           = core.createImage(depthBufferFormat, windowWidth, windowHeight, 1, false, false, false, msaa).getHandle();
+
+    const bool colorBufferRequiresStorage   = !usingMsaa;
+	vkcv::ImageHandle colorBuffer           = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, colorBufferRequiresStorage, true, msaa).getHandle();
+
+	vkcv::ImageHandle resolvedColorBuffer;
+	if (usingMsaa) {
+		resolvedColorBuffer = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
+	}
+	else {
+		resolvedColorBuffer = colorBuffer;
+	}
+
+	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+	bool renderVoxelVis = false;
+	window.e_key.add([&renderVoxelVis](int key ,int scancode, int action, int mods) {
+		if (key == GLFW_KEY_V && action == GLFW_PRESS) {
+			renderVoxelVis = !renderVoxelVis;
+		}
+	});
+
+	bool renderUI = true;
+	window.e_key.add([&renderUI](int key, int scancode, int action, int mods) {
+		if (key == GLFW_KEY_I && action == GLFW_PRESS) {
+			renderUI = !renderUI;
+		}
+	});
+
+	// tonemapping compute shader
+	vkcv::ShaderProgram tonemappingProgram;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/tonemapping.comp", 
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		tonemappingProgram.addShader(shaderStage, path);
+	});
+	vkcv::DescriptorSetHandle tonemappingDescriptorSet = core.createDescriptorSet(
+		tonemappingProgram.getReflectedDescriptors()[0]);
+	vkcv::PipelineHandle tonemappingPipeline = core.createComputePipeline(
+		tonemappingProgram,
+		{ core.getDescriptorSet(tonemappingDescriptorSet).layout });
+
+	// resolve compute shader
+	vkcv::ShaderProgram resolveProgram;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/msaa4XResolve.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		resolveProgram.addShader(shaderStage, path);
+	});
+	vkcv::DescriptorSetHandle resolveDescriptorSet = core.createDescriptorSet(
+		resolveProgram.getReflectedDescriptors()[0]);
+	vkcv::PipelineHandle resolvePipeline = core.createComputePipeline(
+		resolveProgram,
+		{ core.getDescriptorSet(resolveDescriptorSet).layout });
+
+	vkcv::SamplerHandle resolveSampler = core.createSampler(
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerMipmapMode::NEAREST,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+
+	// model matrices per mesh
+	std::vector<glm::mat4> modelMatrices;
+	modelMatrices.resize(scene.vertexGroups.size(), glm::mat4(1.f));
+	for (const auto& mesh : scene.meshes) {
+		const glm::mat4 m = *reinterpret_cast<const glm::mat4*>(&mesh.modelMatrix[0]);
+		for (const auto& vertexGroupIndex : mesh.vertexGroups) {
+			modelMatrices[vertexGroupIndex] = m;
+		}
+	}
+
+	// prepare meshes
+	std::vector<vkcv::Mesh> meshes;
+	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+		vkcv::Mesh mesh(vertexBufferBindings[i], indexBuffers[i].getVulkanHandle(), scene.vertexGroups[i].numIndices);
+		meshes.push_back(mesh);
+	}
+
+	std::vector<vkcv::DrawcallInfo> drawcalls;
+	std::vector<vkcv::DrawcallInfo> prepassDrawcalls;
+	for (int i = 0; i < meshes.size(); i++) {
+
+		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
+			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
+			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+		prepassDrawcalls.push_back(vkcv::DrawcallInfo(meshes[i], {
+			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(prepassDescriptorSet).vulkanHandle),
+			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+	}
+
+	vkcv::SamplerHandle voxelSampler = core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+
+	ShadowMapping shadowMapping(&core, vertexLayout);
+
+	Voxelization::Dependencies voxelDependencies;
+	voxelDependencies.colorBufferFormat = colorBufferFormat;
+	voxelDependencies.depthBufferFormat = depthBufferFormat;
+	voxelDependencies.vertexLayout = vertexLayout;
+	Voxelization voxelization(
+		&core,
+		voxelDependencies,
+		shadowMapping.getLightInfoBuffer(),
+		shadowMapping.getShadowMap(),
+		shadowMapping.getShadowSampler(),
+		voxelSampler,
+		msaa);
+
+	BloomAndFlares bloomFlares(&core, colorBufferFormat, windowWidth, windowHeight);
+
+	window.e_key.add([&](int key, int scancode, int action, int mods) {
+		if (key == GLFW_KEY_R && action == GLFW_PRESS) {
+			bloomFlares = BloomAndFlares(&core, colorBufferFormat, windowWidth, windowHeight);
+		}
+	});
+
+	vkcv::Buffer<glm::vec3> cameraPosBuffer = core.createBuffer<glm::vec3>(vkcv::BufferType::UNIFORM, 1);
+
+	struct VolumetricSettings {
+		glm::vec3   scatteringCoefficient;
+		float       ambientLight;
+		glm::vec3   absorptionCoefficient;
+	};
+	vkcv::Buffer<VolumetricSettings> volumetricSettingsBuffer
+		= core.createBuffer<VolumetricSettings>(vkcv::BufferType::UNIFORM ,1);
+
+	// write forward pass descriptor set
+	vkcv::DescriptorWrites forwardDescriptorWrites;
+	forwardDescriptorWrites.uniformBufferWrites = {
+		vkcv::UniformBufferDescriptorWrite(0, shadowMapping.getLightInfoBuffer()),
+		vkcv::UniformBufferDescriptorWrite(3, cameraPosBuffer.getHandle()),
+		vkcv::UniformBufferDescriptorWrite(6, voxelization.getVoxelInfoBufferHandle()),
+		vkcv::UniformBufferDescriptorWrite(7, volumetricSettingsBuffer.getHandle())};
+	forwardDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(1, shadowMapping.getShadowMap()),
+		vkcv::SampledImageDescriptorWrite(4, voxelization.getVoxelImageHandle()) };
+	forwardDescriptorWrites.samplerWrites = { 
+		vkcv::SamplerDescriptorWrite(2, shadowMapping.getShadowSampler()),
+		vkcv::SamplerDescriptorWrite(5, voxelSampler) };
+	core.writeDescriptorSet(forwardShadingDescriptorSet, forwardDescriptorWrites);
+
+	vkcv::gui::GUI gui(core, window);
+
+	glm::vec2   lightAnglesDegree               = glm::vec2(90.f, 0.f);
+	glm::vec3   lightColor                      = glm::vec3(1);
+	float       lightStrength                   = 25.f;
+	float       maxShadowDistance               = 30.f;
+
+	int     voxelVisualisationMip   = 0;
+	float   voxelizationExtent      = 35.f;
+
+	bool msaaCustomResolve = true;
+
+	glm::vec3   scatteringColor     = glm::vec3(1);
+	float       scatteringDensity   = 0.005;
+	glm::vec3   absorptionColor     = glm::vec3(1);
+	float       absorptionDensity   = 0.005;
+	float       volumetricAmbient   = 0.2;
+
+	auto start = std::chrono::system_clock::now();
+	const auto appStartTime = start;
+	while (window.isWindowOpen()) {
+		vkcv::Window::pollEvents();
+
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+			continue;
+		}
+
+		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
+			depthBuffer         = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight, 1, false, false, false, msaa).getHandle();
+			colorBuffer         = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, colorBufferRequiresStorage, true, msaa).getHandle();
+
+			if (usingMsaa) {
+				resolvedColorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
+			}
+			else {
+				resolvedColorBuffer = colorBuffer;
+			}
+
+			windowWidth = swapchainWidth;
+			windowHeight = swapchainHeight;
+
+			bloomFlares.updateImageDimensions(windowWidth, windowHeight);
+		}
+
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
+
+		// update descriptor sets which use swapchain image
+		vkcv::DescriptorWrites tonemappingDescriptorWrites;
+		tonemappingDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, resolvedColorBuffer) };
+		tonemappingDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, colorSampler) };
+		tonemappingDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapchainInput) };
+
+		core.writeDescriptorSet(tonemappingDescriptorSet, tonemappingDescriptorWrites);
+
+		// update resolve descriptor, color images could be changed
+		vkcv::DescriptorWrites resolveDescriptorWrites;
+		resolveDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, colorBuffer) };
+		resolveDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, resolveSampler) };
+		resolveDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, resolvedColorBuffer) };
+		core.writeDescriptorSet(resolveDescriptorSet, resolveDescriptorWrites);
+
+		start = end;
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+		cameraPosBuffer.fill({ cameraManager.getActiveCamera().getPosition() });
+
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		voxelization.updateVoxelOffset(cameraManager.getActiveCamera());
+
+		// shadow map
+		glm::vec2 lightAngleRadian = glm::radians(lightAnglesDegree);
+		shadowMapping.recordShadowMapRendering(
+			cmdStream,
+			lightAngleRadian,
+			lightColor,
+			lightStrength,
+			maxShadowDistance,
+			meshes,
+			modelMatrices,
+			cameraManager.getActiveCamera(),
+			voxelization.getVoxelOffset(),
+			voxelization.getVoxelExtent());
+
+		// voxelization
+		voxelization.setVoxelExtent(voxelizationExtent);
+		voxelization.voxelizeMeshes(
+			cmdStream,
+			meshes, 
+			modelMatrices,
+			perMeshDescriptorSets);
+
+		// depth prepass
+		const glm::mat4 viewProjectionCamera = cameraManager.getActiveCamera().getMVP();
+
+		std::vector<glm::mat4> prepassMatrices;
+		for (const auto& m : modelMatrices) {
+			prepassMatrices.push_back(viewProjectionCamera * m);
+		}
+
+		const vkcv::PushConstantData            prepassPushConstantData((void*)prepassMatrices.data(), sizeof(glm::mat4));
+		const std::vector<vkcv::ImageHandle>    prepassRenderTargets = { depthBuffer };
+
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			prepassPass,
+			prepassPipeline,
+			prepassPushConstantData,
+			prepassDrawcalls,
+			prepassRenderTargets);
+
+		core.recordImageMemoryBarrier(cmdStream, depthBuffer);
+
+		// main pass
+		std::vector<std::array<glm::mat4, 2>> mainPassMatrices;
+		for (const auto& m : modelMatrices) {
+			mainPassMatrices.push_back({ viewProjectionCamera * m, m });
+		}
+
+		VolumetricSettings volumeSettings;
+		volumeSettings.scatteringCoefficient    = scatteringColor * scatteringDensity;
+		volumeSettings.absorptionCoefficient    = absorptionColor * absorptionDensity;
+		volumeSettings.ambientLight             = volumetricAmbient;
+		volumetricSettingsBuffer.fill({ volumeSettings });
+
+		const vkcv::PushConstantData            pushConstantData((void*)mainPassMatrices.data(), 2 * sizeof(glm::mat4));
+		const std::vector<vkcv::ImageHandle>    renderTargets = { colorBuffer, depthBuffer };
+
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			forwardPass,
+			forwardPipeline,
+			pushConstantData,
+			drawcalls,
+			renderTargets);
+
+		if (renderVoxelVis) {
+			voxelization.renderVoxelVisualisation(cmdStream, viewProjectionCamera, renderTargets, voxelVisualisationMip);
+		}
+
+		// sky
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			skyPass,
+			skyPipe,
+			vkcv::PushConstantData((void*)&skySettings, sizeof(skySettings)),
+			{ vkcv::DrawcallInfo(vkcv::Mesh({}, nullptr, 3), {}) },
+			renderTargets);
+
+		const uint32_t fullscreenLocalGroupSize = 8;
+		const uint32_t fulsscreenDispatchCount[3] = {
+			static_cast<uint32_t>(glm::ceil(windowWidth  / static_cast<float>(fullscreenLocalGroupSize))),
+			static_cast<uint32_t>(glm::ceil(windowHeight / static_cast<float>(fullscreenLocalGroupSize))),
+			1
+		};
+
+		if (usingMsaa) {
+			if (msaaCustomResolve) {
+
+				core.prepareImageForSampling(cmdStream, colorBuffer);
+				core.prepareImageForStorage(cmdStream, resolvedColorBuffer);
+
+				assert(msaa == vkcv::Multisampling::MSAA4X);	// shaders is written for msaa 4x
+				core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					resolvePipeline,
+					fulsscreenDispatchCount,
+					{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(resolveDescriptorSet).vulkanHandle) },
+					vkcv::PushConstantData(nullptr, 0));
+
+				core.recordImageMemoryBarrier(cmdStream, resolvedColorBuffer);
+			}
+			else {
+				core.resolveMSAAImage(cmdStream, colorBuffer, resolvedColorBuffer);
+			}
+		}
+
+		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, windowWidth, windowHeight, 
+			glm::normalize(cameraManager.getActiveCamera().getFront()));
+
+		core.prepareImageForStorage(cmdStream, swapchainInput);
+		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);
+
+		auto timeSinceStart = std::chrono::duration_cast<std::chrono::microseconds>(end - appStartTime);
+		float timeF         = static_cast<float>(timeSinceStart.count()) * 0.01;
+
+		core.recordComputeDispatchToCmdStream(
+			cmdStream, 
+			tonemappingPipeline, 
+			fulsscreenDispatchCount,
+			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptorSet).vulkanHandle) },
+			vkcv::PushConstantData(&timeF, sizeof(timeF)));
+
+		// present and end
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+
+		// draw UI
+		gui.beginGUI();
+
+		if (renderUI) {
+			ImGui::Begin("Settings");
+
+			ImGui::Checkbox("MSAA custom resolve", &msaaCustomResolve);
+
+			ImGui::DragFloat2("Light angles", &lightAnglesDegree.x);
+			ImGui::ColorEdit3("Sun color", &lightColor.x);
+			ImGui::DragFloat("Sun strength", &lightStrength);
+			ImGui::DragFloat("Max shadow distance", &maxShadowDistance);
+			maxShadowDistance = std::max(maxShadowDistance, 1.f);
+
+			ImGui::ColorEdit3("Sky color", &skySettings.color.x);
+			ImGui::DragFloat("Sky strength", &skySettings.strength, 0.1);
+
+			ImGui::Checkbox("Draw voxel visualisation", &renderVoxelVis);
+			ImGui::SliderInt("Visualisation mip", &voxelVisualisationMip, 0, 7);
+			ImGui::DragFloat("Voxelization extent", &voxelizationExtent, 1.f, 0.f);
+			voxelizationExtent = std::max(voxelizationExtent, 1.f);
+			voxelVisualisationMip = std::max(voxelVisualisationMip, 0);
+
+			ImGui::ColorEdit3("Scattering color", &scatteringColor.x);
+			ImGui::DragFloat("Scattering density", &scatteringDensity, 0.0001);
+			ImGui::ColorEdit3("Absorption color", &absorptionColor.x);
+			ImGui::DragFloat("Absorption density", &absorptionDensity, 0.0001);
+			ImGui::DragFloat("Volumetric ambient", &volumetricAmbient, 0.002);
+
+			if (ImGui::Button("Reload forward pass")) {
+
+				vkcv::ShaderProgram newForwardProgram;
+				compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
+					[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+					newForwardProgram.addShader(shaderStage, path);
+				});
+				compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+					[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+					newForwardProgram.addShader(shaderStage, path);
+				});
+				forwardPipelineConfig.m_ShaderProgram = newForwardProgram;
+				vkcv::PipelineHandle newPipeline = core.createGraphicsPipeline(forwardPipelineConfig);
+
+				if (newPipeline) {
+					forwardPipeline = newPipeline;
+				}
+			}
+			if (ImGui::Button("Reload tonemapping")) {
+
+				vkcv::ShaderProgram newProgram;
+				compiler.compile(vkcv::ShaderStage::COMPUTE, std::filesystem::path("resources/shaders/tonemapping.comp"),
+					[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+					newProgram.addShader(shaderStage, path);
+				});
+				vkcv::PipelineHandle newPipeline = core.createComputePipeline(
+					newProgram,
+					{ core.getDescriptorSet(tonemappingDescriptorSet).layout });
+
+				if (newPipeline) {
+					tonemappingPipeline = newPipeline;
+				}
+			}
+			ImGui::End();
+		}
+
+		gui.endGUI();
+
+		core.endFrame();
+	}
+	
+	return 0;
+}
diff --git a/src/vkcv/BufferManager.cpp b/src/vkcv/BufferManager.cpp
index 6d494c4ec90726d46039007607464378624f1c75..aec96411c5d9e07f200b24fbdcf9fa69e2af53d5 100644
--- a/src/vkcv/BufferManager.cpp
+++ b/src/vkcv/BufferManager.cpp
@@ -5,6 +5,7 @@
 
 #include "vkcv/BufferManager.hpp"
 #include "vkcv/Core.hpp"
+#include <vkcv/Logger.hpp>
 
 namespace vkcv {
 	
@@ -158,7 +159,7 @@ namespace vkcv {
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Transfer;
 		
-		core->recordAndSubmitCommands(
+		core->recordAndSubmitCommandsImmediate(
 				submitInfo,
 				[&info, &mapped_size](const vk::CommandBuffer& commandBuffer) {
 					const vk::BufferCopy region (
@@ -335,4 +336,33 @@ namespace vkcv {
 		}
 	}
 
+	void BufferManager ::recordBufferMemoryBarrier(const BufferHandle& handle, vk::CommandBuffer cmdBuffer) {
+
+		const uint64_t id = handle.getId();
+
+		if (id >= m_buffers.size()) {
+			vkcv_log(vkcv::LogLevel::ERROR, "Invalid buffer handle");
+			return;
+		}
+
+		auto& buffer = m_buffers[id];
+		
+		vk::BufferMemoryBarrier memoryBarrier(
+			vk::AccessFlagBits::eMemoryWrite, 
+			vk::AccessFlagBits::eMemoryRead,
+			0,
+			0,
+			buffer.m_handle,
+			0,
+			buffer.m_size);
+
+		cmdBuffer.pipelineBarrier(
+			vk::PipelineStageFlagBits::eTopOfPipe,
+			vk::PipelineStageFlagBits::eBottomOfPipe,
+			{},
+			nullptr,
+			memoryBarrier,
+			nullptr);
+	}
+
 }
diff --git a/src/vkcv/Context.cpp b/src/vkcv/Context.cpp
index b53a1a2c2db1008e7c69c880ef1c5a608d879021..e23213b41a3c9a289b679652b66bbe2e75cf0340 100644
--- a/src/vkcv/Context.cpp
+++ b/src/vkcv/Context.cpp
@@ -275,7 +275,14 @@ namespace vkcv
 		deviceCreateInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
 		deviceCreateInfo.ppEnabledLayerNames = validationLayers.data();
 #endif
-		
+
+		// FIXME: check if device feature is supported
+		vk::PhysicalDeviceFeatures deviceFeatures;
+		deviceFeatures.fragmentStoresAndAtomics = true;
+		deviceFeatures.geometryShader = true;
+		deviceFeatures.depthClamp = true;
+		deviceCreateInfo.pEnabledFeatures = &deviceFeatures;
+
 		// Ablauf
 		// qCreateInfos erstellen --> braucht das Device
 		// device erstellen
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index 44e7111e1f4941ef2f0f8114ac788d7db4a13b5a..352a1cf62eabe55ce1bbf2f53a6b5a4bd6e91753 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -15,11 +15,40 @@
 #include "DescriptorManager.hpp"
 #include "ImageLayoutTransitions.hpp"
 #include "vkcv/CommandStreamManager.hpp"
-
+#include <cmath>
 #include "vkcv/Logger.hpp"
 
 namespace vkcv
 {
+	
+	static std::vector<vk::ImageView> createSwapchainImageViews( Context &context, const std::vector<vk::Image>& images,
+																 vk::Format format){
+		std::vector<vk::ImageView> imageViews;
+		imageViews.reserve( images.size() );
+		//here can be swizzled with vk::ComponentSwizzle if needed
+		vk::ComponentMapping componentMapping(
+				vk::ComponentSwizzle::eR,
+				vk::ComponentSwizzle::eG,
+				vk::ComponentSwizzle::eB,
+				vk::ComponentSwizzle::eA );
+		
+		vk::ImageSubresourceRange subResourceRange( vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 );
+		
+		for ( auto image : images )
+		{
+			vk::ImageViewCreateInfo imageViewCreateInfo(
+					vk::ImageViewCreateFlags(),
+					image,
+					vk::ImageViewType::e2D,
+					format,
+					componentMapping,
+					subResourceRange);
+			
+			imageViews.push_back(context.getDevice().createImageView(imageViewCreateInfo));
+		}
+		
+		return imageViews;
+	}
 
     Core Core::create(Window &window,
                       const char *applicationName,
@@ -35,10 +64,10 @@ namespace vkcv
         		deviceExtensions
 		);
 
-        SwapChain swapChain = SwapChain::create(window, context);
-
-        std::vector<vk::ImageView> imageViews;
-        imageViews = createImageViews( context, swapChain);
+        Swapchain swapChain = Swapchain::create(window, context);
+	
+		const auto swapchainImages = context.getDevice().getSwapchainImagesKHR(swapChain.getSwapchain());
+		const auto swapchainImageViews = createSwapchainImageViews( context, swapchainImages, swapChain.getFormat());
 
         const auto& queueManager = context.getQueueManager();
         
@@ -47,20 +76,23 @@ namespace vkcv
 		const auto						commandResources		= createCommandResources(context.getDevice(), queueFamilySet);
 		const auto						defaultSyncResources	= createSyncResources(context.getDevice());
 
-        return Core(std::move(context) , window, swapChain, imageViews, commandResources, defaultSyncResources);
+        return Core(std::move(context) , window, swapChain, swapchainImageViews, commandResources, defaultSyncResources);
     }
 
     const Context &Core::getContext() const
     {
         return m_Context;
     }
+    
+    const Swapchain& Core::getSwapchain() const {
+    	return m_swapchain;
+    }
 
-    Core::Core(Context &&context, Window &window, const SwapChain& swapChain,  std::vector<vk::ImageView> imageViews,
+    Core::Core(Context &&context, Window &window, const Swapchain& swapChain,  std::vector<vk::ImageView> swapchainImageViews,
         const CommandResources& commandResources, const SyncResources& syncResources) noexcept :
             m_Context(std::move(context)),
             m_window(window),
             m_swapchain(swapChain),
-            m_swapchainImageViews(imageViews),
             m_PassManager{std::make_unique<PassManager>(m_Context.m_Device)},
             m_PipelineManager{std::make_unique<PipelineManager>(m_Context.m_Device)},
             m_DescriptorManager(std::make_unique<DescriptorManager>(m_Context.m_Device)),
@@ -76,20 +108,24 @@ namespace vkcv
 		m_CommandStreamManager->init(this);
 
 		m_ImageManager->m_core = this;
-
-		e_resizeHandle = window.e_resize.add( [&](int width, int height) {
+		
+		e_resizeHandle = m_window.e_resize.add( [&](int width, int height) {
 			m_swapchain.signalSwapchainRecreation();
 		});
 
-		m_swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());
-		m_swapchainImageLayouts.resize(m_swapchainImages.size(), vk::ImageLayout::eUndefined);
+		const auto swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());
+		m_ImageManager->setSwapchainImages(
+			swapchainImages, 
+			swapchainImageViews, 
+			swapChain.getExtent().width,
+			swapChain.getExtent().height,
+			swapChain.getFormat());
 	}
 
 	Core::~Core() noexcept {
+    	m_window.e_resize.remove(e_resizeHandle);
+    	
 		m_Context.getDevice().waitIdle();
-		for (auto image : m_swapchainImageViews) {
-			m_Context.m_Device.destroyImageView(image);
-		}
 
 		destroyCommandResources(m_Context.getDevice(), m_CommandResources);
 		destroySyncResources(m_Context.getDevice(), m_SyncResources);
@@ -110,7 +146,6 @@ namespace vkcv
         return m_PipelineManager->createComputePipeline(shaderProgram, descriptorSetLayouts);
     }
 
-
     PassHandle Core::createPass(const PassConfig &config)
     {
         return m_PassManager->createPass(config);
@@ -118,7 +153,6 @@ namespace vkcv
 
 	Result Core::acquireSwapchainImage() {
     	uint32_t imageIndex;
-		
     	vk::Result result;
     	
 		try {
@@ -131,11 +165,18 @@ namespace vkcv
 			);
 		} catch (vk::OutOfDateKHRError e) {
 			result = vk::Result::eErrorOutOfDateKHR;
+		} catch (vk::DeviceLostError e) {
+			result = vk::Result::eErrorDeviceLost;
 		}
 		
-		if (result != vk::Result::eSuccess) {
+		if ((result != vk::Result::eSuccess) &&
+			(result != vk::Result::eSuboptimalKHR)) {
 			vkcv_log(LogLevel::ERROR, "%s", vk::to_string(result).c_str());
 			return Result::ERROR;
+		} else
+		if (result == vk::Result::eSuboptimalKHR) {
+			vkcv_log(LogLevel::WARNING, "Acquired image is suboptimal");
+			m_swapchain.signalSwapchainRecreation();
 		}
 		
 		m_currentSwapchainImageIndex = imageIndex;
@@ -145,16 +186,33 @@ namespace vkcv
 	bool Core::beginFrame(uint32_t& width, uint32_t& height) {
 		if (m_swapchain.shouldUpdateSwapchain()) {
 			m_Context.getDevice().waitIdle();
+
+			m_swapchain.updateSwapchain(m_Context, m_window);
 			
-			for (auto image : m_swapchainImageViews)
-				m_Context.m_Device.destroyImageView(image);
+			if (!m_swapchain.getSwapchain()) {
+				return false;
+			}
 			
-			m_swapchain.updateSwapchain(m_Context, m_window);
-			m_swapchainImageViews = createImageViews(m_Context, m_swapchain);
-			m_swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());
+			const auto swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());
+			const auto swapchainViews = createSwapchainImageViews(m_Context, swapchainImages, m_swapchain.getFormat());
+			
+			const auto& extent = m_swapchain.getExtent();
 
-			m_swapchainImageLayouts.clear();
-			m_swapchainImageLayouts.resize(m_swapchainImages.size(), vk::ImageLayout::eUndefined);
+			m_ImageManager->setSwapchainImages(
+					swapchainImages,
+					swapchainViews,
+					extent.width, extent.height,
+					m_swapchain.getFormat()
+			);
+		}
+		
+		const auto& extent = m_swapchain.getExtent();
+		
+		width = extent.width;
+		height = extent.height;
+		
+		if ((width < MIN_SWAPCHAIN_SIZE) || (height < MIN_SWAPCHAIN_SIZE)) {
+			return false;
 		}
 		
     	if (acquireSwapchainImage() != Result::SUCCESS) {
@@ -165,11 +223,8 @@ namespace vkcv
 		
 		m_Context.getDevice().waitIdle(); // TODO: this is a sin against graphics programming, but its getting late - Alex
 		
-		const auto& extent = m_swapchain.getExtent();
-		
-		width = extent.width;
-		height = extent.height;
-		
+		m_ImageManager->setCurrentSwapchainImageIndex(m_currentSwapchainImageIndex);
+
 		return (m_currentSwapchainImageIndex != std::numeric_limits<uint32_t>::max());
 	}
 
@@ -212,27 +267,19 @@ namespace vkcv
 		const vk::PipelineLayout pipelineLayout = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
 		const vk::Rect2D renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));
 
-		const vk::ImageView swapchainImageView = m_swapchainImageViews[m_currentSwapchainImageIndex];
-
 		std::vector<vk::ImageView> attachmentsViews;
 		for (const ImageHandle handle : renderTargets) {
 			vk::ImageView targetHandle;
 			const auto cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
-			if (handle.isSwapchainImage()) {
-				recordSwapchainImageLayoutTransition(cmdBuffer, vk::ImageLayout::eColorAttachmentOptimal);
-				targetHandle = m_swapchainImageViews[m_currentSwapchainImageIndex];
-			}
-			else {
-				targetHandle = m_ImageManager->getVulkanImageView(handle);
-				const bool isDepthImage = isDepthFormat(m_ImageManager->getImageFormat(handle));
-				const vk::ImageLayout targetLayout = 
-					isDepthFormat ? vk::ImageLayout::eDepthStencilAttachmentOptimal : vk::ImageLayout::eColorAttachmentOptimal;
-				m_ImageManager->recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
-			}
+
+			targetHandle = m_ImageManager->getVulkanImageView(handle);
+			const bool isDepthImage = isDepthFormat(m_ImageManager->getImageFormat(handle));
+			const vk::ImageLayout targetLayout = 
+				isDepthImage ? vk::ImageLayout::eDepthStencilAttachmentOptimal : vk::ImageLayout::eColorAttachmentOptimal;
+			m_ImageManager->recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
 			attachmentsViews.push_back(targetHandle);
 		}
 		
-		vk::Framebuffer framebuffer = nullptr;
         const vk::FramebufferCreateInfo createInfo(
             {},
             renderpass,
@@ -240,16 +287,21 @@ namespace vkcv
             attachmentsViews.data(),
             width,
             height,
-            1);
-        if(m_Context.m_Device.createFramebuffer(&createInfo, nullptr, &framebuffer) != vk::Result::eSuccess)
-        {
+            1
+		);
+		
+		vk::Framebuffer framebuffer = m_Context.m_Device.createFramebuffer(createInfo);
+        
+        if (!framebuffer) {
 			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
             return;
         }
 
-        vk::Viewport dynamicViewport(0.0f, 0.0f,
-            static_cast<float>(width), static_cast<float>(height),
-            0.0f, 1.0f);
+        vk::Viewport dynamicViewport(
+        		0.0f, 0.0f,
+            	static_cast<float>(width), static_cast<float>(height),
+            0.0f, 1.0f
+		);
 
         vk::Rect2D dynamicScissor({0, 0}, {width, height});
 
@@ -349,15 +401,17 @@ namespace vkcv
 		const auto swapchainImages = m_Context.getDevice().getSwapchainImagesKHR(m_swapchain.getSwapchain());
 
 		const auto& queueManager = m_Context.getQueueManager();
-		std::array<vk::Semaphore, 2> waitSemaphores{ 
-			m_SyncResources.renderFinished, 
-			m_SyncResources.swapchainImageAcquired };
+		std::array<vk::Semaphore, 2> waitSemaphores{
+			m_SyncResources.renderFinished,
+			m_SyncResources.swapchainImageAcquired
+		};
 
 		const vk::SwapchainKHR& swapchain = m_swapchain.getSwapchain();
 		const vk::PresentInfoKHR presentInfo(
 			waitSemaphores,
 			swapchain,
-			m_currentSwapchainImageIndex);
+			m_currentSwapchainImageIndex
+		);
 		
 		vk::Result result;
 		
@@ -365,18 +419,21 @@ namespace vkcv
 			result = queueManager.getPresentQueue().handle.presentKHR(presentInfo);
 		} catch (vk::OutOfDateKHRError e) {
 			result = vk::Result::eErrorOutOfDateKHR;
+		} catch (vk::DeviceLostError e) {
+			result = vk::Result::eErrorDeviceLost;
 		}
 		
-		if (result != vk::Result::eSuccess) {
-			vkcv_log(LogLevel::ERROR, "Swapchain present failed (%s)", vk::to_string(result).c_str());
+		if ((result != vk::Result::eSuccess) &&
+			(result != vk::Result::eSuboptimalKHR)) {
+			vkcv_log(LogLevel::ERROR, "Swapchain presentation failed (%s)", vk::to_string(result).c_str());
+		} else
+		if (result == vk::Result::eSuboptimalKHR) {
+			vkcv_log(LogLevel::WARNING, "Swapchain presentation is suboptimal");
+			m_swapchain.signalSwapchainRecreation();
 		}
 	}
-
-	vk::Format Core::getSwapchainImageFormat() {
-		return m_swapchain.getSwapchainFormat();
-	}
 	
-	void Core::recordAndSubmitCommands(
+	void Core::recordAndSubmitCommandsImmediate(
 		const SubmitInfo &submitInfo, 
 		const RecordCommandFunction &record, 
 		const FinishCommandFunction &finish)
@@ -390,10 +447,12 @@ namespace vkcv
 		beginCommandBuffer(cmdBuffer, vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
 		record(cmdBuffer);
 		cmdBuffer.end();
+		
+		vk::Fence waitFence = createFence(device);
 
-		const vk::Fence waitFence = createFence(device);
 		submitCommandBufferToQueue(queue.handle, cmdBuffer, waitFence, submitInfo.waitSemaphores, submitInfo.signalSemaphores);
 		waitForFence(device, waitFence);
+		
 		device.destroyFence(waitFence);
 		
 		device.freeCommandBuffers(cmdPool, cmdBuffer);
@@ -435,9 +494,42 @@ namespace vkcv
 		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode);
 	}
 
-	Image Core::createImage(vk::Format format, uint32_t width, uint32_t height, uint32_t depth)
+	Image Core::createImage(
+		vk::Format      format,
+		uint32_t        width,
+		uint32_t        height,
+		uint32_t        depth,
+		bool            createMipChain,
+		bool            supportStorage,
+		bool            supportColorAttachment,
+		Multisampling   multisampling)
+	{
+
+		uint32_t mipCount = 1;
+		if (createMipChain) {
+			mipCount = 1 + (uint32_t)std::floor(std::log2(std::max(width, std::max(height, depth))));
+		}
+
+		return Image::create(
+			m_ImageManager.get(), 
+			format,
+			width,
+			height,
+			depth,
+			mipCount,
+			supportStorage,
+			supportColorAttachment,
+			multisampling);
+	}
+
+	const uint32_t Core::getImageWidth(ImageHandle imageHandle)
 	{
-    	return Image::create(m_ImageManager.get(), format, width, height, depth);
+		return m_ImageManager->getImageWidth(imageHandle);
+	}
+
+	const uint32_t Core::getImageHeight(ImageHandle imageHandle)
+	{
+		return m_ImageManager->getImageHeight(imageHandle);
 	}
 
     DescriptorSetHandle Core::createDescriptorSet(const std::vector<DescriptorBinding>& bindings)
@@ -447,7 +539,7 @@ namespace vkcv
 
 	void Core::writeDescriptorSet(DescriptorSetHandle handle, const DescriptorWrites &writes) {
 		m_DescriptorManager->writeDescriptorSet(
-			handle,  
+			handle,
 			writes, 
 			*m_ImageManager, 
 			*m_BufferManager, 
@@ -458,53 +550,45 @@ namespace vkcv
 		return m_DescriptorManager->getDescriptorSet(handle);
 	}
 
-    std::vector<vk::ImageView> Core::createImageViews( Context &context, SwapChain& swapChain){
-        std::vector<vk::ImageView> imageViews;
-        std::vector<vk::Image> swapChainImages = context.getDevice().getSwapchainImagesKHR(swapChain.getSwapchain());
-        imageViews.reserve( swapChainImages.size() );
-        //here can be swizzled with vk::ComponentSwizzle if needed
-        vk::ComponentMapping componentMapping(
-                vk::ComponentSwizzle::eR,
-                vk::ComponentSwizzle::eG,
-                vk::ComponentSwizzle::eB,
-                vk::ComponentSwizzle::eA );
-
-        vk::ImageSubresourceRange subResourceRange( vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 );
-
-        for ( auto image : swapChainImages )
-        {
-            vk::ImageViewCreateInfo imageViewCreateInfo(
-                    vk::ImageViewCreateFlags(),
-                    image,
-                    vk::ImageViewType::e2D,
-                    swapChain.getSwapchainFormat(),
-                    componentMapping,
-                    subResourceRange);
-
-            imageViews.push_back(context.getDevice().createImageView(imageViewCreateInfo));
-        }
-        return imageViews;
-    }
+	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle cmdStream) {
+		auto swapchainHandle = ImageHandle::createSwapchainImageHandle();
+		recordCommandsToStream(cmdStream, [swapchainHandle, this](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordImageLayoutTransition(swapchainHandle, vk::ImageLayout::ePresentSrcKHR, cmdBuffer);
+		}, nullptr);
+	}
 
-	void Core::recordSwapchainImageLayoutTransition(vk::CommandBuffer cmdBuffer, vk::ImageLayout newLayout) {
-		auto& imageLayout = m_swapchainImageLayouts[m_currentSwapchainImageIndex];
-		const auto transitionBarrier = createSwapchainImageLayoutTransitionBarrier(
-			m_swapchainImages[m_currentSwapchainImageIndex],
-			imageLayout,
-			newLayout);
-		recordImageBarrier(cmdBuffer, transitionBarrier);
-		imageLayout = newLayout;
+	void Core::prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image) {
+		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eShaderReadOnlyOptimal, cmdBuffer);
+		}, nullptr);
 	}
 
-	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle handle) {
-		m_CommandStreamManager->recordCommandsToStream(handle, [&](vk::CommandBuffer cmdBuffer) {
-			recordSwapchainImageLayoutTransition(cmdBuffer, vk::ImageLayout::ePresentSrcKHR);
-		});
+	void Core::prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image) {
+		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eGeneral, cmdBuffer);
+		}, nullptr);
 	}
 
-	void Core::prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
-			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eShaderReadOnlyOptimal, cmdBuffer);
+			m_ImageManager->recordImageMemoryBarrier(image, cmdBuffer);
+		}, nullptr);
+	}
+
+	void Core::recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer) {
+		recordCommandsToStream(cmdStream, [buffer, this](const vk::CommandBuffer cmdBuffer) {
+			m_BufferManager->recordBufferMemoryBarrier(buffer, cmdBuffer);
 		}, nullptr);
 	}
+	
+	void Core::resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst) {
+		recordCommandsToStream(cmdStream, [src, dst, this](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordMSAAResolve(cmdBuffer, src, dst);
+		}, nullptr);
+	}
+
+	vk::ImageView Core::getSwapchainImageView() const {
+    	return m_ImageManager->getVulkanImageView(vkcv::ImageHandle::createSwapchainImageHandle());
+    }
+	
 }
diff --git a/src/vkcv/DescriptorManager.cpp b/src/vkcv/DescriptorManager.cpp
index f591daf90b47b57a758b2b24c7fa87b5c33e3c46..8e565a766cd407dc33c0291d3d07b01d6d3066e7 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -107,10 +107,11 @@ namespace vkcv
 		std::vector<WriteDescriptorSetInfo> writeInfos;
 
 		for (const auto& write : writes.sampledImageWrites) {
+		    vk::ImageLayout layout = write.useGeneralLayout ? vk::ImageLayout::eGeneral : vk::ImageLayout::eShaderReadOnlyOptimal;
 			const vk::DescriptorImageInfo imageInfo(
 				nullptr,
-				imageManager.getVulkanImageView(write.image),
-				vk::ImageLayout::eShaderReadOnlyOptimal
+				imageManager.getVulkanImageView(write.image, write.mipLevel),
+                layout
 			);
 			
 			imageInfos.push_back(imageInfo);
@@ -128,7 +129,7 @@ namespace vkcv
 		for (const auto& write : writes.storageImageWrites) {
 			const vk::DescriptorImageInfo imageInfo(
 				nullptr,
-				imageManager.getVulkanImageView(write.image),
+				imageManager.getVulkanImageView(write.image, write.mipLevel),
 				vk::ImageLayout::eGeneral
 			);
 			
diff --git a/src/vkcv/DrawcallRecording.cpp b/src/vkcv/DrawcallRecording.cpp
index 85b6eeb5fa413223b7b7f10f77b868252912041b..e6ea18588c251b5e49f454618a5ac9962cc8a264 100644
--- a/src/vkcv/DrawcallRecording.cpp
+++ b/src/vkcv/DrawcallRecording.cpp
@@ -23,19 +23,25 @@ namespace vkcv {
                 nullptr);
         }
 
-        cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, vk::IndexType::eUint16);	//FIXME: choose proper size
-
         const size_t drawcallPushConstantOffset = drawcallIndex * pushConstantData.sizePerDrawcall;
         // char* cast because void* does not support pointer arithmetic
         const void* drawcallPushConstantData = drawcallPushConstantOffset + (char*)pushConstantData.data;
 
-        cmdBuffer.pushConstants(
-            pipelineLayout,
-            vk::ShaderStageFlagBits::eAll,
-            0,
-            pushConstantData.sizePerDrawcall,
-            drawcallPushConstantData);
+        if (pushConstantData.data && pushConstantData.sizePerDrawcall > 0) {
+            cmdBuffer.pushConstants(
+                pipelineLayout,
+                vk::ShaderStageFlagBits::eAll,
+                0,
+                pushConstantData.sizePerDrawcall,
+                drawcallPushConstantData);
+        }
 
-        cmdBuffer.drawIndexed(drawcall.mesh.indexCount, 1, 0, 0, {});
+        if (drawcall.mesh.indexBuffer) {
+            cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, vk::IndexType::eUint16);	//FIXME: choose proper size
+            cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
+        }
+        else {
+            cmdBuffer.draw(drawcall.mesh.indexCount, 1, 0, 0, {});
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/src/vkcv/Image.cpp b/src/vkcv/Image.cpp
index f861daeb1cd7de9697e2f649de444666b8b0e63c..f8d94b734599cbf1f55aad7b590ab4796501d951 100644
--- a/src/vkcv/Image.cpp
+++ b/src/vkcv/Image.cpp
@@ -19,9 +19,20 @@ namespace vkcv{
 		}
 	}
 
-	Image Image::create(ImageManager* manager, vk::Format format, uint32_t width, uint32_t height, uint32_t depth)
+	Image Image::create(
+		ImageManager*   manager,
+		vk::Format      format,
+		uint32_t        width,
+		uint32_t        height,
+		uint32_t        depth,
+		uint32_t        mipCount,
+		bool            supportStorage,
+		bool            supportColorAttachment,
+		Multisampling   msaa)
 	{
-		return Image(manager, manager->createImage(width, height, depth, format));
+		return Image(
+			manager, 
+			manager->createImage(width, height, depth, format, mipCount, supportStorage, supportColorAttachment, msaa));
 	}
 	
 	vk::Format Image::getFormat() const {
@@ -48,10 +59,22 @@ namespace vkcv{
 	vkcv::ImageHandle Image::getHandle() const {
 		return m_handle;
 	}
-	
+
+	uint32_t Image::getMipCount() const {
+		return m_manager->getImageMipCount(m_handle);
+	}
+
 	void Image::fill(void *data, size_t size) {
 		m_manager->fillImage(m_handle, data, size);
 	}
+
+	void Image::generateMipChainImmediate() {
+		m_manager->generateImageMipChainImmediate(m_handle);
+	}
+
+	void Image::recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream) {
+		m_manager->recordImageMipChainGenerationToCmdStream(cmdStream, m_handle);
+	}
 	
 	Image::Image(ImageManager* manager, const ImageHandle& handle) :
 		m_manager(manager),
diff --git a/src/vkcv/ImageConfig.cpp b/src/vkcv/ImageConfig.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..80aeac3d0f702467c8edc1b0f7378d0d7f15b3db
--- /dev/null
+++ b/src/vkcv/ImageConfig.cpp
@@ -0,0 +1,24 @@
+#include <vkcv/ImageConfig.hpp>
+#include <vkcv/Logger.hpp>
+
+namespace vkcv {
+	vk::SampleCountFlagBits msaaToVkSampleCountFlag(Multisampling msaa) {
+		switch (msaa) {
+		case Multisampling::None:   return vk::SampleCountFlagBits::e1;
+		case Multisampling::MSAA2X: return vk::SampleCountFlagBits::e2;
+		case Multisampling::MSAA4X: return vk::SampleCountFlagBits::e4;
+		case Multisampling::MSAA8X: return vk::SampleCountFlagBits::e8;
+		default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown Multisampling enum setting"); return vk::SampleCountFlagBits::e1;
+		}
+	}
+
+	uint32_t msaaToSampleCount(Multisampling msaa) {
+		switch (msaa) {
+		case Multisampling::None:   return 1;
+		case Multisampling::MSAA2X: return 2;
+		case Multisampling::MSAA4X: return 4;
+		case Multisampling::MSAA8X: return 8;
+		default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown Multisampling enum setting"); return 1;
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/vkcv/ImageLayoutTransitions.cpp b/src/vkcv/ImageLayoutTransitions.cpp
index cb0f90a79d188cd80a5744d8c6ad7718e542d473..8d31c64ccbcbf33e259714f8c581c920738190b4 100644
--- a/src/vkcv/ImageLayoutTransitions.cpp
+++ b/src/vkcv/ImageLayoutTransitions.cpp
@@ -15,7 +15,7 @@ namespace vkcv {
 		vk::ImageSubresourceRange imageSubresourceRange(
 			aspectFlags,
 			0,
-			image.m_levels,
+			image.m_viewPerMip.size(),
 			0,
 			image.m_layers
 		);
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index 1e3d19d02d7e86546d142bb64440364407e81824..ae554e6babdd2b2f42c352515c02a34e45182fec 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -13,25 +13,23 @@
 namespace vkcv {
 
 	ImageManager::Image::Image(
-		vk::Image           handle,
-		vk::DeviceMemory    memory,
-		vk::ImageView       view,
-		uint32_t            width,
-		uint32_t            height,
-		uint32_t            depth,
-		vk::Format          format,
-		uint32_t            layers,
-		uint32_t            levels)
+		vk::Image                   handle,
+		vk::DeviceMemory            memory,
+		std::vector<vk::ImageView>  views,
+		uint32_t                    width,
+		uint32_t                    height,
+		uint32_t                    depth,
+		vk::Format                  format,
+		uint32_t                    layers)
 		:
 		m_handle(handle),
 		m_memory(memory),
-		m_view(view),
+        m_viewPerMip(views),
 		m_width(width),
 		m_height(height),
 		m_depth(depth),
 		m_format(format),
-		m_layers(layers),
-		m_levels(levels)
+		m_layers(layers)
 	{}
 
 	/**
@@ -69,6 +67,11 @@ namespace vkcv {
 		for (uint64_t id = 0; id < m_images.size(); id++) {
 			destroyImageById(id);
 		}
+		for (const auto swapchainImage : m_swapchainImages) {
+			for (const auto view : swapchainImage.m_viewPerMip) {
+				m_core->getContext().getDevice().destroy(view);
+			}
+		}
 	}
 	
 	bool isDepthImageFormat(vk::Format format) {
@@ -81,7 +84,15 @@ namespace vkcv {
 		}
 	}
 
-	ImageHandle ImageManager::createImage(uint32_t width, uint32_t height, uint32_t depth, vk::Format format)
+	ImageHandle ImageManager::createImage(
+		uint32_t        width, 
+		uint32_t        height, 
+		uint32_t        depth, 
+		vk::Format      format, 
+		uint32_t        mipCount,
+		bool            supportStorage, 
+		bool            supportColorAttachment,
+		Multisampling   msaa)
 	{
 		const vk::PhysicalDevice& physicalDevice = m_core->getContext().getPhysicalDevice();
 		
@@ -89,9 +100,23 @@ namespace vkcv {
 		
 		vk::ImageCreateFlags createFlags;
 		vk::ImageUsageFlags imageUsageFlags = (
-				vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst
+				vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc
 		);
 		
+		vk::ImageTiling imageTiling = vk::ImageTiling::eOptimal;
+		
+		if (supportStorage) {
+			imageUsageFlags |= vk::ImageUsageFlagBits::eStorage;
+			
+			if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eStorageImage)) {
+				imageTiling = vk::ImageTiling::eLinear;
+			}
+		}
+		
+		if (supportColorAttachment) {
+			imageUsageFlags |= vk::ImageUsageFlagBits::eColorAttachment;
+		}
+		
 		const bool isDepthFormat = isDepthImageFormat(format);
 		
 		if (isDepthFormat) {
@@ -118,8 +143,6 @@ namespace vkcv {
 			imageViewType = vk::ImageViewType::e2D;
 		}
 		
-		vk::ImageTiling imageTiling = vk::ImageTiling::eOptimal;
-		
 		if (!formatProperties.optimalTilingFeatures) {
 			if (!formatProperties.linearTilingFeatures)
 				return ImageHandle();
@@ -127,21 +150,21 @@ namespace vkcv {
 			imageTiling = vk::ImageTiling::eLinear;
 		}
 		
-		const vk::ImageFormatProperties imageFormatProperties = physicalDevice.getImageFormatProperties(
-				format, imageType, imageTiling, imageUsageFlags
-		);
+		const vk::ImageFormatProperties imageFormatProperties = 
+			physicalDevice.getImageFormatProperties(format, imageType, imageTiling, imageUsageFlags);
 		
-		const uint32_t mipLevels = std::min<uint32_t>(1, imageFormatProperties.maxMipLevels);
 		const uint32_t arrayLayers = std::min<uint32_t>(1, imageFormatProperties.maxArrayLayers);
-		
+
+		vk::SampleCountFlagBits sampleCountFlag = msaaToVkSampleCountFlag(msaa);
+
 		const vk::ImageCreateInfo imageCreateInfo(
 			createFlags,
 			imageType,
 			format,
 			vk::Extent3D(width, height, depth),
-			mipLevels,
+			mipCount,
 			arrayLayers,
-			vk::SampleCountFlagBits::e1,
+			sampleCountFlag,
 			imageTiling,
 			imageUsageFlags,
 			vk::SharingMode::eExclusive,
@@ -172,30 +195,33 @@ namespace vkcv {
 			aspectFlags = vk::ImageAspectFlagBits::eColor;
 		}
 		
-		const vk::ImageViewCreateInfo imageViewCreateInfo (
+		std::vector<vk::ImageView> views;
+		for (int mip = 0; mip < mipCount; mip++) {
+			const vk::ImageViewCreateInfo imageViewCreateInfo(
 				{},
 				image,
 				imageViewType,
 				format,
 				vk::ComponentMapping(
-						vk::ComponentSwizzle::eIdentity,
-						vk::ComponentSwizzle::eIdentity,
-						vk::ComponentSwizzle::eIdentity,
-						vk::ComponentSwizzle::eIdentity
+					vk::ComponentSwizzle::eIdentity,
+					vk::ComponentSwizzle::eIdentity,
+					vk::ComponentSwizzle::eIdentity,
+					vk::ComponentSwizzle::eIdentity
 				),
 				vk::ImageSubresourceRange(
-						aspectFlags,
-						0,
-						mipLevels,
-						0,
-						arrayLayers
+					aspectFlags,
+					mip,
+					mipCount - mip,
+					0,
+					arrayLayers
 				)
-		);
-		
-		vk::ImageView view = device.createImageView(imageViewCreateInfo);
+			);
+
+			views.push_back(device.createImageView(imageViewCreateInfo));
+		}
 		
 		const uint64_t id = m_images.size();
-		m_images.push_back(Image(image, memory, view, width, height, depth, format, arrayLayers, mipLevels));
+		m_images.push_back(Image(image, memory, views, width, height, depth, format, arrayLayers));
 		return ImageHandle(id, [&](uint64_t id) { destroyImageById(id); });
 	}
 	
@@ -204,8 +230,12 @@ namespace vkcv {
 	}
 	
 	vk::Image ImageManager::getVulkanImage(const ImageHandle &handle) const {
+
+		if (handle.isSwapchainImage()) {
+			m_swapchainImages[m_currentSwapchainInputImage].m_handle;
+		}
+
 		const uint64_t id = handle.getId();
-		
 		if (id >= m_images.size()) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return nullptr;
@@ -217,8 +247,13 @@ namespace vkcv {
 	}
 	
 	vk::DeviceMemory ImageManager::getVulkanDeviceMemory(const ImageHandle &handle) const {
+
+		if (handle.isSwapchainImage()) {
+			vkcv_log(LogLevel::ERROR, "Swapchain image has no memory");
+			return nullptr;
+		}
+
 		const uint64_t id = handle.getId();
-		
 		if (id >= m_images.size()) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return nullptr;
@@ -229,34 +264,45 @@ namespace vkcv {
 		return image.m_memory;
 	}
 	
-	vk::ImageView ImageManager::getVulkanImageView(const ImageHandle &handle) const {
-		const uint64_t id = handle.getId();
+	vk::ImageView ImageManager::getVulkanImageView(const ImageHandle &handle, const size_t mipLevel) const {
 		
+		if (handle.isSwapchainImage()) {
+			return m_swapchainImages[m_currentSwapchainInputImage].m_viewPerMip[0];
+		}
+
+		const uint64_t id = handle.getId();
 		if (id >= m_images.size()) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return nullptr;
 		}
 		
-		auto& image = m_images[id];
-		
-		return image.m_view;
+		const auto& image = m_images[id];
+
+		if (mipLevel >= image.m_viewPerMip.size()) {
+			vkcv_log(LogLevel::ERROR, "Image does not have requested mipLevel");
+			return nullptr;
+		}
+
+		return image.m_viewPerMip[mipLevel];
 	}
 	
 	void ImageManager::switchImageLayoutImmediate(const ImageHandle& handle, vk::ImageLayout newLayout) {
-		const uint64_t id = handle.getId();
+		uint64_t id = handle.getId();
 		
-		if (id >= m_images.size()) {
+		const bool isSwapchainImage = handle.isSwapchainImage();
+
+		if (id >= m_images.size() && !isSwapchainImage) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return;
 		}
 		
-		auto& image = m_images[id];
+		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
 		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, newLayout);
 		
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Graphics;
 		
-		m_core->recordAndSubmitCommands(
+		m_core->recordAndSubmitCommandsImmediate(
 			submitInfo,
 			[transitionBarrier](const vk::CommandBuffer& commandBuffer) {
 			// TODO: precise PipelineStageFlagBits, will require a lot of context
@@ -279,22 +325,59 @@ namespace vkcv {
 		vk::CommandBuffer cmdBuffer) {
 
 		const uint64_t id = handle.getId();
+		const bool isSwapchainImage = handle.isSwapchainImage();
 
-		if (id >= m_images.size()) {
+		if (id >= m_images.size() && !isSwapchainImage) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return;
 		}
 
-		auto& image = m_images[id];
+		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
 		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, newLayout);
 		recordImageBarrier(cmdBuffer, transitionBarrier);
 		image.m_layout = newLayout;
 	}
+
+	void ImageManager::recordImageMemoryBarrier(
+		const ImageHandle& handle,
+		vk::CommandBuffer cmdBuffer) {
+
+		const uint64_t id = handle.getId();
+		const bool isSwapchainImage = handle.isSwapchainImage();
+
+		if (id >= m_images.size() && !isSwapchainImage) {
+			std::cerr << "Error: ImageManager::recordImageMemoryBarrier invalid handle" << std::endl;
+			return;
+		}
+
+		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
+		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, image.m_layout);
+		recordImageBarrier(cmdBuffer, transitionBarrier);
+	}
+	
+	constexpr uint32_t getChannelsByFormat(vk::Format format) {
+		switch (format) {
+			case vk::Format::eR8Unorm:
+				return 1;
+			case vk::Format::eR8G8B8A8Srgb:
+				return 4;
+			case vk::Format::eR8G8B8A8Unorm:
+				return 4;
+			default:
+				std::cerr << "Unknown image format" << std::endl;
+				return 4;
+		}
+	}
 	
 	void ImageManager::fillImage(const ImageHandle& handle, void* data, size_t size)
 	{
 		const uint64_t id = handle.getId();
 		
+		if (handle.isSwapchainImage()) {
+			vkcv_log(LogLevel::ERROR, "Swapchain image cannot be filled");
+			return;
+		}
+
 		if (id >= m_images.size()) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return;
@@ -306,7 +389,7 @@ namespace vkcv {
 				handle,
 				vk::ImageLayout::eTransferDstOptimal);
 		
-		uint32_t channels = 4; // TODO: check image.m_format
+		uint32_t channels = getChannelsByFormat(image.m_format);
 		const size_t image_size = (
 				image.m_width * image.m_height * image.m_depth * channels
 		);
@@ -324,7 +407,7 @@ namespace vkcv {
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Transfer;
 		
-		m_core->recordAndSubmitCommands(
+		m_core->recordAndSubmitCommandsImmediate(
 				submitInfo,
 				[&image, &stagingBuffer](const vk::CommandBuffer& commandBuffer) {
 					vk::ImageAspectFlags aspectFlags;
@@ -365,42 +448,165 @@ namespace vkcv {
 				}
 		);
 	}
-	
+
+	void ImageManager::recordImageMipGenerationToCmdBuffer(vk::CommandBuffer cmdBuffer, const ImageHandle& handle) {
+
+		const auto id = handle.getId();
+		if (id >= m_images.size()) {
+			vkcv_log(vkcv::LogLevel::ERROR, "Invalid image handle");
+			return;
+		}
+
+		auto& image = m_images[id];
+		recordImageLayoutTransition(handle, vk::ImageLayout::eGeneral, cmdBuffer);
+
+		vk::ImageAspectFlags aspectMask = isDepthImageFormat(image.m_format) ?
+			vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor;
+
+		uint32_t srcWidth = image.m_width;
+		uint32_t srcHeight = image.m_height;
+		uint32_t srcDepth = image.m_depth;
+
+		auto half = [](uint32_t in) {
+			return std::max<uint32_t>(in / 2, 1);
+		};
+
+		uint32_t dstWidth = half(srcWidth);
+		uint32_t dstHeight = half(srcHeight);
+		uint32_t dstDepth = half(srcDepth);
+
+		for (uint32_t srcMip = 0; srcMip < image.m_viewPerMip.size() - 1; srcMip++) {
+			uint32_t dstMip = srcMip + 1;
+			vk::ImageBlit region(
+				vk::ImageSubresourceLayers(aspectMask, srcMip, 0, 1),
+				{ vk::Offset3D(0, 0, 0), vk::Offset3D(srcWidth, srcHeight, srcDepth) },
+				vk::ImageSubresourceLayers(aspectMask, dstMip, 0, 1),
+				{ vk::Offset3D(0, 0, 0), vk::Offset3D(dstWidth, dstHeight, dstDepth) });
+
+			cmdBuffer.blitImage(
+				image.m_handle,
+				vk::ImageLayout::eGeneral,
+				image.m_handle,
+				vk::ImageLayout::eGeneral,
+				region,
+				vk::Filter::eLinear);
+
+			srcWidth = dstWidth;
+			srcHeight = dstHeight;
+			srcDepth = dstDepth;
+
+			dstWidth = half(dstWidth);
+			dstHeight = half(dstHeight);
+			dstDepth = half(dstDepth);
+
+			recordImageMemoryBarrier(handle, cmdBuffer);
+		}
+	}
+
+	void ImageManager::generateImageMipChainImmediate(const ImageHandle& handle) {
+
+		const auto& device = m_core->getContext().getDevice();
+
+		SubmitInfo submitInfo;
+		submitInfo.queueType = QueueType::Graphics;
+
+		if (handle.isSwapchainImage()) {
+			vkcv_log(vkcv::LogLevel::ERROR, "You cannot generate a mip chain for the swapchain, what are you smoking?");
+			return;
+		}
+
+		const auto record = [this, handle](const vk::CommandBuffer cmdBuffer) {
+			recordImageMipGenerationToCmdBuffer(cmdBuffer, handle);
+		};
+
+		m_core->recordAndSubmitCommandsImmediate(submitInfo, record, nullptr);
+	}
+
+	void ImageManager::recordImageMipChainGenerationToCmdStream(
+		const vkcv::CommandStreamHandle& cmdStream,
+		const ImageHandle& handle) {
+
+		const auto record = [this, handle](const vk::CommandBuffer cmdBuffer) {
+			recordImageMipGenerationToCmdBuffer(cmdBuffer, handle);
+		};
+		m_core->recordCommandsToStream(cmdStream, record, nullptr);
+	}
+
+	void ImageManager::recordMSAAResolve(vk::CommandBuffer cmdBuffer, ImageHandle src, ImageHandle dst) {
+
+		const uint64_t srcId = src.getId();
+		const uint64_t dstId = dst.getId();
+
+		const bool isSrcSwapchainImage = src.isSwapchainImage();
+		const bool isDstSwapchainImage = dst.isSwapchainImage();
+
+		const bool isSrcHandleInvalid = srcId >= m_images.size() && !isSrcSwapchainImage;
+		const bool isDstHandleInvalid = dstId >= m_images.size() && !isDstSwapchainImage;
+
+		if (isSrcHandleInvalid || isDstHandleInvalid) {
+			vkcv_log(LogLevel::ERROR, "Invalid handle");
+			return;
+		}
+
+		auto& srcImage = isSrcSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[srcId];
+		auto& dstImage = isDstSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[dstId];
+
+		vk::ImageResolve region(
+			vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1),
+			vk::Offset3D(0, 0, 0),
+			vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1),
+			vk::Offset3D(0, 0, 0), 
+			vk::Extent3D(dstImage.m_width, dstImage.m_height, dstImage.m_depth));
+
+		recordImageLayoutTransition(src, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer);
+		recordImageLayoutTransition(dst, vk::ImageLayout::eTransferDstOptimal, cmdBuffer);
+
+		cmdBuffer.resolveImage(
+			srcImage.m_handle,
+			srcImage.m_layout,
+			dstImage.m_handle,
+			dstImage.m_layout,
+			region);
+	}
+
 	uint32_t ImageManager::getImageWidth(const ImageHandle &handle) const {
 		const uint64_t id = handle.getId();
-		
-		if (id >= m_images.size()) {
+		const bool isSwapchainImage = handle.isSwapchainImage();
+
+		if (id >= m_images.size() && !isSwapchainImage) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return 0;
 		}
 		
-		auto& image = m_images[id];
+		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
 		
 		return image.m_width;
 	}
 	
 	uint32_t ImageManager::getImageHeight(const ImageHandle &handle) const {
 		const uint64_t id = handle.getId();
+		const bool isSwapchainImage = handle.isSwapchainImage();
 		
-		if (id >= m_images.size()) {
+		if (id >= m_images.size() && !isSwapchainImage) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return 0;
 		}
 		
-		auto& image = m_images[id];
+		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
 		
 		return image.m_height;
 	}
 	
 	uint32_t ImageManager::getImageDepth(const ImageHandle &handle) const {
 		const uint64_t id = handle.getId();
-		
-		if (id >= m_images.size()) {
+		const bool isSwapchainImage = handle.isSwapchainImage();
+
+		if (id >= m_images.size() && !isSwapchainImage) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return 0;
 		}
 		
-		auto& image = m_images[id];
+		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
 		
 		return image.m_depth;
 	}
@@ -416,9 +622,11 @@ namespace vkcv {
 
 		const vk::Device& device = m_core->getContext().getDevice();
 		
-		if (image.m_view) {
-			device.destroyImageView(image.m_view);
-			image.m_view = nullptr;
+		for (auto& view : image.m_viewPerMip) {
+			if (view) {
+				device.destroyImageView(view);
+				view = nullptr;
+			}
 		}
 
 		if (image.m_memory) {
@@ -435,13 +643,51 @@ namespace vkcv {
 	vk::Format ImageManager::getImageFormat(const ImageHandle& handle) const {
 
 		const uint64_t id = handle.getId();
+		const bool isSwapchainFormat = handle.isSwapchainImage();
 
-		if (id >= m_images.size()) {
+		if (id >= m_images.size() && !isSwapchainFormat) {
 			vkcv_log(LogLevel::ERROR, "Invalid handle");
 			return vk::Format::eUndefined;
 		}
 
-		return m_images[id].m_format;
+		return isSwapchainFormat ? m_swapchainImages[m_currentSwapchainInputImage].m_format : m_images[id].m_format;
+	}
+
+	uint32_t ImageManager::getImageMipCount(const ImageHandle& handle) const {
+		const uint64_t id = handle.getId();
+		const bool isSwapchainFormat = handle.isSwapchainImage();
+
+		if (handle.isSwapchainImage()) {
+			return 1;
+		}
+
+		if (id >= m_images.size()) {
+			vkcv_log(LogLevel::ERROR, "Invalid handle");
+			return 0;
+		}
+
+		return m_images[id].m_viewPerMip.size();
+	}
+
+	void ImageManager::setCurrentSwapchainImageIndex(int index) {
+		m_currentSwapchainInputImage = index;
+	}
+
+	void ImageManager::setSwapchainImages(const std::vector<vk::Image>& images, std::vector<vk::ImageView> views, 
+		uint32_t width, uint32_t height, vk::Format format) {
+
+		// destroy old views
+		for (auto image : m_swapchainImages) {
+			for (const auto& view : image.m_viewPerMip) {
+				m_core->getContext().getDevice().destroyImageView(view);
+			}
+		}
+
+		assert(images.size() == views.size());
+		m_swapchainImages.clear();
+		for (int i = 0; i < images.size(); i++) {
+			m_swapchainImages.push_back(Image(images[i], nullptr, { views[i] }, width, height, 1, format, 1));
+		}
 	}
 
 }
\ No newline at end of file
diff --git a/src/vkcv/ImageManager.hpp b/src/vkcv/ImageManager.hpp
index b9fccb25ec16bc1fd9569ab1a94627bd7ff06b18..1d8ce207b645e30cee291816eac3c934ed40e92a 100644
--- a/src/vkcv/ImageManager.hpp
+++ b/src/vkcv/ImageManager.hpp
@@ -9,6 +9,7 @@
 
 #include "vkcv/BufferManager.hpp"
 #include "vkcv/Handles.hpp"
+#include "vkcv/ImageConfig.hpp"
 
 namespace vkcv {
 
@@ -18,29 +19,29 @@ namespace vkcv {
 	public:
 		struct Image
 		{
-			vk::Image           m_handle;
-			vk::DeviceMemory    m_memory;
-			vk::ImageView       m_view;
-			uint32_t            m_width = 0;
-			uint32_t            m_height = 0;
-			uint32_t            m_depth = 0;
-			vk::Format          m_format;
-			uint32_t            m_layers = 1;
-			uint32_t            m_levels = 1;
-			vk::ImageLayout     m_layout = vk::ImageLayout::eUndefined;
+			vk::Image                   m_handle;
+			vk::DeviceMemory            m_memory;
+			std::vector<vk::ImageView>  m_viewPerMip;
+			uint32_t                    m_width     = 0;
+			uint32_t                    m_height    = 0;
+			uint32_t                    m_depth     = 0;
+			vk::Format                  m_format;
+			uint32_t                    m_layers    = 1;
+			vk::ImageLayout             m_layout    = vk::ImageLayout::eUndefined;
 		private:
 			// struct is public so utility functions can access members, but only ImageManager can create Image
 			friend ImageManager;
 			Image(
-				vk::Image           handle,
-				vk::DeviceMemory    memory,
-				vk::ImageView       view,
-				uint32_t            width,
-				uint32_t            height,
-				uint32_t            depth,
-				vk::Format          format,
-				uint32_t            layers,
-				uint32_t            levels);
+				vk::Image                   handle,
+				vk::DeviceMemory            memory,
+				std::vector<vk::ImageView>  views,
+				uint32_t                    width,
+				uint32_t                    height,
+				uint32_t                    depth,
+				vk::Format                  format,
+				uint32_t                    layers);
+
+			Image();
 		};
 	private:
 		
@@ -48,6 +49,8 @@ namespace vkcv {
 		BufferManager& m_bufferManager;
 		
 		std::vector<Image> m_images;
+		std::vector<Image> m_swapchainImages;
+		int m_currentSwapchainInputImage;
 		
 		ImageManager(BufferManager& bufferManager) noexcept;
 		
@@ -58,7 +61,9 @@ namespace vkcv {
 		 * @param id Image handle id
 		 */
 		void destroyImageById(uint64_t id);
-		
+
+		void recordImageMipGenerationToCmdBuffer(vk::CommandBuffer cmdBuffer, const ImageHandle& handle);
+
 	public:
 		~ImageManager() noexcept;
 		ImageManager(ImageManager&& other) = delete;
@@ -67,7 +72,15 @@ namespace vkcv {
 		ImageManager& operator=(ImageManager&& other) = delete;
 		ImageManager& operator=(const ImageManager& other) = delete;
 		
-		ImageHandle createImage(uint32_t width, uint32_t height, uint32_t depth, vk::Format format);
+		ImageHandle createImage(
+			uint32_t        width, 
+			uint32_t        height, 
+			uint32_t        depth, 
+			vk::Format      format, 
+			uint32_t        mipCount,
+			bool            supportStorage, 
+			bool            supportColorAttachment,
+			Multisampling   msaa);
 		
 		ImageHandle createSwapchainImage();
 		
@@ -78,7 +91,7 @@ namespace vkcv {
 		vk::DeviceMemory getVulkanDeviceMemory(const ImageHandle& handle) const;
 		
 		[[nodiscard]]
-		vk::ImageView getVulkanImageView(const ImageHandle& handle) const;
+		vk::ImageView getVulkanImageView(const ImageHandle& handle, const size_t mipLevel = 0) const;
 
 		void switchImageLayoutImmediate(const ImageHandle& handle, vk::ImageLayout newLayout);
 		void recordImageLayoutTransition(
@@ -86,8 +99,15 @@ namespace vkcv {
 			vk::ImageLayout newLayout, 
 			vk::CommandBuffer cmdBuffer);
 
+		void recordImageMemoryBarrier(
+			const ImageHandle& handle,
+			vk::CommandBuffer cmdBuffer);
+
 		void fillImage(const ImageHandle& handle, void* data, size_t size);
-		
+		void generateImageMipChainImmediate(const ImageHandle& handle);
+		void recordImageMipChainGenerationToCmdStream(const vkcv::CommandStreamHandle& cmdStream, const ImageHandle& handle);
+		void recordMSAAResolve(vk::CommandBuffer cmdBuffer, ImageHandle src, ImageHandle dst);
+
 		[[nodiscard]]
 		uint32_t getImageWidth(const ImageHandle& handle) const;
 		
@@ -99,5 +119,13 @@ namespace vkcv {
 		
 		[[nodiscard]]
 		vk::Format getImageFormat(const ImageHandle& handle) const;
+
+		[[nodiscard]]
+		uint32_t getImageMipCount(const ImageHandle& handle) const;
+
+		void setCurrentSwapchainImageIndex(int index);
+		void setSwapchainImages(const std::vector<vk::Image>& images, std::vector<vk::ImageView> views,
+			uint32_t width, uint32_t height, vk::Format format);
+
 	};
 }
\ No newline at end of file
diff --git a/src/vkcv/PassConfig.cpp b/src/vkcv/PassConfig.cpp
index 602f1d3e2a8100ebd9bbb83772312d3d659abe86..78bd5808b63fee7333243db4fca640047f76eae9 100644
--- a/src/vkcv/PassConfig.cpp
+++ b/src/vkcv/PassConfig.cpp
@@ -13,7 +13,7 @@ namespace vkcv
 	format(format)
     {};
 
-    PassConfig::PassConfig(std::vector<AttachmentDescription> attachments) noexcept :
-    attachments{std::move(attachments)}
+    PassConfig::PassConfig(std::vector<AttachmentDescription> attachments, Multisampling msaa) noexcept :
+    attachments{std::move(attachments) }, msaa(msaa)
     {}
 }
\ No newline at end of file
diff --git a/src/vkcv/PassManager.cpp b/src/vkcv/PassManager.cpp
index c34b0d3631c48561f42eb7f21ba5578156910f51..e50f800a482460cdb81687c76f8b1318598f689c 100644
--- a/src/vkcv/PassManager.cpp
+++ b/src/vkcv/PassManager.cpp
@@ -94,7 +94,7 @@ namespace vkcv
             vk::AttachmentDescription attachmentDesc(
                 {},
                 format,
-                vk::SampleCountFlagBits::e1,
+                msaaToVkSampleCountFlag(config.msaa),
                 getVKLoadOpFromAttachOp(config.attachments[i].load_operation),
                 getVkStoreOpFromAttachOp(config.attachments[i].store_operation),
                 vk::AttachmentLoadOp::eDontCare,
diff --git a/src/vkcv/PipelineConfig.cpp b/src/vkcv/PipelineConfig.cpp
deleted file mode 100644
index 3bd2a68cb86f167afecc551dbd664dee8a63eb08..0000000000000000000000000000000000000000
--- a/src/vkcv/PipelineConfig.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @authors Mara Vogt, Mark Mints
- * @file src/vkcv/Pipeline.cpp
- * @brief Pipeline class to handle shader stages
- */
-
-#include "vkcv/PipelineConfig.hpp"
-
-namespace vkcv {
-
-    PipelineConfig::PipelineConfig(
-		const ShaderProgram&                        shaderProgram,
-		uint32_t                                    width,
-		uint32_t                                    height,
-		const PassHandle                            &passHandle,
-		const VertexLayout                          &vertexLayout,
-		const std::vector<vk::DescriptorSetLayout>  &descriptorLayouts,
-		bool                                        useDynamicViewport)
-		:
-		m_ShaderProgram(shaderProgram),
-		m_Height(height),
-		m_Width(width),
-		m_PassHandle(passHandle),
-		m_VertexLayout(vertexLayout),
-		m_DescriptorLayouts(descriptorLayouts),
-		m_UseDynamicViewport(useDynamicViewport)
-		{}
-}
diff --git a/src/vkcv/PipelineManager.cpp b/src/vkcv/PipelineManager.cpp
index 81b7525b160374915b1918c30870b05e619a30a4..8b1f0b68be3a72f60103ca0dd8136f2c923513a5 100644
--- a/src/vkcv/PipelineManager.cpp
+++ b/src/vkcv/PipelineManager.cpp
@@ -7,8 +7,7 @@ namespace vkcv
 
     PipelineManager::PipelineManager(vk::Device device) noexcept :
     m_Device{device},
-    m_Pipelines{},
-    m_Configs{}
+    m_Pipelines{}
     {}
 
     PipelineManager::~PipelineManager() noexcept
@@ -43,6 +42,27 @@ namespace vkcv
 		}
 	}
 
+    vk::PrimitiveTopology primitiveTopologyToVulkanPrimitiveTopology(const PrimitiveTopology topology) {
+        switch (topology) {
+        case(PrimitiveTopology::PointList):     return vk::PrimitiveTopology::ePointList;
+        case(PrimitiveTopology::LineList):      return vk::PrimitiveTopology::eLineList;
+        case(PrimitiveTopology::TriangleList):  return vk::PrimitiveTopology::eTriangleList;
+        default: std::cout << "Error: Unknown primitive topology type" << std::endl; return vk::PrimitiveTopology::eTriangleList;
+        }
+    }
+
+    vk::CompareOp depthTestToVkCompareOp(DepthTest depthTest) {
+        switch (depthTest) {
+        case(DepthTest::None):          return vk::CompareOp::eAlways;
+        case(DepthTest::Less):          return vk::CompareOp::eLess;
+        case(DepthTest::LessEqual):     return vk::CompareOp::eLessOrEqual;
+        case(DepthTest::Greater):       return vk::CompareOp::eGreater;
+        case(DepthTest::GreatherEqual): return vk::CompareOp::eGreaterOrEqual;
+        case(DepthTest::Equal):         return vk::CompareOp::eEqual;
+        default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown depth test enum"); return vk::CompareOp::eAlways;
+        }
+    }
+
     PipelineHandle PipelineManager::createPipeline(const PipelineConfig &config, PassManager& passManager)
     {
 		const vk::RenderPass &pass = passManager.getVkPass(config.m_PassHandle);
@@ -125,9 +145,9 @@ namespace vkcv
 
         // input assembly state
         vk::PipelineInputAssemblyStateCreateInfo pipelineInputAssemblyStateCreateInfo(
-                {},
-                vk::PrimitiveTopology::eTriangleList,
-                false
+            {},
+            primitiveTopologyToVulkanPrimitiveTopology(config.m_PrimitiveTopology),
+            false
         );
 
         // viewport state
@@ -135,13 +155,21 @@ namespace vkcv
         vk::Rect2D scissor({ 0,0 }, { config.m_Width, config.m_Height });
         vk::PipelineViewportStateCreateInfo pipelineViewportStateCreateInfo({}, 1, &viewport, 1, &scissor);
 
+        vk::CullModeFlags cullMode;
+        switch (config.m_culling) {
+            case CullMode::None:    cullMode = vk::CullModeFlagBits::eNone;     break;
+            case CullMode::Front:   cullMode = vk::CullModeFlagBits::eFront;    break;
+            case CullMode::Back:    cullMode = vk::CullModeFlagBits::eBack;     break;
+			default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown CullMode"); cullMode = vk::CullModeFlagBits::eNone;
+        }
+
         // rasterization state
         vk::PipelineRasterizationStateCreateInfo pipelineRasterizationStateCreateInfo(
                 {},
-                false,
+                config.m_EnableDepthClamping,
                 false,
                 vk::PolygonMode::eFill,
-                vk::CullModeFlagBits::eNone,
+                cullMode,
                 vk::FrontFace::eCounterClockwise,
                 false,
                 0.f,
@@ -149,15 +177,23 @@ namespace vkcv
                 0.f,
                 1.f
         );
+        vk::PipelineRasterizationConservativeStateCreateInfoEXT conservativeRasterization;
+        if (config.m_UseConservativeRasterization) {
+            conservativeRasterization = vk::PipelineRasterizationConservativeStateCreateInfoEXT(
+                {}, 
+                vk::ConservativeRasterizationModeEXT::eOverestimate,
+                0.f);
+            pipelineRasterizationStateCreateInfo.pNext = &conservativeRasterization;
+        }
 
         // multisample state
         vk::PipelineMultisampleStateCreateInfo pipelineMultisampleStateCreateInfo(
                 {},
-                vk::SampleCountFlagBits::e1,
+                msaaToVkSampleCountFlag(config.m_multisampling),
                 false,
                 0.f,
                 nullptr,
-                false,
+                config.m_alphaToCoverage,
                 false
         );
 
@@ -166,8 +202,11 @@ namespace vkcv
                                                VK_COLOR_COMPONENT_G_BIT |
                                                VK_COLOR_COMPONENT_B_BIT |
                                                VK_COLOR_COMPONENT_A_BIT);
+
+        // currently set to additive, if not disabled
+        // BlendFactors must be set as soon as additional BlendModes are added
         vk::PipelineColorBlendAttachmentState colorBlendAttachmentState(
-                false,
+                config.m_blendMode != BlendMode::None,
                 vk::BlendFactor::eOne,
                 vk::BlendFactor::eOne,
                 vk::BlendOp::eAdd,
@@ -185,14 +224,18 @@ namespace vkcv
                 { 1.f,1.f,1.f,1.f }
         );
 
-		const size_t matrixPushConstantSize = config.m_ShaderProgram.getPushConstantSize();
-		const vk::PushConstantRange pushConstantRange(vk::ShaderStageFlagBits::eAll, 0, matrixPushConstantSize);
+		const size_t pushConstantSize = config.m_ShaderProgram.getPushConstantSize();
+		const vk::PushConstantRange pushConstantRange(vk::ShaderStageFlagBits::eAll, 0, pushConstantSize);
 
         // pipeline layout
         vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo(
 			{},
 			(config.m_DescriptorLayouts),
 			(pushConstantRange));
+		if (pushConstantSize == 0) {
+			pipelineLayoutCreateInfo.pushConstantRangeCount = 0;
+		}
+
 
         vk::PipelineLayout vkPipelineLayout{};
         if (m_Device.createPipelineLayout(&pipelineLayoutCreateInfo, nullptr, &vkPipelineLayout) != vk::Result::eSuccess)
@@ -204,9 +247,9 @@ namespace vkcv
 	
 		const vk::PipelineDepthStencilStateCreateInfo depthStencilCreateInfo(
 				vk::PipelineDepthStencilStateCreateFlags(),
-				true,
-				true,
-				vk::CompareOp::eLessOrEqual,
+				config.m_depthTest != DepthTest::None,
+				config.m_depthWrite,
+				depthTestToVkCompareOp(config.m_depthTest),
 				false,
 				false,
 				{},
@@ -239,6 +282,20 @@ namespace vkcv
 
         // graphics pipeline create
         std::vector<vk::PipelineShaderStageCreateInfo> shaderStages = { pipelineVertexShaderStageInfo, pipelineFragmentShaderStageInfo };
+
+		const char *geometryShaderName = "main";	// outside of if to make sure it stays in scope
+		vk::ShaderModule geometryModule;
+		if (config.m_ShaderProgram.existsShader(ShaderStage::GEOMETRY)) {
+			const vkcv::Shader geometryShader = config.m_ShaderProgram.getShader(ShaderStage::GEOMETRY);
+			const auto& geometryCode = geometryShader.shaderCode;
+			const vk::ShaderModuleCreateInfo geometryModuleInfo({}, geometryCode.size(), reinterpret_cast<const uint32_t*>(geometryCode.data()));
+			if (m_Device.createShaderModule(&geometryModuleInfo, nullptr, &geometryModule) != vk::Result::eSuccess) {
+				return PipelineHandle();
+			}
+			vk::PipelineShaderStageCreateInfo geometryStage({}, vk::ShaderStageFlagBits::eGeometry, geometryModule, geometryShaderName);
+			shaderStages.push_back(geometryStage);
+		}
+
         const vk::GraphicsPipelineCreateInfo graphicsPipelineCreateInfo(
                 {},
                 static_cast<uint32_t>(shaderStages.size()),
@@ -264,15 +321,21 @@ namespace vkcv
         {
             m_Device.destroy(vertexModule);
             m_Device.destroy(fragmentModule);
+            if (geometryModule) {
+                m_Device.destroy(geometryModule);
+            }
+            m_Device.destroy();
             return PipelineHandle();
         }
 
         m_Device.destroy(vertexModule);
         m_Device.destroy(fragmentModule);
+        if (geometryModule) {
+            m_Device.destroy(geometryModule);
+        }
         
         const uint64_t id = m_Pipelines.size();
-        m_Pipelines.push_back({ vkPipeline, vkPipelineLayout });
-        m_Configs.push_back(config);
+        m_Pipelines.push_back({ vkPipeline, vkPipelineLayout, config });
         return PipelineHandle(id, [&](uint64_t id) { destroyPipelineById(id); });
     }
 
@@ -320,10 +383,17 @@ namespace vkcv
 		}
     }
 
-    const PipelineConfig &PipelineManager::getPipelineConfig(const PipelineHandle &handle) const
+    const PipelineConfig& PipelineManager::getPipelineConfig(const PipelineHandle &handle) const
     {
         const uint64_t id = handle.getId();
-        return m_Configs.at(id);
+        
+        if (id >= m_Pipelines.size()) {
+        	static PipelineConfig dummyConfig;
+			vkcv_log(LogLevel::ERROR, "Invalid handle");
+			return dummyConfig;
+        }
+        
+        return m_Pipelines[id].m_config;
     }
 
     PipelineHandle PipelineManager::createComputePipeline(
@@ -373,7 +443,7 @@ namespace vkcv
         m_Device.destroy(computeModule);
 
         const uint64_t id = m_Pipelines.size();
-        m_Pipelines.push_back({ vkPipeline, vkPipelineLayout });
+        m_Pipelines.push_back({ vkPipeline, vkPipelineLayout, PipelineConfig() });
 
         return PipelineHandle(id, [&](uint64_t id) { destroyPipelineById(id); });
     }
diff --git a/src/vkcv/PipelineManager.hpp b/src/vkcv/PipelineManager.hpp
index 634f5f4e6464532306e35fd10d9a1623df6ace16..b153eb4632b844e84b92953fe8abf6666a13e0c9 100644
--- a/src/vkcv/PipelineManager.hpp
+++ b/src/vkcv/PipelineManager.hpp
@@ -14,11 +14,11 @@ namespace vkcv
     	struct Pipeline {
 			vk::Pipeline m_handle;
 			vk::PipelineLayout m_layout;
+			PipelineConfig m_config;
     	};
     	
         vk::Device m_Device;
         std::vector<Pipeline> m_Pipelines;
-        std::vector<PipelineConfig> m_Configs;
         
         void destroyPipelineById(uint64_t id);
 
diff --git a/src/vkcv/SamplerManager.cpp b/src/vkcv/SamplerManager.cpp
index eb44356f2cbee1caaf4cb0635c8c8937890b06f9..a6ebb95b5e237dcd06ed8041b3f16489f7339d6a 100644
--- a/src/vkcv/SamplerManager.cpp
+++ b/src/vkcv/SamplerManager.cpp
@@ -87,7 +87,7 @@ namespace vkcv {
 				false,
 				vk::CompareOp::eAlways,
 				0.0f,
-				1.0f,
+				16.0f,
 				vk::BorderColor::eIntOpaqueBlack,
 				false
 		);
diff --git a/src/vkcv/ShaderProgram.cpp b/src/vkcv/ShaderProgram.cpp
index aa945bb18e7cf04513b41510f1ea993a02e1f46d..971797d9a42d071a1730ebf31a0b554f92fa361f 100644
--- a/src/vkcv/ShaderProgram.cpp
+++ b/src/vkcv/ShaderProgram.cpp
@@ -14,17 +14,21 @@ namespace vkcv {
      * @param[in] relative path to the shader code
      * @return vector of chars as a buffer for the code
      */
-	std::vector<char> readShaderCode(const std::filesystem::path &shaderPath)
-	{
-		std::ifstream file(shaderPath.string(), std::ios::ate | std::ios::binary);
+	std::vector<char> readShaderCode(const std::filesystem::path &shaderPath) {
+		std::ifstream file (shaderPath.string(), std::ios::ate | std::ios::binary);
+		
 		if (!file.is_open()) {
 			vkcv_log(LogLevel::ERROR, "The file could not be opened");
 			return std::vector<char>{};
 		}
+		
 		size_t fileSize = (size_t)file.tellg();
 		std::vector<char> buffer(fileSize);
+		
 		file.seekg(0);
 		file.read(buffer.data(), fileSize);
+		file.close();
+		
         return buffer;
 	}
 
@@ -209,8 +213,7 @@ namespace vkcv {
         return m_VertexAttachments;
 	}
 
-    const std::vector<std::vector<DescriptorBinding>> &ShaderProgram::getReflectedDescriptors() const
-    {
+    const std::vector<std::vector<DescriptorBinding>>& ShaderProgram::getReflectedDescriptors() const {
         return m_DescriptorSets;
     }
 
diff --git a/src/vkcv/SwapChain.cpp b/src/vkcv/Swapchain.cpp
similarity index 81%
rename from src/vkcv/SwapChain.cpp
rename to src/vkcv/Swapchain.cpp
index b787536b66cfd802dfd435a773a584c875eeb391..94e7301d66bfcc513434ef6d22520d1b95f98161 100644
--- a/src/vkcv/SwapChain.cpp
+++ b/src/vkcv/Swapchain.cpp
@@ -1,7 +1,6 @@
-#include <vkcv/SwapChain.hpp>
+#include <vkcv/Swapchain.hpp>
 #include <utility>
 
-#define GLFW_INCLUDE_VULKAN
 #include <GLFW/glfw3.h>
 
 namespace vkcv
@@ -27,44 +26,44 @@ namespace vkcv
         return vk::SurfaceKHR(surface);
     }
 
-    SwapChain::SwapChain(const Surface &surface,
+    Swapchain::Swapchain(const Surface &surface,
                          vk::SwapchainKHR swapchain,
                          vk::Format format,
                          vk::ColorSpaceKHR colorSpace,
                          vk::PresentModeKHR presentMode,
                          uint32_t imageCount,
 						 vk::Extent2D extent) noexcept :
-    m_Surface(surface),
-    m_Swapchain(swapchain),
-    m_SwapchainFormat(format),
-    m_SwapchainColorSpace(colorSpace),
-    m_SwapchainPresentMode(presentMode),
-    m_SwapchainImageCount(imageCount),
-	m_Extent(extent),
-    m_RecreationRequired(false)
+			m_Surface(surface),
+			m_Swapchain(swapchain),
+			m_Format(format),
+			m_ColorSpace(colorSpace),
+			m_PresentMode(presentMode),
+			m_ImageCount(imageCount),
+			m_Extent(extent),
+			m_RecreationRequired(false)
     {}
     
-    SwapChain::SwapChain(const SwapChain &other) :
+    Swapchain::Swapchain(const Swapchain &other) :
 			m_Surface(other.m_Surface),
 			m_Swapchain(other.m_Swapchain),
-			m_SwapchainFormat(other.m_SwapchainFormat),
-			m_SwapchainColorSpace(other.m_SwapchainColorSpace),
-			m_SwapchainPresentMode(other.m_SwapchainPresentMode),
-			m_SwapchainImageCount(other.m_SwapchainImageCount),
+			m_Format(other.m_Format),
+			m_ColorSpace(other.m_ColorSpace),
+			m_PresentMode(other.m_PresentMode),
+			m_ImageCount(other.m_ImageCount),
 			m_Extent(other.m_Extent),
 			m_RecreationRequired(other.m_RecreationRequired.load())
 	{}
 
-    const vk::SwapchainKHR& SwapChain::getSwapchain() const {
+    const vk::SwapchainKHR& Swapchain::getSwapchain() const {
         return m_Swapchain;
     }
 
-    vk::SurfaceKHR SwapChain::getSurface() const {
+    vk::SurfaceKHR Swapchain::getSurface() const {
         return m_Surface.handle;
     }
 
-    vk::Format SwapChain::getSwapchainFormat() const{
-        return m_SwapchainFormat;
+    vk::Format Swapchain::getFormat() const{
+        return m_Format;
     }
 
     /**
@@ -79,10 +78,13 @@ namespace vkcv
         if(physicalDevice.getSurfaceCapabilitiesKHR(surface,&surfaceCapabilities) != vk::Result::eSuccess){
             throw std::runtime_error("cannot get surface capabilities. There is an issue with the surface.");
         }
-
+        
+        int fb_width, fb_height;
+        window.getFramebufferSize(fb_width, fb_height);
+        
         VkExtent2D extent2D = {
-                static_cast<uint32_t>(window.getWidth()),
-                static_cast<uint32_t>(window.getHeight())
+                static_cast<uint32_t>(fb_width),
+                static_cast<uint32_t>(fb_height)
         };
         
         extent2D.width = std::max(surfaceCapabilities.minImageExtent.width, std::min(surfaceCapabilities.maxImageExtent.width, extent2D.width));
@@ -162,7 +164,7 @@ namespace vkcv
      * @param context that keeps instance, physicalDevice and a device.
      * @return swapchain
      */
-    SwapChain SwapChain::create(const Window &window, const Context &context) {
+    Swapchain Swapchain::create(const Window &window, const Context &context) {
         const vk::Instance& instance = context.getInstance();
         const vk::PhysicalDevice& physicalDevice = context.getPhysicalDevice();
         const vk::Device& device = context.getDevice();
@@ -186,7 +188,7 @@ namespace vkcv
                 chosenSurfaceFormat.colorSpace,   // imageColorSpace
                 chosenExtent,   // imageExtent
                 1,  // imageArrayLayers TODO: should we only allow non-stereoscopic applications? yes -> 1, no -> ? "must be greater than 0, less or equal to maxImageArrayLayers"
-                vk::ImageUsageFlagBits::eColorAttachment,  // imageUsage TODO: what attachments? only color? depth?
+                vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eStorage,  // imageUsage TODO: what attachments? only color? depth?
                 vk::SharingMode::eExclusive,    // imageSharingMode TODO: which sharing mode? "VK_SHARING_MODE_EXCLUSIV access exclusive to a single queue family, better performance", "VK_SHARING_MODE_CONCURRENT access from multiple queues"
                 0,  // queueFamilyIndexCount, the number of queue families having access to the image(s) of the swapchain when imageSharingMode is VK_SHARING_MODE_CONCURRENT
                 nullptr,    // pQueueFamilyIndices, the pointer to an array of queue family indices having access to the images(s) of the swapchain when imageSharingMode is VK_SHARING_MODE_CONCURRENT
@@ -199,7 +201,7 @@ namespace vkcv
 
         vk::SwapchainKHR swapchain = device.createSwapchainKHR(swapchainCreateInfo);
 
-        return SwapChain(surface,
+        return Swapchain(surface,
                          swapchain,
                          chosenSurfaceFormat.format,
                          chosenSurfaceFormat.colorSpace,
@@ -208,55 +210,64 @@ namespace vkcv
 						 chosenExtent);
     }
     
-    bool SwapChain::shouldUpdateSwapchain() const {
+    bool Swapchain::shouldUpdateSwapchain() const {
     	return m_RecreationRequired;
     }
     
-    void SwapChain::updateSwapchain(const Context &context, const Window &window) {
+    void Swapchain::updateSwapchain(const Context &context, const Window &window) {
     	if (!m_RecreationRequired.exchange(false))
     		return;
     	
 		vk::SwapchainKHR oldSwapchain = m_Swapchain;
 		vk::Extent2D extent2D = chooseExtent(context.getPhysicalDevice(), m_Surface.handle, window);
 	
-		vk::SwapchainCreateInfoKHR swapchainCreateInfo(
-				vk::SwapchainCreateFlagsKHR(),
-				m_Surface.handle,
-				m_SwapchainImageCount,
-				m_SwapchainFormat,
-				m_SwapchainColorSpace,
-				extent2D,
-				1,
-				vk::ImageUsageFlagBits::eColorAttachment,
-				vk::SharingMode::eExclusive,
-				0,
-				nullptr,
-				vk::SurfaceTransformFlagBitsKHR::eIdentity,
-				vk::CompositeAlphaFlagBitsKHR::eOpaque,
-				m_SwapchainPresentMode,
-				true,
-				oldSwapchain
-		);
-	
-		m_Swapchain = context.getDevice().createSwapchainKHR(swapchainCreateInfo);
-		context.getDevice().destroySwapchainKHR(oldSwapchain);
+		if ((extent2D.width >= MIN_SWAPCHAIN_SIZE) && (extent2D.height >= MIN_SWAPCHAIN_SIZE)) {
+			vk::SwapchainCreateInfoKHR swapchainCreateInfo(
+					vk::SwapchainCreateFlagsKHR(),
+					m_Surface.handle,
+					m_ImageCount,
+					m_Format,
+					m_ColorSpace,
+					extent2D,
+					1,
+					vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eStorage,
+					vk::SharingMode::eExclusive,
+					0,
+					nullptr,
+					vk::SurfaceTransformFlagBitsKHR::eIdentity,
+					vk::CompositeAlphaFlagBitsKHR::eOpaque,
+					m_PresentMode,
+					true,
+					oldSwapchain
+			);
+			
+			m_Swapchain = context.getDevice().createSwapchainKHR(swapchainCreateInfo);
+		} else {
+			m_Swapchain = nullptr;
+			
+			signalSwapchainRecreation();
+		}
+		
+		if (oldSwapchain) {
+			context.getDevice().destroySwapchainKHR(oldSwapchain);
+		}
 		
 		m_Extent = extent2D;
     }
 
-    void SwapChain::signalSwapchainRecreation() {
+    void Swapchain::signalSwapchainRecreation() {
 		m_RecreationRequired = true;
     }
     
-    const vk::Extent2D& SwapChain::getExtent() const {
+    const vk::Extent2D& Swapchain::getExtent() const {
     	return m_Extent;
     }
 
-    SwapChain::~SwapChain() {
+    Swapchain::~Swapchain() {
         // needs to be destroyed by creator
     }
 
-	uint32_t SwapChain::getImageCount() {
-		return m_SwapchainImageCount;
+	uint32_t Swapchain::getImageCount() const {
+		return m_ImageCount;
 	}
 }
diff --git a/src/vkcv/Window.cpp b/src/vkcv/Window.cpp
index 210178fa0545caea061c8673323fe7e11f3ef2c8..ea72582d67d5350e5fbf3f3c0fa2aae2ba407b0e 100644
--- a/src/vkcv/Window.cpp
+++ b/src/vkcv/Window.cpp
@@ -5,15 +5,22 @@
  */
 
 #include <GLFW/glfw3.h>
-
 #include "vkcv/Window.hpp"
 
 namespace vkcv {
 
 	static std::vector<GLFWwindow*> s_Windows;
 
-    Window::Window(GLFWwindow *window)
-            : m_window(window) {
+    Window::Window(GLFWwindow *window) :
+    m_window(window),
+	e_mouseButton(true),
+	e_mouseMove(true),
+	e_mouseScroll(true),
+	e_resize(true),
+	e_key(true),
+	e_char(true),
+	e_gamepad(true)
+    {
 		glfwSetWindowUserPointer(m_window, this);
 	
 		// combine Callbacks with Events
@@ -22,9 +29,18 @@ namespace vkcv {
 		glfwSetWindowSizeCallback(m_window, Window::onResize);
 		glfwSetKeyCallback(m_window, Window::onKeyEvent);
 		glfwSetScrollCallback(m_window, Window::onMouseScrollEvent);
+		glfwSetCharCallback(m_window, Window::onCharEvent);
     }
 
     Window::~Window() {
+        Window::e_mouseButton.unlock();
+        Window::e_mouseMove.unlock();
+        Window::e_mouseScroll.unlock();
+        Window::e_resize.unlock();
+        Window::e_key.unlock();
+        Window::e_char.unlock();
+        Window::e_gamepad.unlock();
+
 		s_Windows.erase(std::find(s_Windows.begin(), s_Windows.end(), m_window));
         glfwDestroyWindow(m_window);
 
@@ -34,48 +50,58 @@ namespace vkcv {
     }
 
     Window Window::create( const char *windowTitle, int width, int height, bool resizable) {
-        if(s_Windows.empty()) {
-            glfwInit();
-        }
-
-        width = std::max(width, 1);
-        height = std::max(height, 1);
-
-        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
-        glfwWindowHint(GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE);
+		if(s_Windows.empty()) {
+			glfwInit();
+		}
+	
+		width = std::max(width, 1);
+		height = std::max(height, 1);
+	
+		glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+		glfwWindowHint(GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE);
 		GLFWwindow *window = glfwCreateWindow(width, height, windowTitle, nullptr, nullptr);
-		
+	
 		s_Windows.push_back(window);
-
-        return Window(window);
+	
+		return Window(window);
     }
 
     void Window::pollEvents() {
+
     	for (auto glfwWindow : s_Windows) {
 			auto window = static_cast<Window *>(glfwGetWindowUserPointer(glfwWindow));
 			
 			window->e_mouseButton.unlock();
 			window->e_mouseMove.unlock();
+			window->e_mouseScroll.unlock();
 			window->e_resize.unlock();
 			window->e_key.unlock();
-			window->e_mouseScroll.unlock();
+			window->e_char.unlock();
+			window->e_gamepad.unlock();
     	}
-    	
+
         glfwPollEvents();
+    	
+    	for (int gamepadIndex = GLFW_JOYSTICK_1; gamepadIndex <= GLFW_JOYSTICK_LAST; gamepadIndex++) {
+    		if (glfwJoystickPresent(gamepadIndex)) {
+				onGamepadEvent(gamepadIndex);
+			}
+		}
 	
 		for (auto glfwWindow : s_Windows) {
 			auto window = static_cast<Window *>(glfwGetWindowUserPointer(glfwWindow));
 		
 			window->e_mouseButton.lock();
 			window->e_mouseMove.lock();
+			window->e_mouseScroll.lock();
 			window->e_resize.lock();
 			window->e_key.lock();
-			window->e_mouseScroll.lock();
+			window->e_char.lock();
+			window->e_gamepad.lock();
 		}
     }
 
     void Window::onMouseButtonEvent(GLFWwindow *callbackWindow, int button, int action, int mods) {
-
         auto window = static_cast<Window *>(glfwGetWindowUserPointer(callbackWindow));
 
         if (window != nullptr) {
@@ -84,7 +110,6 @@ namespace vkcv {
     }
 
     void Window::onMouseMoveEvent(GLFWwindow *callbackWindow, double x, double y) {
-
         auto window = static_cast<Window *>(glfwGetWindowUserPointer(callbackWindow));
 
         if (window != nullptr) {
@@ -101,7 +126,6 @@ namespace vkcv {
     }
 
     void Window::onResize(GLFWwindow *callbackWindow, int width, int height) {
-
         auto window = static_cast<Window *>(glfwGetWindowUserPointer(callbackWindow));
 
         if (window != nullptr) {
@@ -110,13 +134,33 @@ namespace vkcv {
     }
 
     void Window::onKeyEvent(GLFWwindow *callbackWindow, int key, int scancode, int action, int mods) {
-
         auto window = static_cast<Window *>(glfwGetWindowUserPointer(callbackWindow));
 
         if (window != nullptr) {
             window->e_key(key, scancode, action, mods);
         }
     }
+    
+    void Window::onCharEvent(GLFWwindow *callbackWindow, unsigned int c) {
+		auto window = static_cast<Window *>(glfwGetWindowUserPointer(callbackWindow));
+	
+		if (window != nullptr) {
+			window->e_char(c);
+		}
+    }
+
+    void Window::onGamepadEvent(int gamepadIndex) {
+        int activeWindowIndex = std::find_if(s_Windows.begin(),
+                                             s_Windows.end(),
+                                             [](GLFWwindow* window){return glfwGetWindowAttrib(window, GLFW_FOCUSED);})
+                                - s_Windows.begin();
+        activeWindowIndex *= (activeWindowIndex < s_Windows.size());    // fixes index getting out of bounds (e.g. if there is no focused window)
+        auto window = static_cast<Window *>(glfwGetWindowUserPointer(s_Windows[activeWindowIndex]));
+
+        if (window != nullptr) {
+            window->e_gamepad(gamepadIndex);
+        }
+    }
 
     bool Window::isWindowOpen() const {
         return !glfwWindowShouldClose(m_window);
@@ -137,4 +181,9 @@ namespace vkcv {
     GLFWwindow *Window::getWindow() const {
         return m_window;
     }
-}
\ No newline at end of file
+    
+    void Window::getFramebufferSize(int &width, int &height) const {
+		glfwGetFramebufferSize(m_window, &width, &height);
+    }
+    
+}