From 1789c8fddaf4a936393166dca4810b53450474ae Mon Sep 17 00:00:00 2001
From: Alexander Gauggel <agauggel@uni-koblenz.de>
Date: Tue, 17 Aug 2021 13:28:22 +0200
Subject: [PATCH] [#106] Refactored motion blur into separate class for easier
 reuse

---
 projects/indirect_dispatch/CMakeLists.txt     |  11 +-
 projects/indirect_dispatch/src/App.cpp        | 265 +++++-------------
 projects/indirect_dispatch/src/App.hpp        |  10 +-
 projects/indirect_dispatch/src/AppConfig.hpp  |   1 -
 projects/indirect_dispatch/src/AppSetup.cpp   |  32 +--
 projects/indirect_dispatch/src/AppSetup.hpp   |   7 +-
 projects/indirect_dispatch/src/MotionBlur.cpp | 232 +++++++++++++++
 projects/indirect_dispatch/src/MotionBlur.hpp |  57 ++++
 .../src/MotionBlurConfig.hpp                  |  14 +
 .../indirect_dispatch/src/MotionBlurSetup.cpp |  41 +++
 .../indirect_dispatch/src/MotionBlurSetup.hpp |  12 +
 11 files changed, 437 insertions(+), 245 deletions(-)
 create mode 100644 projects/indirect_dispatch/src/MotionBlur.cpp
 create mode 100644 projects/indirect_dispatch/src/MotionBlur.hpp
 create mode 100644 projects/indirect_dispatch/src/MotionBlurConfig.hpp
 create mode 100644 projects/indirect_dispatch/src/MotionBlurSetup.cpp
 create mode 100644 projects/indirect_dispatch/src/MotionBlurSetup.hpp

diff --git a/projects/indirect_dispatch/CMakeLists.txt b/projects/indirect_dispatch/CMakeLists.txt
index b54a3186..7bc86cbc 100644
--- a/projects/indirect_dispatch/CMakeLists.txt
+++ b/projects/indirect_dispatch/CMakeLists.txt
@@ -16,10 +16,17 @@ target_sources(indirect_dispatch PRIVATE
     src/App.cpp
 
     src/AppConfig.hpp
+    src/MotionBlurConfig.hpp
     
     src/AppSetup.hpp
-    src/AppSetup.cpp)
-
+    src/AppSetup.cpp
+    
+    src/MotionBlur.hpp
+    src/MotionBlur.cpp
+    
+    src/MotionBlurSetup.hpp
+    src/MotionBlurSetup.cpp)
+    
 # this should fix the execution path to load local files from the project (for MSVC)
 if(MSVC)
 	set_target_properties(indirect_dispatch PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
diff --git a/projects/indirect_dispatch/src/App.cpp b/projects/indirect_dispatch/src/App.cpp
index 02442423..88fdca59 100644
--- a/projects/indirect_dispatch/src/App.cpp
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -38,36 +38,21 @@ bool App::initialize() {
 	if (!loadComputePass(m_core, "resources/shaders/gammaCorrection.comp", &m_gammaCorrectionPass))
 		return false;
 
-	if(!loadComputePass(m_core, "resources/shaders/motionBlur.comp", &m_motionBlurPass))
-		return false;
-
-	if (!loadComputePass(m_core, "resources/shaders/motionVectorMax.comp", &m_motionVectorMaxPass))
-		return false;
-
-	if (!loadComputePass(m_core, "resources/shaders/motionVectorMaxNeighbourhood.comp", &m_motionVectorMaxNeighbourhoodPass))
-		return false;
-
-	if (!loadComputePass(m_core, "resources/shaders/motionVectorVisualisation.comp", &m_motionVectorVisualisationPass))
-		return false;
-
 	if (!loadMesh(m_core, "resources/models/sphere.gltf", & m_sphereMesh))
 		return false;
 
 	if (!loadMesh(m_core, "resources/models/cube.gltf", &m_cubeMesh))
 		return false;
 
+	if (!m_motionBlur.initialize(&m_core, m_windowWidth, m_windowHeight))
+		return false;
+
 	m_linearSampler = m_core.createSampler(
 		vkcv::SamplerFilterType::LINEAR,
 		vkcv::SamplerFilterType::LINEAR,
 		vkcv::SamplerMipmapMode::LINEAR,
 		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
 
-	m_nearestSampler = m_core.createSampler(
-		vkcv::SamplerFilterType::NEAREST,
-		vkcv::SamplerFilterType::NEAREST,
-		vkcv::SamplerMipmapMode::NEAREST,
-		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
-
 	m_renderTargets = createRenderTargets(m_core, m_windowWidth, m_windowHeight);
 
 	const int cameraIndex = m_cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
@@ -85,32 +70,22 @@ void App::run() {
 	const vkcv::DrawcallInfo    cubeDrawcall(m_cubeMesh.mesh, {}, 1);
 
 	vkcv::gui::GUI gui(m_core, m_window);
-	enum class eDebugView : int { 
-		None                                = 0, 
-		MotionVector                        = 1, 
-		MotionVectorMaxTile                 = 2, 
-		MotionVectorMaxTileNeighbourhood    = 3,
-		OptionCount                         = 4 };
-
-	const char* debugViewLabels[] = {
-		"None",
-		"Motion vectors",
-		"Motion vector max tiles",
-		"Motion vector tile neighbourhood max" };
 
-	enum class eMotionBlurInput : int {
-		MotionVector                        = 0,
-		MotionVectorMaxTile                 = 1,
-		MotionVectorMaxTileNeighbourhood    = 2,
-		OptionCount                         = 3 };
+	enum class eMotionVectorVisualisationMode : int {
+		None                    = 0,
+		FullResolution          = 1,
+		MaxTile                 = 2,
+		MaxTileNeighbourhood    = 3,
+		OptionCount             = 4 };
 
-	const char* motionInputLabels[] = {
-		"Motion vectors",
-		"Motion vector max tiles",
-		"Motion vector tile neighbourhood max" };
+	const char* motionVectorVisualisationModeLabels[] = {
+		"None",
+		"Full resolution",
+		"Max tiles",
+		"Tile neighbourhood max" };
 
-	eDebugView          debugView       = eDebugView::None;
-	eMotionBlurInput    motionBlurInput = eMotionBlurInput::MotionVectorMaxTileNeighbourhood;
+	eMotionVectorVisualisationMode  motionVectorVisualisationMode   = eMotionVectorVisualisationMode::None;
+	eMotionVectorMode               motionBlurMotionMode            = eMotionVectorMode::MaxTileNeighbourhood;
 
 	float   objectVerticalSpeed             = 5;
 	float   motionBlurMinVelocity           = 0.001;
@@ -136,6 +111,7 @@ void App::run() {
 			m_windowHeight = swapchainHeight;
 
 			m_renderTargets = createRenderTargets(m_core, m_windowWidth, m_windowHeight);
+			m_motionBlur.setResolution(m_windowWidth, m_windowHeight);
 		}
 
 		auto frameEndTime   = std::chrono::system_clock::now();
@@ -186,53 +162,6 @@ void App::run() {
 			{ cubeDrawcall },
 			prepassRenderTargets);
 
-		// motion vector max tiles
-		vkcv::DescriptorWrites motionVectorMaxTilesDescriptorWrites;
-		motionVectorMaxTilesDescriptorWrites.sampledImageWrites = {
-			vkcv::SampledImageDescriptorWrite(0, m_renderTargets.motionBuffer) };
-		motionVectorMaxTilesDescriptorWrites.samplerWrites = {
-			vkcv::SamplerDescriptorWrite(1, m_linearSampler) };
-		motionVectorMaxTilesDescriptorWrites.storageImageWrites = {
-			vkcv::StorageImageDescriptorWrite(2, m_renderTargets.motionMax)};
-
-		m_core.writeDescriptorSet(m_motionVectorMaxPass.descriptorSet, motionVectorMaxTilesDescriptorWrites);
-
-		m_core.prepareImageForSampling(cmdStream, m_renderTargets.motionBuffer);
-		m_core.prepareImageForStorage(cmdStream, m_renderTargets.motionMax);
-
-		const uint32_t motionTileDispatchCounts[3] = {
-			(m_core.getImageWidth( m_renderTargets.motionMax) + 7) / 8,
-			(m_core.getImageHeight(m_renderTargets.motionMax) + 7) / 8,
-			1 };
-
-		m_core.recordComputeDispatchToCmdStream(
-			cmdStream,
-			m_motionVectorMaxPass.pipeline,
-			motionTileDispatchCounts,
-			{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_motionVectorMaxPass.descriptorSet).vulkanHandle) },
-			vkcv::PushConstants(0));
-
-		// motion vector max neighbourhood
-		vkcv::DescriptorWrites motionVectorMaxNeighbourhoodDescriptorWrites;
-		motionVectorMaxNeighbourhoodDescriptorWrites.sampledImageWrites = {
-			vkcv::SampledImageDescriptorWrite(0, m_renderTargets.motionMax) };
-		motionVectorMaxNeighbourhoodDescriptorWrites.samplerWrites = {
-			vkcv::SamplerDescriptorWrite(1, m_linearSampler) };
-		motionVectorMaxNeighbourhoodDescriptorWrites.storageImageWrites = {
-			vkcv::StorageImageDescriptorWrite(2, m_renderTargets.motionMaxNeighbourhood) };
-
-		m_core.writeDescriptorSet(m_motionVectorMaxNeighbourhoodPass.descriptorSet, motionVectorMaxNeighbourhoodDescriptorWrites);
-
-		m_core.prepareImageForSampling(cmdStream, m_renderTargets.motionMax);
-		m_core.prepareImageForStorage(cmdStream, m_renderTargets.motionMaxNeighbourhood);
-
-		m_core.recordComputeDispatchToCmdStream(
-			cmdStream,
-			m_motionVectorMaxNeighbourhoodPass.pipeline,
-			motionTileDispatchCounts,
-			{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_motionVectorMaxNeighbourhoodPass.descriptorSet).vulkanHandle) },
-			vkcv::PushConstants(0));
-
 		// main pass
 		const std::vector<vkcv::ImageHandle> renderTargets   = { 
 			m_renderTargets.colorBuffer, 
@@ -262,122 +191,52 @@ void App::run() {
 			renderTargets);
 
 		// motion blur
-		vkcv::ImageHandle motionBuffer;
-		if (motionBlurInput == eMotionBlurInput::MotionVector)
-			motionBuffer = m_renderTargets.motionBuffer;
-		else if (motionBlurInput == eMotionBlurInput::MotionVectorMaxTile)
-			motionBuffer = m_renderTargets.motionMax;
-		else if (motionBlurInput == eMotionBlurInput::MotionVectorMaxTileNeighbourhood)
-			motionBuffer = m_renderTargets.motionMaxNeighbourhood;
-		else {
-			vkcv_log(vkcv::LogLevel::ERROR, "Unknown eMotionInput enum value");
-			motionBuffer = m_renderTargets.motionBuffer;
-		}
+		vkcv::ImageHandle motionBlurOutput;
 
-		vkcv::DescriptorWrites motionBlurDescriptorWrites;
-		motionBlurDescriptorWrites.sampledImageWrites = {
-			vkcv::SampledImageDescriptorWrite(0, m_renderTargets.colorBuffer),
-			vkcv::SampledImageDescriptorWrite(1, m_renderTargets.depthBuffer),
-			vkcv::SampledImageDescriptorWrite(2, motionBuffer) };
-		motionBlurDescriptorWrites.samplerWrites = {
-			vkcv::SamplerDescriptorWrite(3, m_nearestSampler) };
-		motionBlurDescriptorWrites.storageImageWrites = {
-			vkcv::StorageImageDescriptorWrite(4, m_renderTargets.motionBlurOutput) };
+		if (motionVectorVisualisationMode == eMotionVectorVisualisationMode::None) {
+			const float microsecondToSecond = 0.000001;
+			const float fDeltaTimeSeconds = microsecondToSecond * std::chrono::duration_cast<std::chrono::microseconds>(frameEndTime - frameStartTime).count();
 
-		m_core.writeDescriptorSet(m_motionBlurPass.descriptorSet, motionBlurDescriptorWrites);
+			float cameraNear;
+			float cameraFar;
+			m_cameraManager.getActiveCamera().getNearFar(cameraNear, cameraFar);
 
-		uint32_t fullScreenImageDispatch[3] = {
-			static_cast<uint32_t>((m_windowWidth + 7) / 8),
-			static_cast<uint32_t>((m_windowHeight + 7) / 8),
-			static_cast<uint32_t>(1) };
-
-		m_core.prepareImageForStorage(cmdStream, m_renderTargets.motionBlurOutput);
-		m_core.prepareImageForSampling(cmdStream, m_renderTargets.colorBuffer);
-		m_core.prepareImageForSampling(cmdStream, m_renderTargets.depthBuffer);
-		m_core.prepareImageForSampling(cmdStream, motionBuffer);
-
-		const float microsecondToSecond     = 0.000001;
-		const float fDeltatimeSeconds       = microsecondToSecond * std::chrono::duration_cast<std::chrono::microseconds>(frameEndTime - frameStartTime).count();
-
-		// must match layout in "motionBlur.comp"
-		struct MotionBlurConstantData {
-			float motionFactor;
-			float minVelocity;
-			float cameraNearPlane;
-			float cameraFarPlane;
-		};
-		MotionBlurConstantData motionBlurConstantData;
-
-		// small mouse movements are restricted to pixel level and therefore quite unprecise
-		// therefore extrapolating movement at high framerates results in big jerky movements
-		// this results in wide sudden motion blur, which looks quite bad
-		// as a workaround the time scale is limited to a maximum value
-		const float motionBlurTimeScaleMax  = 1.f / 60;
-		const float deltaTimeMotionBlur     = std::max(fDeltatimeSeconds, motionBlurTimeScaleMax);
-
-		motionBlurConstantData.motionFactor = 1 / (deltaTimeMotionBlur * cameraShutterSpeedInverse);
-		motionBlurConstantData.minVelocity = motionBlurMinVelocity;
-
-		float cameraNear, cameraFar;
-		m_cameraManager.getActiveCamera().getNearFar(cameraNear, cameraFar);
-		motionBlurConstantData.cameraNearPlane  = cameraNear;
-		motionBlurConstantData.cameraFarPlane   = cameraFar;
-
-		vkcv::PushConstants motionBlurPushConstants(sizeof(motionBlurConstantData));
-		motionBlurPushConstants.appendDrawcall(motionBlurConstantData);
-
-		m_core.recordComputeDispatchToCmdStream(
-			cmdStream,
-			m_motionBlurPass.pipeline,
-			fullScreenImageDispatch,
-			{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_motionBlurPass.descriptorSet).vulkanHandle) },
-			motionBlurPushConstants);
-
-		// motion vector debug visualisation
-		// writes to motion blur output
-		if (debugView != eDebugView::None) {
-			vkcv::ImageHandle visualisationInput;
-			if (debugView == eDebugView::MotionVector)
-				visualisationInput = m_renderTargets.motionBuffer;
-			else if (debugView == eDebugView::MotionVectorMaxTile)
-				visualisationInput = m_renderTargets.motionMax;
-			else if (debugView == eDebugView::MotionVectorMaxTileNeighbourhood)
-				visualisationInput = m_renderTargets.motionMaxNeighbourhood;
+			motionBlurOutput = m_motionBlur.render(
+				cmdStream,
+				m_renderTargets.motionBuffer,
+				m_renderTargets.colorBuffer,
+				m_renderTargets.depthBuffer,
+				motionBlurMotionMode,
+				cameraNear,
+				cameraFar,
+				fDeltaTimeSeconds,
+				cameraShutterSpeedInverse,
+				motionBlurMinVelocity);
+		}
+		else {
+			eMotionVectorMode debugViewMode;
+			if (motionVectorVisualisationMode == eMotionVectorVisualisationMode::FullResolution)
+				debugViewMode = eMotionVectorMode::FullResolution;
+			else if(motionVectorVisualisationMode == eMotionVectorVisualisationMode::MaxTile)
+				debugViewMode = eMotionVectorMode::MaxTile;
+			else if (motionVectorVisualisationMode == eMotionVectorVisualisationMode::MaxTileNeighbourhood)
+				debugViewMode = eMotionVectorMode::MaxTileNeighbourhood;
 			else {
-				vkcv_log(vkcv::LogLevel::ERROR, "Unknown eDebugView enum value");
-				visualisationInput = m_renderTargets.motionBlurOutput;
+				vkcv_log(vkcv::LogLevel::ERROR, "Unknown eMotionVectorMode enum option");
+				debugViewMode = eMotionVectorMode::FullResolution;
 			}
 
-			vkcv::DescriptorWrites motionVectorVisualisationDescriptorWrites;
-			motionVectorVisualisationDescriptorWrites.sampledImageWrites = {
-				vkcv::SampledImageDescriptorWrite(0, visualisationInput) };
-			motionVectorVisualisationDescriptorWrites.samplerWrites = {
-				vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
-			motionVectorVisualisationDescriptorWrites.storageImageWrites = {
-				vkcv::StorageImageDescriptorWrite(2, m_renderTargets.motionBlurOutput) };
-
-			m_core.writeDescriptorSet(
-				m_motionVectorVisualisationPass.descriptorSet, 
-				motionVectorVisualisationDescriptorWrites);
-
-			m_core.prepareImageForSampling(cmdStream, visualisationInput);
-			m_core.prepareImageForStorage(cmdStream, m_renderTargets.motionBlurOutput);
-
-			vkcv::PushConstants motionVectorVisualisationPushConstants(sizeof(float));
-			motionVectorVisualisationPushConstants.appendDrawcall(motionVectorVisualisationRange);
-
-			m_core.recordComputeDispatchToCmdStream(
+			motionBlurOutput = m_motionBlur.renderMotionVectorVisualisation(
 				cmdStream,
-				m_motionVectorVisualisationPass.pipeline,
-				fullScreenImageDispatch,
-				{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_motionVectorVisualisationPass.descriptorSet).vulkanHandle) },
-				motionVectorVisualisationPushConstants);
+				m_renderTargets.motionBuffer,
+				debugViewMode,
+				motionVectorVisualisationRange);
 		}
 
 		// gamma correction
 		vkcv::DescriptorWrites gammaCorrectionDescriptorWrites;
 		gammaCorrectionDescriptorWrites.sampledImageWrites = {
-			vkcv::SampledImageDescriptorWrite(0, m_renderTargets.motionBlurOutput) };
+			vkcv::SampledImageDescriptorWrite(0, motionBlurOutput) };
 		gammaCorrectionDescriptorWrites.samplerWrites = {
 			vkcv::SamplerDescriptorWrite(1, m_linearSampler) };
 		gammaCorrectionDescriptorWrites.storageImageWrites = {
@@ -385,9 +244,14 @@ void App::run() {
 
 		m_core.writeDescriptorSet(m_gammaCorrectionPass.descriptorSet, gammaCorrectionDescriptorWrites);
 
-		m_core.prepareImageForSampling(cmdStream, m_renderTargets.motionBlurOutput);
+		m_core.prepareImageForSampling(cmdStream, motionBlurOutput);
 		m_core.prepareImageForStorage (cmdStream, swapchainInput);
 
+		const uint32_t fullScreenImageDispatch[3] = {
+			static_cast<uint32_t>((m_windowWidth  + 7) / 8),
+			static_cast<uint32_t>((m_windowHeight + 7) / 8),
+			static_cast<uint32_t>(1) };
+
 		m_core.recordComputeDispatchToCmdStream(
 			cmdStream,
 			m_gammaCorrectionPass.pipeline,
@@ -403,19 +267,18 @@ void App::run() {
 
 		ImGui::Combo(
 			"Debug view",
-			reinterpret_cast<int*>(&debugView),
-			debugViewLabels,
-			static_cast<int>(eDebugView::OptionCount));
+			reinterpret_cast<int*>(&motionVectorVisualisationMode),
+			motionVectorVisualisationModeLabels,
+			static_cast<int>(eMotionVectorVisualisationMode::OptionCount));
 
-		if (debugView != eDebugView::None) {
+		if (motionVectorVisualisationMode != eMotionVectorVisualisationMode::None)
 			ImGui::InputFloat("Motion vector visualisation range", &motionVectorVisualisationRange);
-		}
 
 		ImGui::Combo(
 			"Motion blur input",
-			reinterpret_cast<int*>(&motionBlurInput),
-			motionInputLabels,
-			static_cast<int>(eMotionBlurInput::OptionCount));
+			reinterpret_cast<int*>(&motionBlurMotionMode),
+			MotionVectorModeLabels,
+			static_cast<int>(eMotionVectorMode::OptionCount));
 
 		ImGui::InputFloat("Object movement speed", &objectVerticalSpeed);
 		ImGui::InputInt("Camera shutter speed inverse", &cameraShutterSpeedInverse);
diff --git a/projects/indirect_dispatch/src/App.hpp b/projects/indirect_dispatch/src/App.hpp
index 49743e4a..9871776f 100644
--- a/projects/indirect_dispatch/src/App.hpp
+++ b/projects/indirect_dispatch/src/App.hpp
@@ -2,6 +2,7 @@
 #include <vkcv/Core.hpp>
 #include <vkcv/camera/CameraManager.hpp>
 #include "AppSetup.hpp"
+#include "MotionBlur.hpp"
 
 class App {
 public:
@@ -18,6 +19,8 @@ private:
 	vkcv::Core                  m_core;
 	vkcv::camera::CameraManager m_cameraManager;
 
+	MotionBlur m_motionBlur;
+
 	MeshResources m_sphereMesh;
 	MeshResources m_cubeMesh;
 
@@ -27,12 +30,7 @@ private:
 	GraphicPassHandles m_skyPrePass;
 
 	ComputePassHandles m_gammaCorrectionPass;
-	ComputePassHandles m_motionBlurPass;
-	ComputePassHandles m_motionVectorMaxPass;
-	ComputePassHandles m_motionVectorMaxNeighbourhoodPass;
-	ComputePassHandles m_motionVectorVisualisationPass;
 
-	RenderTargets       m_renderTargets;
+	AppRenderTargets    m_renderTargets;
 	vkcv::SamplerHandle m_linearSampler;
-	vkcv::SamplerHandle m_nearestSampler;
 };
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/AppConfig.hpp b/projects/indirect_dispatch/src/AppConfig.hpp
index 370b9684..c89c34ea 100644
--- a/projects/indirect_dispatch/src/AppConfig.hpp
+++ b/projects/indirect_dispatch/src/AppConfig.hpp
@@ -7,5 +7,4 @@ namespace AppConfig{
 	const vk::Format    depthBufferFormat   = vk::Format::eD32Sfloat;
 	const vk::Format    colorBufferFormat   = vk::Format::eB10G11R11UfloatPack32;
 	const vk::Format    motionBufferFormat  = vk::Format::eR16G16Sfloat;
-    const uint32_t      maxMotionTileSize   = 20;	// must match "motionTileSize" in motionVectorMax.comp
 }
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/AppSetup.cpp b/projects/indirect_dispatch/src/AppSetup.cpp
index 0733aee4..1d27e141 100644
--- a/projects/indirect_dispatch/src/AppSetup.cpp
+++ b/projects/indirect_dispatch/src/AppSetup.cpp
@@ -232,9 +232,9 @@ bool loadComputePass(vkcv::Core& core, const std::filesystem::path& path, Comput
 	return true;
 }
 
-RenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height) {
+AppRenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height) {
 
-	RenderTargets targets;
+	AppRenderTargets targets;
 
 	targets.depthBuffer = core.createImage(
 		AppConfig::depthBufferFormat,
@@ -252,14 +252,6 @@ RenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const
 		false,
 		true).getHandle();
 
-	targets.motionBlurOutput = core.createImage(
-		AppConfig::colorBufferFormat,
-		width,
-		height,
-		1,
-		false,
-		true).getHandle();
-
 	targets.motionBuffer = core.createImage(
 		AppConfig::motionBufferFormat,
 		width,
@@ -269,25 +261,5 @@ RenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const
 		false,
 		true).getHandle();
 
-	// divide and ceil to int
-	const uint32_t motionMaxWidth  = (width  + (AppConfig::maxMotionTileSize - 1)) / AppConfig::maxMotionTileSize;
-	const uint32_t motionMaxheight = (height + (AppConfig::maxMotionTileSize - 1)) / AppConfig::maxMotionTileSize;
-
-	targets.motionMax = core.createImage(
-		AppConfig::motionBufferFormat,
-		motionMaxWidth,
-		motionMaxheight,
-		1,
-		false,
-		true).getHandle();
-
-	targets.motionMaxNeighbourhood = core.createImage(
-		AppConfig::motionBufferFormat,
-		motionMaxWidth,
-		motionMaxheight,
-		1,
-		false,
-		true).getHandle();
-
 	return targets;
 }
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/AppSetup.hpp b/projects/indirect_dispatch/src/AppSetup.hpp
index adb8c198..f4aaad0a 100644
--- a/projects/indirect_dispatch/src/AppSetup.hpp
+++ b/projects/indirect_dispatch/src/AppSetup.hpp
@@ -1,13 +1,10 @@
 #pragma once
 #include <vkcv/Core.hpp>
 
-struct RenderTargets {
+struct AppRenderTargets {
 	vkcv::ImageHandle depthBuffer;
 	vkcv::ImageHandle colorBuffer;
-	vkcv::ImageHandle motionBlurOutput;
 	vkcv::ImageHandle motionBuffer;
-	vkcv::ImageHandle motionMax;
-	vkcv::ImageHandle motionMaxNeighbourhood;
 };
 
 struct GraphicPassHandles {
@@ -44,4 +41,4 @@ bool loadSkyPrePass(vkcv::Core& core, GraphicPassHandles* outHandles);
 
 bool loadComputePass(vkcv::Core& core, const std::filesystem::path& path, ComputePassHandles* outComputePass);
 
-RenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height);
\ No newline at end of file
+AppRenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height);
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlur.cpp b/projects/indirect_dispatch/src/MotionBlur.cpp
new file mode 100644
index 00000000..946bc008
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlur.cpp
@@ -0,0 +1,232 @@
+#include "MotionBlur.hpp"
+#include "MotionBlurConfig.hpp"
+#include "MotionBlurSetup.hpp"
+#include <array>
+
+std::array<uint32_t, 3> computeFullscreenDispatchSize(
+	const uint32_t imageWidth,
+	const uint32_t imageHeight,
+	const uint32_t localGroupSize) {
+
+	// optimized divide and ceil
+	return std::array<uint32_t, 3>{
+		static_cast<uint32_t>(imageWidth  + (localGroupSize - 1) / localGroupSize),
+		static_cast<uint32_t>(imageHeight + (localGroupSize - 1) / localGroupSize),
+		static_cast<uint32_t>(1) };
+}
+
+bool MotionBlur::initialize(vkcv::Core* corePtr, const uint32_t targetWidth, const uint32_t targetHeight) {
+
+	if (!corePtr) {
+		vkcv_log(vkcv::LogLevel::ERROR, "MotionBlur got invalid corePtr")
+		return false;
+	}
+
+	m_core = corePtr;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlur.comp", &m_motionBlurPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionVectorMax.comp", &m_motionVectorMaxPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionVectorMaxNeighbourhood.comp", &m_motionVectorMaxNeighbourhoodPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionVectorVisualisation.comp", &m_motionVectorVisualisationPass))
+		return false;
+
+	m_renderTargets = MotionBlurSetup::createRenderTargets(targetWidth, targetHeight, *m_core);
+
+	m_nearestSampler = m_core->createSampler(
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerMipmapMode::NEAREST,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+}
+
+void MotionBlur::setResolution(const uint32_t targetWidth, const uint32_t targetHeight) {
+	m_renderTargets = MotionBlurSetup::createRenderTargets(targetWidth, targetHeight, *m_core);
+}
+
+vkcv::ImageHandle MotionBlur::render(
+	const vkcv::CommandStreamHandle cmdStream,
+	const vkcv::ImageHandle         motionBufferFullRes,
+	const vkcv::ImageHandle         colorBuffer,
+	const vkcv::ImageHandle         depthBuffer,
+	const eMotionVectorMode         motionVectorMode,
+	const float                     cameraNear,
+	const float                     cameraFar,
+	const float                     deltaTimeSeconds,
+	const float                     cameraShutterSpeedInverse,
+	const float                     motionBlurMinVelocity) {
+
+	computeMotionTiles(cmdStream, motionBufferFullRes);
+
+	vkcv::ImageHandle inputMotionBuffer;
+	if (motionVectorMode == eMotionVectorMode::FullResolution)
+		inputMotionBuffer = motionBufferFullRes;
+	else if (motionVectorMode == eMotionVectorMode::MaxTile)
+		inputMotionBuffer = m_renderTargets.motionMax;
+	else if (motionVectorMode == eMotionVectorMode::MaxTileNeighbourhood)
+		inputMotionBuffer = m_renderTargets.motionMaxNeighbourhood;
+	else {
+		vkcv_log(vkcv::LogLevel::ERROR, "Unknown eMotionInput enum value");
+		inputMotionBuffer = motionBufferFullRes;
+	}
+
+	vkcv::DescriptorWrites motionBlurDescriptorWrites;
+	motionBlurDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, colorBuffer),
+		vkcv::SampledImageDescriptorWrite(1, depthBuffer),
+		vkcv::SampledImageDescriptorWrite(2, inputMotionBuffer) };
+	motionBlurDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(3, m_nearestSampler) };
+	motionBlurDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(4, m_renderTargets.outputColor) };
+
+	m_core->writeDescriptorSet(m_motionBlurPass.descriptorSet, motionBlurDescriptorWrites);
+
+	// must match layout in "motionBlur.comp"
+	struct MotionBlurConstantData {
+		float motionFactor;
+		float minVelocity;
+		float cameraNearPlane;
+		float cameraFarPlane;
+	};
+	MotionBlurConstantData motionBlurConstantData;
+
+	const float deltaTimeMotionBlur = std::max(deltaTimeSeconds, MotionBlurConfig::timeScaleMax);
+
+	motionBlurConstantData.motionFactor     = 1 / (deltaTimeMotionBlur * cameraShutterSpeedInverse);
+	motionBlurConstantData.minVelocity      = motionBlurMinVelocity;
+	motionBlurConstantData.cameraNearPlane  = cameraNear;
+	motionBlurConstantData.cameraFarPlane   = cameraFar;
+
+	vkcv::PushConstants motionBlurPushConstants(sizeof(motionBlurConstantData));
+	motionBlurPushConstants.appendDrawcall(motionBlurConstantData);
+
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.outputColor);
+	m_core->prepareImageForSampling(cmdStream, colorBuffer);
+	m_core->prepareImageForSampling(cmdStream, depthBuffer);
+	m_core->prepareImageForSampling(cmdStream, inputMotionBuffer);
+
+	const auto fullscreenDispatchSizes = computeFullscreenDispatchSize(
+		m_core->getImageWidth(m_renderTargets.outputColor),
+		m_core->getImageHeight(m_renderTargets.outputColor),
+		8);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_motionBlurPass.pipeline,
+		fullscreenDispatchSizes.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurPass.descriptorSet).vulkanHandle) },
+		motionBlurPushConstants);
+
+	return m_renderTargets.outputColor;
+}
+
+vkcv::ImageHandle MotionBlur::renderMotionVectorVisualisation(
+	const vkcv::CommandStreamHandle cmdStream,
+	const vkcv::ImageHandle         motionBuffer,
+	const eMotionVectorMode         debugView,
+	const float                     velocityRange) {
+
+	computeMotionTiles(cmdStream, motionBuffer);
+
+	vkcv::ImageHandle visualisationInput;
+	if (     debugView == eMotionVectorMode::FullResolution)
+		visualisationInput = motionBuffer;
+	else if (debugView == eMotionVectorMode::MaxTile)
+		visualisationInput = m_renderTargets.motionMax;
+	else if (debugView == eMotionVectorMode::MaxTileNeighbourhood)
+		visualisationInput = m_renderTargets.motionMaxNeighbourhood;
+	else {
+		vkcv_log(vkcv::LogLevel::ERROR, "Unknown eDebugView enum value");
+		return motionBuffer;
+	}
+
+	vkcv::DescriptorWrites motionVectorVisualisationDescriptorWrites;
+	motionVectorVisualisationDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, visualisationInput) };
+	motionVectorVisualisationDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+	motionVectorVisualisationDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(2, m_renderTargets.outputColor) };
+
+	m_core->writeDescriptorSet(
+		m_motionVectorVisualisationPass.descriptorSet,
+		motionVectorVisualisationDescriptorWrites);
+
+	m_core->prepareImageForSampling(cmdStream, visualisationInput);
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.outputColor);
+
+	vkcv::PushConstants motionVectorVisualisationPushConstants(sizeof(float));
+	motionVectorVisualisationPushConstants.appendDrawcall(velocityRange);
+
+	const auto dispatchSizes = computeFullscreenDispatchSize(
+		m_core->getImageWidth(m_renderTargets.outputColor), 
+		m_core->getImageHeight(m_renderTargets.outputColor), 
+		8);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_motionVectorVisualisationPass.pipeline,
+		dispatchSizes.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorVisualisationPass.descriptorSet).vulkanHandle) },
+		motionVectorVisualisationPushConstants);
+
+	return m_renderTargets.outputColor;
+}
+
+void MotionBlur::computeMotionTiles(
+	const vkcv::CommandStreamHandle cmdStream,
+	const vkcv::ImageHandle         motionBufferFullRes) {
+
+	// motion vector max tiles
+	vkcv::DescriptorWrites motionVectorMaxTilesDescriptorWrites;
+	motionVectorMaxTilesDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, motionBufferFullRes) };
+	motionVectorMaxTilesDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+	motionVectorMaxTilesDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(2, m_renderTargets.motionMax) };
+
+	m_core->writeDescriptorSet(m_motionVectorMaxPass.descriptorSet, motionVectorMaxTilesDescriptorWrites);
+
+	m_core->prepareImageForSampling(cmdStream, motionBufferFullRes);
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.motionMax);
+
+	const std::array<uint32_t, 3> motionTileDispatchCounts = computeFullscreenDispatchSize(
+		m_core->getImageWidth( m_renderTargets.motionMax),
+		m_core->getImageHeight(m_renderTargets.motionMax),
+		8);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_motionVectorMaxPass.pipeline,
+		motionTileDispatchCounts.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMaxPass.descriptorSet).vulkanHandle) },
+		vkcv::PushConstants(0));
+
+	// motion vector max neighbourhood
+	vkcv::DescriptorWrites motionVectorMaxNeighbourhoodDescriptorWrites;
+	motionVectorMaxNeighbourhoodDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, m_renderTargets.motionMax) };
+	motionVectorMaxNeighbourhoodDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+	motionVectorMaxNeighbourhoodDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(2, m_renderTargets.motionMaxNeighbourhood) };
+
+	m_core->writeDescriptorSet(m_motionVectorMaxNeighbourhoodPass.descriptorSet, motionVectorMaxNeighbourhoodDescriptorWrites);
+
+	m_core->prepareImageForSampling(cmdStream, m_renderTargets.motionMax);
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.motionMaxNeighbourhood);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_motionVectorMaxNeighbourhoodPass.pipeline,
+		motionTileDispatchCounts.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMaxNeighbourhoodPass.descriptorSet).vulkanHandle) },
+		vkcv::PushConstants(0));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlur.hpp b/projects/indirect_dispatch/src/MotionBlur.hpp
new file mode 100644
index 00000000..1812a536
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlur.hpp
@@ -0,0 +1,57 @@
+#pragma once
+#include "AppSetup.hpp"
+#include "MotionBlurSetup.hpp"
+
+// selection for motion blur input and visualisation
+enum class eMotionVectorMode : int {
+	FullResolution = 0,
+	MaxTile = 1,
+	MaxTileNeighbourhood = 2,
+	OptionCount = 3
+};
+
+static const char* MotionVectorModeLabels[3] = {
+	"Full resolution",
+	"Max tile",
+	"Tile neighbourhood max" };
+
+class MotionBlur {
+public:
+
+	bool initialize(vkcv::Core* corePtr, const uint32_t targetWidth, const uint32_t targetHeight);
+	void setResolution(const uint32_t targetWidth, const uint32_t targetHeight);
+
+	vkcv::ImageHandle render(
+		const vkcv::CommandStreamHandle cmdStream,
+		const vkcv::ImageHandle         motionBufferFullRes,
+		const vkcv::ImageHandle         colorBuffer,
+		const vkcv::ImageHandle         depthBuffer,
+		const eMotionVectorMode         motionVectorMode,
+		const float                     cameraNear,
+		const float                     cameraFar,
+		const float                     deltaTimeSeconds,
+		const float                     cameraShutterSpeedInverse,
+		const float                     motionBlurMinVelocity);
+
+	vkcv::ImageHandle MotionBlur::renderMotionVectorVisualisation(
+		const vkcv::CommandStreamHandle cmdStream,
+		const vkcv::ImageHandle         motionBuffer,
+		const eMotionVectorMode         debugView,
+		const float                     velocityRange);
+
+private:
+	// computes max per tile and neighbourhood tile max
+	void computeMotionTiles(
+		const vkcv::CommandStreamHandle cmdStream,
+		const vkcv::ImageHandle         motionBufferFullRes);
+
+	vkcv::Core* m_core;
+
+	MotionBlurRenderTargets m_renderTargets;
+	vkcv::SamplerHandle     m_nearestSampler;
+
+	ComputePassHandles m_motionBlurPass;
+	ComputePassHandles m_motionVectorMaxPass;
+	ComputePassHandles m_motionVectorMaxNeighbourhoodPass;
+	ComputePassHandles m_motionVectorVisualisationPass;
+};
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlurConfig.hpp b/projects/indirect_dispatch/src/MotionBlurConfig.hpp
new file mode 100644
index 00000000..ecd7f8f8
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlurConfig.hpp
@@ -0,0 +1,14 @@
+#pragma once
+#include "vulkan/vulkan.hpp"
+
+namespace MotionBlurConfig {
+	const vk::Format    motionVectorTileFormat  = vk::Format::eR16G16Sfloat;
+	const vk::Format    outputColorFormat       = vk::Format::eB10G11R11UfloatPack32;
+	const uint32_t      maxMotionTileSize       = 20;	// must match "motionTileSize" in motionVectorMax.comp
+
+	// small mouse movements are restricted to pixel level and therefore quite unprecise
+	// therefore extrapolating movement at high framerates results in big jerky movements
+	// this results in wide sudden motion blur, which looks quite bad
+	// as a workaround the time scale is limited to a maximum value
+	const float timeScaleMax = 1.f / 60;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlurSetup.cpp b/projects/indirect_dispatch/src/MotionBlurSetup.cpp
new file mode 100644
index 00000000..2ac1e7d1
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlurSetup.cpp
@@ -0,0 +1,41 @@
+#include "MotionBlurSetup.hpp"
+#include "MotionBlurConfig.hpp"
+
+namespace MotionBlurSetup {
+
+MotionBlurRenderTargets createRenderTargets(const uint32_t width, const uint32_t height, vkcv::Core& core) {
+
+	MotionBlurRenderTargets targets;
+
+	// divide and ceil to int
+	const uint32_t motionMaxWidth  = (width  + (MotionBlurConfig::maxMotionTileSize - 1)) / MotionBlurConfig::maxMotionTileSize;
+	const uint32_t motionMaxheight = (height + (MotionBlurConfig::maxMotionTileSize - 1)) / MotionBlurConfig::maxMotionTileSize;
+
+	targets.motionMax = core.createImage(
+		MotionBlurConfig::motionVectorTileFormat,
+		motionMaxWidth,
+		motionMaxheight,
+		1,
+		false,
+		true).getHandle();
+
+	targets.motionMaxNeighbourhood = core.createImage(
+		MotionBlurConfig::motionVectorTileFormat,
+		motionMaxWidth,
+		motionMaxheight,
+		1,
+		false,
+		true).getHandle();
+
+	targets.outputColor = core.createImage(
+		MotionBlurConfig::outputColorFormat,
+		width,
+		height,
+		1,
+		false,
+		true).getHandle();
+
+	return targets;
+}
+
+}	// namespace MotionBlurSetup
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlurSetup.hpp b/projects/indirect_dispatch/src/MotionBlurSetup.hpp
new file mode 100644
index 00000000..9c104ce7
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlurSetup.hpp
@@ -0,0 +1,12 @@
+#pragma once
+#include <vkcv/Core.hpp>
+
+struct MotionBlurRenderTargets {
+	vkcv::ImageHandle outputColor;
+	vkcv::ImageHandle motionMax;
+	vkcv::ImageHandle motionMaxNeighbourhood;
+};
+
+namespace MotionBlurSetup {
+	MotionBlurRenderTargets createRenderTargets(const uint32_t width, const uint32_t height, vkcv::Core& core);
+}
\ No newline at end of file
-- 
GitLab