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/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index dab18892b892aff9564a6d86b9252789ea3c2b03..c7512346c9137b77c365e807b679b3950925f535 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -41,7 +41,6 @@ namespace vkcv
 		QueueType queueType;
 		std::vector<vk::Semaphore> waitSemaphores;
 		std::vector<vk::Semaphore> signalSemaphores;
-		vk::Fence fence;
 	};
 
     class Core final
@@ -215,7 +214,14 @@ namespace vkcv
          * @return Image-Object
          */
         [[nodiscard]]
-        Image createImage(vk::Format format, uint32_t width, uint32_t height, uint32_t depth = 1, bool supportStorage = false, bool supportColorAttachment = false);
+        Image createImage(
+			vk::Format  format,
+			uint32_t    width,
+			uint32_t    height,
+			uint32_t    depth = 1,
+			bool        createMipChain = false,
+			bool        supportStorage = false,
+			bool        supportColorAttachment = false);
 
         /** TODO:
          *   @param setDescriptions
@@ -261,7 +267,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);
@@ -280,7 +286,7 @@ namespace vkcv
 		void recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image);
 		void recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer);
 		
-		const vk::ImageView& getSwapchainImageView() const;
+		vk::ImageView getSwapchainImageView() const;
 		
     };
 }
diff --git a/include/vkcv/DescriptorWrites.hpp b/include/vkcv/DescriptorWrites.hpp
index c9d046566a664ffea419dad2b293e206d0b2cdc5..7cc76c6960b536ff9d207d773e7c75d010e503d6 100644
--- a/include/vkcv/DescriptorWrites.hpp
+++ b/include/vkcv/DescriptorWrites.hpp
@@ -10,9 +10,11 @@ namespace vkcv {
 	};
 
 	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 {
diff --git a/include/vkcv/Image.hpp b/include/vkcv/Image.hpp
index 1795b63e844a002564932f5d7ef839746e32fae5..9e1f9708c6318aa6deb750097c414358ffde2c65 100644
--- a/include/vkcv/Image.hpp
+++ b/include/vkcv/Image.hpp
@@ -32,17 +32,30 @@ namespace vkcv {
 
 		[[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;
 
 		Image(ImageManager* manager, const ImageHandle& handle);
 		
-		static Image create(ImageManager* manager, vk::Format format, uint32_t width, uint32_t height, uint32_t depth, bool supportStorage, bool supportColorAttachment);
+		static Image create(
+			ImageManager* manager,
+			vk::Format format,
+			uint32_t width,
+			uint32_t height,
+			uint32_t depth,
+			uint32_t mipCount,
+			bool supportStorage,
+			bool supportColorAttachment);
 		
 	};
 	
diff --git a/modules/gui/src/vkcv/gui/GUI.cpp b/modules/gui/src/vkcv/gui/GUI.cpp
index 6a08cb4f02551cae2fd5d1e3ea4e6ff0bd2b2e04..a9a8bd01df379a0f4615c2c53aee09082d107b1c 100644
--- a/modules/gui/src/vkcv/gui/GUI.cpp
+++ b/modules/gui/src/vkcv/gui/GUI.cpp
@@ -141,7 +141,7 @@ namespace vkcv::gui {
 		
 		const SubmitInfo submitInfo { QueueType::Graphics, {}, {} };
 		
-		m_core.recordAndSubmitCommands(submitInfo, [](const vk::CommandBuffer& commandBuffer) {
+		m_core.recordAndSubmitCommandsImmediate(submitInfo, [](const vk::CommandBuffer& commandBuffer) {
 			ImGui_ImplVulkan_CreateFontsTexture(commandBuffer);
 		}, []() {
 			ImGui_ImplVulkan_DestroyFontUploadObjects();
@@ -214,7 +214,7 @@ namespace vkcv::gui {
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Graphics;
 		
-		m_core.recordAndSubmitCommands(submitInfo, [&](const vk::CommandBuffer& commandBuffer) {
+		m_core.recordAndSubmitCommandsImmediate(submitInfo, [&](const vk::CommandBuffer& commandBuffer) {
 			const vk::Rect2D renderArea (
 					vk::Offset2D(0, 0),
 					extent
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index d9650f3577cf5633ac915f435b59367a9f993d62..74e6de3ff6d9f80d764b774fea5dc55b4b53b2f8 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -123,6 +123,8 @@ int main(int argc, const char** argv) {
 	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,
@@ -142,7 +144,7 @@ int main(int argc, const char** argv) {
 
 	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();
 
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index 33cfaef6197079b72ab2f295a4503e80be724db4..bc87996096226af4e3f3d05c3e10bb287c61cc8d 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -26,7 +26,7 @@ if(MSVC)
 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})
+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)
+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/shaders/lightInfo.inc b/projects/voxelization/resources/shaders/lightInfo.inc
new file mode 100644
index 0000000000000000000000000000000000000000..4345d4f1504d27df7392b34bcaf17efdcfecef33
--- /dev/null
+++ b/projects/voxelization/resources/shaders/lightInfo.inc
@@ -0,0 +1,6 @@
+struct LightInfo{
+    vec3 L;             float padding;
+    vec3 sunColor;      
+    float sunStrength;
+    mat4 lightMatrix;
+};
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/shader.frag b/projects/voxelization/resources/shaders/shader.frag
index edafc8c0077416cb57aedc4e7358126846507e18..8653ae5958ce3b42eac6b1eaa6813f85b6ed589c 100644
--- a/projects/voxelization/resources/shaders/shader.frag
+++ b/projects/voxelization/resources/shaders/shader.frag
@@ -3,6 +3,8 @@
 #extension GL_GOOGLE_include_directive : enable
 
 #include "perMeshResources.inc"
+#include "lightInfo.inc"
+#include "shadowMapping.inc"
 
 layout(location = 0) in vec3 passNormal;
 layout(location = 1) in vec2 passUV;
@@ -11,35 +13,16 @@ layout(location = 2) in vec3 passPos;
 layout(location = 0) out vec3 outColor;
 
 layout(set=0, binding=0) uniform sunBuffer {
-    vec3 L; float padding;
-    mat4 lightMatrix;
+    LightInfo lightInfo;
 };
 layout(set=0, binding=1) uniform texture2D  shadowMap;
 layout(set=0, binding=2) uniform sampler    shadowMapSampler;
 
-float shadowTest(vec3 worldPos){
-    vec4 lightPos = lightMatrix * vec4(worldPos, 1);
-    lightPos /= lightPos.w;
-    lightPos.xy = lightPos.xy * 0.5 + 0.5;
-    
-    if(any(lessThan(lightPos.xy, vec2(0))) || any(greaterThan(lightPos.xy, vec2(1)))){
-        return 1;
-    }
-    
-    lightPos.z = clamp(lightPos.z, 0, 1);
-    
-    float shadowMapSample = texture(sampler2D(shadowMap, shadowMapSampler), lightPos.xy).r;
-    float bias = 0.01f;
-    shadowMapSample += bias;
-    return shadowMapSample < lightPos.z ? 0 : 1;
-}
-
 void main()	{
-    vec3 N = normalize(passNormal);
-    vec3 sunColor = vec3(1);
-    vec3 sun = sunColor * clamp(dot(N, L), 0, 1);
-    sun *= shadowTest(passPos);
-    vec3 ambient = vec3(0.1);
-    vec3 albedo = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
-	outColor = albedo * (sun + ambient);
+    vec3 N          = normalize(passNormal);
+    vec3 sun        = lightInfo.sunStrength * lightInfo.sunColor * clamp(dot(N, lightInfo.L), 0, 1);
+    sun             *= shadowTest(passPos, lightInfo, shadowMap, shadowMapSampler);
+    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/voxelization/resources/shaders/shadowMapping.inc b/projects/voxelization/resources/shaders/shadowMapping.inc
new file mode 100644
index 0000000000000000000000000000000000000000..1fa34a388c35b96a3316e972ca562d35e2c3cf90
--- /dev/null
+++ b/projects/voxelization/resources/shaders/shadowMapping.inc
@@ -0,0 +1,16 @@
+float shadowTest(vec3 worldPos, LightInfo lightInfo, texture2D shadowMap, sampler shadowMapSampler){
+    vec4 lightPos = lightInfo.lightMatrix * vec4(worldPos, 1);
+    lightPos /= lightPos.w;
+    lightPos.xy = lightPos.xy * 0.5 + 0.5;
+    
+    if(any(lessThan(lightPos.xy, vec2(0))) || any(greaterThan(lightPos.xy, vec2(1)))){
+        return 1;
+    }
+    
+    lightPos.z = clamp(lightPos.z, 0, 1);
+    
+    float shadowMapSample = texture(sampler2D(shadowMap, shadowMapSampler), lightPos.xy).r;
+    float bias = 0.01f;
+    shadowMapSample += bias;
+    return shadowMapSample < lightPos.z ? 0 : 1;
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/gammaCorrection.comp b/projects/voxelization/resources/shaders/tonemapping.comp
similarity index 60%
rename from projects/voxelization/resources/shaders/gammaCorrection.comp
rename to projects/voxelization/resources/shaders/tonemapping.comp
index 411a59c3e38b3414adbda260803c3f3322b16ff2..2383302fa946e7d92871039daff28232df2eafdd 100644
--- a/projects/voxelization/resources/shaders/gammaCorrection.comp
+++ b/projects/voxelization/resources/shaders/tonemapping.comp
@@ -11,8 +11,9 @@ void main(){
     if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(inImage)))){
         return;
     }
-    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
-    vec3 linearColor = imageLoad(inImage, uv).rgb;
-    vec3 gammaCorrected = pow(linearColor, vec3(1.f / 2.2f));
+    ivec2 uv            = ivec2(gl_GlobalInvocationID.xy);
+    vec3 linearColor    = imageLoad(inImage, uv).rgb;
+    vec3 tonemapped     = linearColor / (linearColor + 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/voxelization/resources/shaders/voxel.inc b/projects/voxelization/resources/shaders/voxel.inc
index d2b4400235817e3be1739dc46857ab42f260ebf7..25c0a82bbc887913a4d69ccdeee2b0d8934828c8 100644
--- a/projects/voxelization/resources/shaders/voxel.inc
+++ b/projects/voxelization/resources/shaders/voxel.inc
@@ -7,12 +7,26 @@ uint flattenVoxelUVToIndex(ivec3 UV, ivec3 voxelImageSize){
     return UV.x + UV.y * voxelImageSize.x + UV.z *  voxelImageSize.x*  voxelImageSize.y;
 }
 
+// packed voxel data: 
+// 1 bit opacity
+// 7 bit exposure
+// 8 bit blue
+// 8 bit green
+// 8 bit red
+float maxExposure = 16.f;
+
 uint packVoxelInfo(vec3 color){
-    uint opaqueBit   = 1 << 31;     
-    uint redBits     = uint(color.r * 255);
-    uint greenBits   = uint(color.g * 255) << 8;
-    uint blueBits    = uint(color.b * 255) << 16;
-    return opaqueBit | redBits | greenBits | blueBits;
+    
+    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 unpackVoxelInfo(uint packed){
@@ -20,6 +34,9 @@ vec4 unpackVoxelInfo(uint packed){
     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.a =  packed  >> 31; 
+    
+    rgba.rgb *= (packed >> 24 & 0x0000007F) / 127.f * maxExposure; 
+    
     return rgba;
 }
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelization.frag b/projects/voxelization/resources/shaders/voxelization.frag
index 7ea161ce4f5a4d59bb3d50c78553df0dfb5ab4ec..a49b13185ec26b069661141cfdbbfbbe45d14fd3 100644
--- a/projects/voxelization/resources/shaders/voxelization.frag
+++ b/projects/voxelization/resources/shaders/voxelization.frag
@@ -4,9 +4,12 @@
 
 #include "voxel.inc"
 #include "perMeshResources.inc"
+#include "lightInfo.inc"
+#include "shadowMapping.inc"
 
-layout(location = 0) in     vec3 passPos;
-layout(location = 1) out    vec2 passUV;
+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{
     uint packedVoxelData[];
@@ -18,6 +21,13 @@ layout(set=0, binding=1) uniform voxelizationInfo{
 
 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;
+
 vec3 worldToVoxelCoordinates(vec3 world, VoxelInfo info){
     return (world - info.offset) / info.extent + 0.5f;
 }
@@ -34,7 +44,14 @@ void main()	{
         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);
+    vec3 color  = albedo * sun;
+    color = albedo * sun;
     
-    vec3 color = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
     atomicMax(packedVoxelData[flatIndex], packVoxelInfo(color));
 }
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/voxelization.geom b/projects/voxelization/resources/shaders/voxelization.geom
index 19e31e2d2d032b5a9e5c273f6420c6449be9203e..56542d960d65db6ca12c5f84837cb0c0a9ff0ded 100644
--- a/projects/voxelization/resources/shaders/voxelization.geom
+++ b/projects/voxelization/resources/shaders/voxelization.geom
@@ -6,9 +6,11 @@ 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
@@ -29,6 +31,7 @@ void main()	{
         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();
diff --git a/projects/voxelization/resources/shaders/voxelization.vert b/projects/voxelization/resources/shaders/voxelization.vert
index 7a43c08b64d3df384d3a7e627d789db9be99f680..1302a42441b5b9c8ea7d24f97d29b684e4d64993 100644
--- a/projects/voxelization/resources/shaders/voxelization.vert
+++ b/projects/voxelization/resources/shaders/voxelization.vert
@@ -7,6 +7,7 @@ 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;
@@ -17,4 +18,5 @@ void main()	{
 	gl_Position = mvp * vec4(inPosition, 1.0);
     passPos     = (model * vec4(inPosition, 1)).xyz;
     passUV      = inUV;
+    passN       = inNormal;
 }
\ No newline at end of file
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index a67177447ec3b795c3c65476e6881b067a33df84..a04131cedfdc508e14a3dbd97da642e1af48f8da 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -1,6 +1,7 @@
 #include "Voxelization.hpp"
 #include <vkcv/shader/GLSLCompiler.hpp>
 #include <glm/gtc/matrix_transform.hpp>
+#include <algorithm>
 
 vkcv::ShaderProgram loadVoxelizationShader() {
 	vkcv::shader::GLSLCompiler compiler;
@@ -62,11 +63,16 @@ const uint32_t voxelResolution = 128;
 uint32_t voxelCount = voxelResolution * voxelResolution * voxelResolution;
 const vk::Format voxelizationDummyFormat = vk::Format::eR8Unorm;
 
-Voxelization::Voxelization(vkcv::Core* corePtr, const Dependencies& dependencies) 
+Voxelization::Voxelization(
+	vkcv::Core* corePtr,
+	const Dependencies& dependencies,
+	vkcv::BufferHandle  lightInfoBuffer,
+	vkcv::ImageHandle   shadowMap,
+	vkcv::SamplerHandle shadowSampler)
 	:
 	m_corePtr(corePtr), 
-	m_voxelImage(m_corePtr->createImage(vk::Format::eR16G16B16A16Sfloat, voxelResolution, voxelResolution, voxelResolution, true)),
-	m_dummyRenderTarget(m_corePtr->createImage(voxelizationDummyFormat, voxelResolution, voxelResolution, 1, false, true)),
+	m_voxelImage(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)){
 
@@ -100,7 +106,12 @@ Voxelization::Voxelization(vkcv::Core* corePtr, const Dependencies& dependencies
 
 	vkcv::DescriptorWrites voxelizationDescriptorWrites;
 	voxelizationDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
-	voxelizationDescriptorWrites.uniformBufferWrites = { vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.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_voxelImage.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_voxelizationDescriptorSet, voxelizationDescriptorWrites);
 
@@ -143,13 +154,6 @@ Voxelization::Voxelization(vkcv::Core* corePtr, const Dependencies& dependencies
 		voxelIndexData.push_back(i);
 	}
 
-	vkcv::DescriptorWrites voxelVisualisationDescriptorWrite;
-	voxelVisualisationDescriptorWrite.storageImageWrites = 
-	{ vkcv::StorageImageDescriptorWrite(0, m_voxelImage.getHandle()) };
-	voxelVisualisationDescriptorWrite.uniformBufferWrites = 
-	{ vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()) };
-	m_corePtr->writeDescriptorSet(m_visualisationDescriptorSet, voxelVisualisationDescriptorWrite);
-
 	const vkcv::DescriptorSetUsage voxelizationDescriptorUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle);
 
 	vkcv::ShaderProgram resetVoxelShader = loadVoxelResetShader();
@@ -262,17 +266,32 @@ void Voxelization::voxelizeMeshes(
 		vkcv::PushConstantData(nullptr, 0));
 
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImage.getHandle());
+
+	m_voxelImage.recordMipChainGeneration(cmdStream);
 }
 
 void Voxelization::renderVoxelVisualisation(
-	vkcv::CommandStreamHandle cmdStream, 
-	const glm::mat4& viewProjectin,
-	const std::vector<vkcv::ImageHandle>& renderTargets) {
+	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, voxelCount),
+		vkcv::Mesh({}, nullptr, drawVoxelCount),
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle) });
 
 	m_corePtr->recordDrawcallsToCmdStream(
@@ -282,4 +301,8 @@ void Voxelization::renderVoxelVisualisation(
 		voxelVisualisationPushConstantData,
 		{ drawcall },
 		renderTargets);
+}
+
+void Voxelization::setVoxelExtent(float extent) {
+	m_voxelExtent = extent;
 }
\ No newline at end of file
diff --git a/projects/voxelization/src/Voxelization.hpp b/projects/voxelization/src/Voxelization.hpp
index f9b96998b39e24a3481d130efa68ebaa813b8256..25830b171edb9154e37b2d597c2bbbf2daea6b2e 100644
--- a/projects/voxelization/src/Voxelization.hpp
+++ b/projects/voxelization/src/Voxelization.hpp
@@ -9,7 +9,12 @@ public:
 		vk::Format          colorBufferFormat;
 		vk::Format          depthBufferFormat;
 	};
-	Voxelization(vkcv::Core* corePtr, const Dependencies& dependencies);
+	Voxelization(
+		vkcv::Core*         corePtr, 
+		const Dependencies& dependencies, 
+		vkcv::BufferHandle  lightInfoBuffer,
+		vkcv::ImageHandle   shadowMap,
+		vkcv::SamplerHandle shadowSampler);
 
 	void voxelizeMeshes(
 		vkcv::CommandStreamHandle                       cmdStream, 
@@ -21,7 +26,10 @@ public:
 	void renderVoxelVisualisation(
 		vkcv::CommandStreamHandle               cmdStream,
 		const glm::mat4&                        viewProjectin,
-		const std::vector<vkcv::ImageHandle>&   renderTargets);
+		const std::vector<vkcv::ImageHandle>&   renderTargets,
+		uint32_t                                mipLevel);
+
+	void setVoxelExtent(float extent);
 
 private:
 	vkcv::Core* m_corePtr;
@@ -55,5 +63,5 @@ private:
 	};
 	vkcv::Buffer<VoxelizationInfo> m_voxelInfoBuffer;
 
-	const float m_voxelExtent = 20.f;
+	float m_voxelExtent = 20.f;
 };
\ No newline at end of file
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index 81d99f8af37722735742c697171219e012f6b339..309c75d5cad862ed8bbd43cfd001c05a660caeec 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -8,12 +8,13 @@
 #include <vkcv/Logger.hpp>
 #include "Voxelization.hpp"
 #include <glm/glm.hpp>
+#include "vkcv/gui/GUI.hpp"
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Voxelization";
 
-	uint32_t windowWidth = 800;
-	uint32_t windowHeight = 600;
+	uint32_t windowWidth = 1280;
+	uint32_t windowHeight = 720;
 	
 	vkcv::Window window = vkcv::Window::create(
 		applicationName,
@@ -151,13 +152,15 @@ int main(int argc, const char** argv) {
 	);
 	const vk::Format shadowMapFormat = vk::Format::eD16Unorm;
 	const uint32_t shadowMapResolution = 1024;
-	const vkcv::Image shadowMap = core.createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1);
+	const vkcv::Image shadowMap = core.createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution);
 
 	// light info buffer
 	struct LightInfo {
-		glm::vec3 direction;
-		float padding;
-		glm::mat4 lightMatrix;
+		glm::vec3   direction;
+		float       padding;
+		glm::vec3   sunColor    = glm::vec3(1.f);
+		float       sunStrength = 8.f;
+		glm::mat4   lightMatrix;
 	};
 	LightInfo lightInfo;
 	vkcv::Buffer lightBuffer = core.createBuffer<LightInfo>(vkcv::BufferType::UNIFORM, sizeof(glm::vec3));
@@ -178,24 +181,25 @@ int main(int argc, const char** argv) {
 		vkcv::SamplerAddressMode::REPEAT
 	);
 
-	// prepare per mesh descriptor sets
-	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
+	// create descriptor sets
+	std::vector<vkcv::DescriptorSetHandle> materialDescriptorSets;
 	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];
 
+	for (const auto& material : scene.materials) {
 		int baseColorIndex = material.baseColor;
 		if (baseColorIndex < 0) {
 			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks base color");
 			baseColorIndex = 0;
 		}
 
+		materialDescriptorSets.push_back(core.createDescriptorSet(forwardProgram.getReflectedDescriptors()[1]));
+
 		vkcv::asset::Texture& sceneTexture = scene.textures[baseColorIndex];
 
-		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, sceneTexture.w, sceneTexture.h));
+		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, sceneTexture.w, sceneTexture.h, 1, true));
 		sceneImages.back().fill(sceneTexture.data.data());
+		sceneImages.back().generateMipChainImmediate();
+		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
 
 		vkcv::DescriptorWrites setWrites;
 		setWrites.sampledImageWrites = {
@@ -204,7 +208,12 @@ int main(int argc, const char** argv) {
 		setWrites.samplerWrites = {
 			vkcv::SamplerDescriptorWrite(1, colorSampler),
 		};
-		core.writeDescriptorSet(perMeshDescriptorSets.back(), setWrites);
+		core.writeDescriptorSet(materialDescriptorSets.back(), setWrites);
+	}
+
+	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		perMeshDescriptorSets.push_back(materialDescriptorSets[vertexGroup.materialIndex]);
 	}
 
 	const vkcv::PipelineConfig forwardPipelineConfig {
@@ -226,7 +235,7 @@ int main(int argc, const char** argv) {
 	}
 
 	vkcv::ImageHandle depthBuffer = core.createImage(depthBufferFormat, windowWidth, windowHeight).getHandle();
-	vkcv::ImageHandle colorBuffer = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, true, true).getHandle();
+	vkcv::ImageHandle colorBuffer = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
@@ -266,15 +275,15 @@ int main(int argc, const char** argv) {
 		}
 	});
 
-	// gamma correction compute shader
-	vkcv::ShaderProgram gammaCorrectionProgram;
-	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/gammaCorrection.comp", 
+	// tonemapping compute shader
+	vkcv::ShaderProgram tonemappingProgram;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/tonemapping.comp", 
 		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		gammaCorrectionProgram.addShader(shaderStage, path);
+		tonemappingProgram.addShader(shaderStage, path);
 	});
-	vkcv::DescriptorSetHandle gammaCorrectionDescriptorSet = core.createDescriptorSet(gammaCorrectionProgram.getReflectedDescriptors()[0]);
-	vkcv::PipelineHandle gammaCorrectionPipeline = core.createComputePipeline(gammaCorrectionProgram, 
-		{ core.getDescriptorSet(gammaCorrectionDescriptorSet).layout });
+	vkcv::DescriptorSetHandle tonemappingDescriptorSet = core.createDescriptorSet(tonemappingProgram.getReflectedDescriptors()[0]);
+	vkcv::PipelineHandle tonemappingPipeline = core.createComputePipeline(tonemappingProgram,
+		{ core.getDescriptorSet(tonemappingDescriptorSet).layout });
 
 	// model matrices per mesh
 	std::vector<glm::mat4> modelMatrices;
@@ -310,7 +319,18 @@ int main(int argc, const char** argv) {
 	voxelDependencies.colorBufferFormat = colorBufferFormat;
 	voxelDependencies.depthBufferFormat = depthBufferFormat;
 	voxelDependencies.vertexLayout = vertexLayout;
-	Voxelization voxelization(&core, voxelDependencies);
+	Voxelization voxelization(
+		&core,
+		voxelDependencies,
+		lightBuffer.getHandle(),
+		shadowMap.getHandle(),
+		shadowSampler);
+
+	vkcv::gui::GUI gui(core, window);
+
+	glm::vec2 lightAngles(90.f, 0.f);
+	int voxelVisualisationMip = 0;
+	float voxelizationExtent = 20.f;
 
 	auto start = std::chrono::system_clock::now();
 	const auto appStartTime = start;
@@ -324,7 +344,7 @@ int main(int argc, const char** argv) {
 
 		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
 			depthBuffer = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight).getHandle();
-			colorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, true, true).getHandle();
+			colorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
 
 			windowWidth = swapchainWidth;
 			windowHeight = swapchainHeight;
@@ -334,19 +354,20 @@ int main(int argc, const char** argv) {
 		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
 
 		// update descriptor sets which use swapchain image
-		vkcv::DescriptorWrites gammaCorrectionDescriptorWrites;
-		gammaCorrectionDescriptorWrites.storageImageWrites = {
+		vkcv::DescriptorWrites tonemappingDescriptorWrites;
+		tonemappingDescriptorWrites.storageImageWrites = {
 			vkcv::StorageImageDescriptorWrite(0, colorBuffer),
 			vkcv::StorageImageDescriptorWrite(1, swapchainInput) };
-		core.writeDescriptorSet(gammaCorrectionDescriptorSet, gammaCorrectionDescriptorWrites);
+		core.writeDescriptorSet(tonemappingDescriptorSet, tonemappingDescriptorWrites);
 
 		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.001f * static_cast<float>(duration.count());
-		lightInfo.direction = glm::normalize(glm::vec3(std::cos(sunTheta), 1, std::sin(sunTheta)));
+		glm::vec2 lightAngleRadian = glm::radians(lightAngles);
+		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)));
 
 		const float shadowProjectionSize = 20.f;
 		glm::mat4 projectionLight = glm::ortho(
@@ -393,6 +414,7 @@ int main(int argc, const char** argv) {
 			{ shadowMap.getHandle() });
 		core.prepareImageForSampling(cmdStream, shadowMap.getHandle());
 
+		voxelization.setVoxelExtent(voxelizationExtent);
 		voxelization.voxelizeMeshes(
 			cmdStream, 
 			cameraManager.getActiveCamera().getPosition(), 
@@ -410,13 +432,13 @@ int main(int argc, const char** argv) {
 			renderTargets);
 
 		if (renderVoxelVis) {
-			voxelization.renderVoxelVisualisation(cmdStream, viewProjectionCamera, renderTargets);
+			voxelization.renderVoxelVisualisation(cmdStream, viewProjectionCamera, renderTargets, voxelVisualisationMip);
 		}
 
-		const uint32_t gammaCorrectionLocalGroupSize = 8;
-		const uint32_t gammaCorrectionDispatchCount[3] = {
-			static_cast<uint32_t>(glm::ceil(windowWidth / static_cast<float>(gammaCorrectionLocalGroupSize))),
-			static_cast<uint32_t>(glm::ceil(windowHeight / static_cast<float>(gammaCorrectionLocalGroupSize))),
+		const uint32_t tonemappingLocalGroupSize = 8;
+		const uint32_t tonemappingDispatchCount[3] = {
+			static_cast<uint32_t>(glm::ceil(windowWidth / static_cast<float>(tonemappingLocalGroupSize))),
+			static_cast<uint32_t>(glm::ceil(windowHeight / static_cast<float>(tonemappingLocalGroupSize))),
 			1
 		};
 
@@ -425,15 +447,29 @@ int main(int argc, const char** argv) {
 
 		core.recordComputeDispatchToCmdStream(
 			cmdStream, 
-			gammaCorrectionPipeline, 
-			gammaCorrectionDispatchCount,
-			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(gammaCorrectionDescriptorSet).vulkanHandle) },
+			tonemappingPipeline, 
+			tonemappingDispatchCount,
+			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptorSet).vulkanHandle) },
 			vkcv::PushConstantData(nullptr, 0));
 
 		// present and end
 		core.prepareSwapchainImageForPresent(cmdStream);
 		core.submitCommandStream(cmdStream);
 
+		gui.beginGUI();
+
+		ImGui::Begin("Settings");
+		ImGui::DragFloat2("Light angles",   &lightAngles.x);
+		ImGui::ColorEdit3("Sun color",      &lightInfo.sunColor.x);
+		ImGui::DragFloat("Sun strength",    &lightInfo.sunStrength);
+		ImGui::Checkbox("Draw voxel visualisation", &renderVoxelVis);
+		ImGui::SliderInt("Visualisation mip",       &voxelVisualisationMip, 0, 7);
+		ImGui::DragFloat("Voxelization extent",     &voxelizationExtent, 1.f, 0.f);
+		voxelVisualisationMip = std::max(voxelVisualisationMip, 0);
+		ImGui::End();
+
+		gui.endGUI();
+
 		core.endFrame();
 	}
 	
diff --git a/src/vkcv/BufferManager.cpp b/src/vkcv/BufferManager.cpp
index 4df411c193acffa42401e4f5932d97f531cac9c9..aec96411c5d9e07f200b24fbdcf9fa69e2af53d5 100644
--- a/src/vkcv/BufferManager.cpp
+++ b/src/vkcv/BufferManager.cpp
@@ -159,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 (
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index fd2c595b4305740e801e8d3d50af74521fc3418c..1492b1afa563543e6a9eef380295bcb71fef58b8 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -15,7 +15,7 @@
 #include "DescriptorManager.hpp"
 #include "ImageLayoutTransitions.hpp"
 #include "vkcv/CommandStreamManager.hpp"
-
+#include <cmath>
 #include "vkcv/Logger.hpp"
 
 namespace vkcv
@@ -375,7 +375,7 @@ namespace vkcv
 		}
 	}
 	
-	void Core::recordAndSubmitCommands(
+	void Core::recordAndSubmitCommandsImmediate(
 		const SubmitInfo &submitInfo, 
 		const RecordCommandFunction &record, 
 		const FinishCommandFunction &finish)
@@ -390,18 +390,12 @@ namespace vkcv
 		record(cmdBuffer);
 		cmdBuffer.end();
 		
-		vk::Fence waitFence;
-		
-		if (!submitInfo.fence) {
-			waitFence = createFence(device);
-		}
-		
+		vk::Fence waitFence = createFence(device);
+
 		submitCommandBufferToQueue(queue.handle, cmdBuffer, waitFence, submitInfo.waitSemaphores, submitInfo.signalSemaphores);
 		waitForFence(device, waitFence);
 		
-		if (!submitInfo.fence) {
-			device.destroyFence(waitFence);
-		}
+		device.destroyFence(waitFence);
 		
 		device.freeCommandBuffers(cmdPool, cmdBuffer);
 		
@@ -442,9 +436,30 @@ 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, bool supportStorage, bool supportColorAttachment)
+	Image Core::createImage(
+		vk::Format  format,
+		uint32_t    width,
+		uint32_t    height,
+		uint32_t    depth,
+		bool        createMipChain,
+		bool        supportStorage,
+		bool        supportColorAttachment)
 	{
-    	return Image::create(m_ImageManager.get(), format, width, height, depth, supportStorage, supportColorAttachment);
+
+		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);
 	}
 
     DescriptorSetHandle Core::createDescriptorSet(const std::vector<DescriptorBinding>& bindings)
@@ -524,7 +539,7 @@ namespace vkcv
 		}, nullptr);
 	}
 	
-	const vk::ImageView& Core::getSwapchainImageView() const {
+	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..265532232304106f7271fdd445d52074b7c011a1 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -128,7 +128,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/Image.cpp b/src/vkcv/Image.cpp
index 9f7fdadc7400fd3e63061f1e40cd494db63a7869..c48b015335e00f23a892bb96d3e89a2c0877ae61 100644
--- a/src/vkcv/Image.cpp
+++ b/src/vkcv/Image.cpp
@@ -19,9 +19,17 @@ namespace vkcv{
 		}
 	}
 
-	Image Image::create(ImageManager* manager, vk::Format format, uint32_t width, uint32_t height, uint32_t depth, bool supportStorage, bool supportColorAttachment)
+	Image Image::create(
+		ImageManager*   manager,
+		vk::Format      format,
+		uint32_t        width,
+		uint32_t        height,
+		uint32_t        depth,
+		uint32_t        mipCount,
+		bool            supportStorage,
+		bool            supportColorAttachment)
 	{
-		return Image(manager, manager->createImage(width, height, depth, format, supportStorage, supportColorAttachment));
+		return Image(manager, manager->createImage(width, height, depth, format, mipCount, supportStorage, supportColorAttachment));
 	}
 	
 	vk::Format Image::getFormat() const {
@@ -48,10 +56,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/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 fff98dc5b56d0ad085c6ebc68acf7230223d9cd1..a3364ce0dfd6f59dc78c85b43570eea25cfc052d 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,8 +67,11 @@ namespace vkcv {
 		for (uint64_t id = 0; id < m_images.size(); id++) {
 			destroyImageById(id);
 		}
-		for (const auto swapchainImage : m_swapchainImages)
-			m_core->getContext().getDevice().destroy(swapchainImage.m_view);
+		for (const auto swapchainImage : m_swapchainImages) {
+			for (const auto view : swapchainImage.m_viewPerMip) {
+				m_core->getContext().getDevice().destroy(view);
+			}
+		}
 	}
 	
 	bool isDepthImageFormat(vk::Format format) {
@@ -83,7 +84,14 @@ namespace vkcv {
 		}
 	}
 
-	ImageHandle ImageManager::createImage(uint32_t width, uint32_t height, uint32_t depth, vk::Format format, bool supportStorage, bool supportColorAttachment)
+	ImageHandle ImageManager::createImage(
+		uint32_t    width, 
+		uint32_t    height, 
+		uint32_t    depth, 
+		vk::Format  format, 
+		uint32_t    mipCount,
+		bool        supportStorage, 
+		bool        supportColorAttachment)
 	{
 		const vk::PhysicalDevice& physicalDevice = m_core->getContext().getPhysicalDevice();
 		
@@ -91,7 +99,7 @@ namespace vkcv {
 		
 		vk::ImageCreateFlags createFlags;
 		vk::ImageUsageFlags imageUsageFlags = (
-				vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst
+				vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc
 		);
 		if (supportStorage) {
 			imageUsageFlags |= vk::ImageUsageFlagBits::eStorage;
@@ -135,11 +143,9 @@ 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);
 		
 		const vk::ImageCreateInfo imageCreateInfo(
@@ -147,7 +153,7 @@ namespace vkcv {
 			imageType,
 			format,
 			vk::Extent3D(width, height, depth),
-			mipLevels,
+			mipCount,
 			arrayLayers,
 			vk::SampleCountFlagBits::e1,
 			imageTiling,
@@ -180,30 +186,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); });
 	}
 	
@@ -246,10 +255,10 @@ namespace vkcv {
 		return image.m_memory;
 	}
 	
-	vk::ImageView ImageManager::getVulkanImageView(const ImageHandle &handle) const {
+	vk::ImageView ImageManager::getVulkanImageView(const ImageHandle &handle, const size_t mipLevel) const {
 		
 		if (handle.isSwapchainImage()) {
-			return m_swapchainImages[m_currentSwapchainInputImage].m_view;
+			return m_swapchainImages[m_currentSwapchainInputImage].m_viewPerMip[0];
 		}
 
 		const uint64_t id = handle.getId();
@@ -258,7 +267,14 @@ namespace vkcv {
 			return nullptr;
 		}
 		
-		return m_images[id].m_view;
+		const auto& image = m_images[id];
+
+		if (mipLevel >= m_images.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) {
@@ -277,7 +293,7 @@ namespace vkcv {
 		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
@@ -380,7 +396,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;
@@ -421,7 +437,90 @@ 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);
+	}
+
 	uint32_t ImageManager::getImageWidth(const ImageHandle &handle) const {
 		const uint64_t id = handle.getId();
 		const bool isSwapchainImage = handle.isSwapchainImage();
@@ -475,9 +574,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) {
@@ -504,6 +605,22 @@ namespace vkcv {
 		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;
 	}
@@ -512,13 +629,16 @@ namespace vkcv {
 		uint32_t width, uint32_t height, vk::Format format) {
 
 		// destroy old views
-		for (auto image : m_swapchainImages)
-			m_core->getContext().getDevice().destroyImageView(image.m_view);
+		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, 1));
+			m_swapchainImages.push_back(Image(images[i], nullptr, { views[i] }, width, height, 1, format, 1));
 		}
 	}
 
diff --git a/src/vkcv/ImageManager.hpp b/src/vkcv/ImageManager.hpp
index 7bb4cc7ef7951a8677c19f589bd7948dbcedb25d..ecba7eb5959c1d78a0be41e0b3ac555bffd92d95 100644
--- a/src/vkcv/ImageManager.hpp
+++ b/src/vkcv/ImageManager.hpp
@@ -18,29 +18,27 @@ 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();
 		};
@@ -62,7 +60,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;
@@ -71,7 +71,14 @@ 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, bool supportStorage, bool supportColorAttachment);
+		ImageHandle createImage(
+			uint32_t    width, 
+			uint32_t    height, 
+			uint32_t    depth, 
+			vk::Format  format, 
+			uint32_t    mipCount,
+			bool        supportStorage, 
+			bool        supportColorAttachment);
 		
 		ImageHandle createSwapchainImage();
 		
@@ -82,7 +89,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(
@@ -95,6 +102,8 @@ namespace vkcv {
 			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);
 		
 		[[nodiscard]]
 		uint32_t getImageWidth(const ImageHandle& handle) const;
@@ -108,6 +117,9 @@ 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);
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
 		);