diff --git a/projects/indirect_dispatch/resources/models/cube.bin b/projects/indirect_dispatch/resources/models/cube.bin
index 48295ae1e6df3d3b1f057229802b46a6d3efe9dc..728d38cd39cd10c30a93c15eef021cb0cf7dda74 100644
--- a/projects/indirect_dispatch/resources/models/cube.bin
+++ b/projects/indirect_dispatch/resources/models/cube.bin
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:0c9795a04850906951d977716160cae68a1555ee0b4f5c719c00c5d3314ac280
-size 648
+oid sha256:ccc59e0be3552b4347457bc935d8e548b52f12ca91716a0f0dc37d5bac65f123
+size 840
diff --git a/projects/indirect_dispatch/resources/models/cube.gltf b/projects/indirect_dispatch/resources/models/cube.gltf
index 38b1c218a965b4ccdf4e88b5d18a3c1bdc519847..ef975c326c71ec1a2fa650a422989534f1c32191 100644
--- a/projects/indirect_dispatch/resources/models/cube.gltf
+++ b/projects/indirect_dispatch/resources/models/cube.gltf
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:d710d2e005893256abeee910fdba8851e9c654c7f3b548bdd39bdbc3c043e73f
-size 2270
+oid sha256:0072448af64bdebffe8eec5a7f32f110579b1a256cd97438bf227e4cc4a87328
+size 2571
diff --git a/projects/indirect_dispatch/resources/models/ground.gltf b/projects/indirect_dispatch/resources/models/ground.gltf
index 6e8d49502b54d1fae06fa55040f3ebd7a33911d7..84c2100480357eb90ae5e07d96200c731297b21c 100644
--- a/projects/indirect_dispatch/resources/models/ground.gltf
+++ b/projects/indirect_dispatch/resources/models/ground.gltf
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:158b9c73a199aabaa1b0be99d81e5b5c13a963458916783d1c113f0f03c6c898
-size 2836
+oid sha256:e49f29edbbf3a2e3b8ab7200979054e2c3474c75170a54b98f1ca369d755d08e
+size 2840
diff --git a/projects/indirect_dispatch/resources/models/sphere.bin b/projects/indirect_dispatch/resources/models/sphere.bin
deleted file mode 100644
index 786ec34b2c359e707acd56306d175e72a34c984e..0000000000000000000000000000000000000000
--- a/projects/indirect_dispatch/resources/models/sphere.bin
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a1c5fefa4a07401791a2af9318665bca3b328cf11736b307e129ae8ba4c8931d
-size 17328
diff --git a/projects/indirect_dispatch/resources/models/sphere.gltf b/projects/indirect_dispatch/resources/models/sphere.gltf
deleted file mode 100644
index 05ca88fb02594b5fc7bff14420d198e3cfff7bc9..0000000000000000000000000000000000000000
--- a/projects/indirect_dispatch/resources/models/sphere.gltf
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:46911098be2cba7a817d6a2b92a65fbb36c48f3556385ef28d2fb5d9cddbcf2c
-size 1847
diff --git a/projects/indirect_dispatch/resources/shaders/mesh.frag b/projects/indirect_dispatch/resources/shaders/mesh.frag
index 7e57746eb8621dff65ce5896ec81d3b7d26d0533..531c9cbf8b5e097af618d2ca639821a62a30611d 100644
--- a/projects/indirect_dispatch/resources/shaders/mesh.frag
+++ b/projects/indirect_dispatch/resources/shaders/mesh.frag
@@ -2,12 +2,15 @@
 #extension GL_ARB_separate_shader_objects : enable
 
 layout(location = 0) in vec3 passNormal;
-layout(location = 1) in vec3 passPosObject;
+layout(location = 1) in vec2 passUV;
 
 layout(location = 0) out vec3 outColor;
 
+layout(set=0, binding=0)    uniform texture2D   albedoTexture;
+layout(set=0, binding=1)    uniform sampler     textureSampler;
+
 void main()	{
-    vec3    albedo  = vec3(sin(passPosObject.y * 100) * 0.5 + 0.5);
+    vec3    albedo  = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
     vec3    N       = normalize(passNormal);
     float   light   = max(N.y * 0.5 + 0.5, 0);
     outColor        = light * albedo;
diff --git a/projects/indirect_dispatch/resources/shaders/mesh.vert b/projects/indirect_dispatch/resources/shaders/mesh.vert
index 175d88c859bc9c7aab32a78023c7bab42c1e8e8b..734fd63cdee66e5fbf61cc427ca21fae18a31d82 100644
--- a/projects/indirect_dispatch/resources/shaders/mesh.vert
+++ b/projects/indirect_dispatch/resources/shaders/mesh.vert
@@ -3,9 +3,10 @@
 
 layout(location = 0) in vec3 inPosition;
 layout(location = 1) in vec3 inNormal;
+layout(location = 2) in vec2 inUV;
 
 layout(location = 0) out vec3 passNormal;
-layout(location = 1) out vec3 passPosObject;
+layout(location = 1) out vec2 passUV;
 
 layout( push_constant ) uniform constants{
     mat4 mvp;
@@ -13,7 +14,7 @@ layout( push_constant ) uniform constants{
 };
 
 void main()	{
-	gl_Position     = mvp * vec4(inPosition, 1.0);
-	passNormal      = (model * vec4(inNormal, 0)).xyz;
-    passPosObject   = inPosition;
+	gl_Position = mvp * vec4(inPosition, 1.0);
+	passNormal  = (model * vec4(inNormal, 0)).xyz;
+    passUV      = inUV;
 }
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/prepass.vert b/projects/indirect_dispatch/resources/shaders/prepass.vert
index 00b47280bd832e55718535c4f39879051a526eca..230346208007fae0bb7724b5b6d05f62726c4ded 100644
--- a/projects/indirect_dispatch/resources/shaders/prepass.vert
+++ b/projects/indirect_dispatch/resources/shaders/prepass.vert
@@ -2,7 +2,6 @@
 #extension GL_ARB_separate_shader_objects : enable
 
 layout(location = 0) in vec3 inPosition;
-layout(location = 1) in vec3 inNormal;
 
 layout(location = 0) out vec4 passNDC;
 layout(location = 1) out vec4 passNDCPrevious;
diff --git a/projects/indirect_dispatch/src/App.cpp b/projects/indirect_dispatch/src/App.cpp
index 8fb9031ba76cfa14f27ecaa56d3e7e89ea84f4b6..64ef58dafc38713c1a3319176732b77df3c98f11 100644
--- a/projects/indirect_dispatch/src/App.cpp
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -39,15 +39,15 @@ bool App::initialize() {
 	if (!loadComputePass(m_core, "resources/shaders/gammaCorrection.comp", &m_gammaCorrectionPass))
 		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 (!loadMesh(m_core, "resources/models/ground.gltf", &m_groundMesh))
 		return false;
 
+	if(!loadImage(m_core, "resources/models/grid.png", &m_gridTexture))
+		return false;
+
 	if (!m_motionBlur.initialize(&m_core, m_windowWidth, m_windowHeight))
 		return false;
 
@@ -63,6 +63,11 @@ bool App::initialize() {
 	m_cameraManager.getCamera(cameraIndex).setPosition(glm::vec3(0, 1, -3));
 	m_cameraManager.getCamera(cameraIndex).setNearFar(0.1f, 30.f);
 	
+	vkcv::DescriptorWrites meshPassDescriptorWrites;
+	meshPassDescriptorWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(0, m_gridTexture) };
+	meshPassDescriptorWrites.samplerWrites = { vkcv::SamplerDescriptorWrite(1, m_linearSampler) };
+	m_core.writeDescriptorSet(m_meshPass.descriptorSet, meshPassDescriptorWrites);
+
 	return true;
 }
 
@@ -101,6 +106,8 @@ void App::run() {
 
 	glm::mat4 viewProjectionPrevious    = m_cameraManager.getActiveCamera().getMVP();
 
+
+
 	struct Object {
 		MeshResources meshResources;
 		glm::mat4 modelMatrix   = glm::mat4(1.f);
@@ -175,9 +182,9 @@ void App::run() {
 			m_renderTargets.motionBuffer,
 			m_renderTargets.depthBuffer };
 
-		std::vector<vkcv::DrawcallInfo> sceneDrawcalls;
+		std::vector<vkcv::DrawcallInfo> prepassSceneDrawcalls;
 		for (const Object& obj : sceneObjects) {
-			sceneDrawcalls.push_back(vkcv::DrawcallInfo(obj.meshResources.mesh, {}));
+			prepassSceneDrawcalls.push_back(vkcv::DrawcallInfo(obj.meshResources.mesh, {}));
 		}
 
 		m_core.recordDrawcallsToCmdStream(
@@ -185,7 +192,7 @@ void App::run() {
 			m_prePass.renderPass,
 			m_prePass.pipeline,
 			prepassPushConstants,
-			sceneDrawcalls,
+			prepassSceneDrawcalls,
 			prepassRenderTargets);
 
 		// sky prepass
@@ -214,12 +221,19 @@ void App::run() {
 			meshPushConstants.appendDrawcall(matrices);
 		}
 
+		std::vector<vkcv::DrawcallInfo> forwardSceneDrawcalls;
+		for (const Object& obj : sceneObjects) {
+			forwardSceneDrawcalls.push_back(vkcv::DrawcallInfo(
+				obj.meshResources.mesh, 
+				{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_meshPass.descriptorSet).vulkanHandle) }));
+		}
+
 		m_core.recordDrawcallsToCmdStream(
 			cmdStream,
 			m_meshPass.renderPass,
 			m_meshPass.pipeline,
 			meshPushConstants,
-			sceneDrawcalls,
+			forwardSceneDrawcalls,
 			renderTargets);
 
 		// sky
diff --git a/projects/indirect_dispatch/src/App.hpp b/projects/indirect_dispatch/src/App.hpp
index ffe75c87bcb3462746129a32e1657e16566ac663..d580793b0fdc4e7dc8c8654d29a75f04e14ea422 100644
--- a/projects/indirect_dispatch/src/App.hpp
+++ b/projects/indirect_dispatch/src/App.hpp
@@ -21,7 +21,8 @@ private:
 
 	MotionBlur m_motionBlur;
 
-	MeshResources m_sphereMesh;
+	vkcv::ImageHandle m_gridTexture;
+
 	MeshResources m_cubeMesh;
 	MeshResources m_groundMesh;
 
diff --git a/projects/indirect_dispatch/src/AppSetup.cpp b/projects/indirect_dispatch/src/AppSetup.cpp
index 1d27e14112b538d3ca10f9369b5dfad3791ba30e..023e30fc63360d426856be3940749e95089f2577 100644
--- a/projects/indirect_dispatch/src/AppSetup.cpp
+++ b/projects/indirect_dispatch/src/AppSetup.cpp
@@ -48,13 +48,40 @@ bool loadMesh(vkcv::Core& core, const std::filesystem::path& path, MeshResources
 
 	const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
 		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[0].offset), vertexBuffer.getVulkanHandle()),
-		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[1].offset), vertexBuffer.getVulkanHandle()) };
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[1].offset), vertexBuffer.getVulkanHandle()),
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[2].offset), vertexBuffer.getVulkanHandle()) };
 
 	outMesh->mesh = vkcv::Mesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), scene.vertexGroups[0].numIndices);
 
 	return true;
 }
 
+bool loadImage(vkcv::Core& core, const std::filesystem::path& path, vkcv::ImageHandle* outImage) {
+
+	assert(outImage);
+
+	const vkcv::asset::TextureData textureData = vkcv::asset::loadTexture(path);
+
+	if (textureData.componentCount != 4) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Expecting image with four components");
+		return false;
+	}
+
+	vkcv::Image image = core.createImage(
+		vk::Format::eR8G8B8A8Srgb, 
+		textureData.width, 
+		textureData.height, 
+		1, 
+		true);
+
+	image.fill(textureData.data.data(), textureData.data.size());
+	image.generateMipChainImmediate();
+	image.switchLayout(vk::ImageLayout::eReadOnlyOptimalKHR);
+
+	*outImage = image.getHandle();
+	return true;
+}
+
 bool loadGraphicPass(
 	vkcv::Core& core,
 	const std::filesystem::path vertexPath,
@@ -93,13 +120,22 @@ bool loadGraphicPass(
 
 	const vkcv::VertexLayout vertexLayout(bindings);
 
+	const auto descriptorBindings = shaderProgram.getReflectedDescriptors();
+	const bool hasDescriptor = descriptorBindings.size() > 0;
+	if (hasDescriptor)
+		outPassHandles->descriptorSet = core.createDescriptorSet(descriptorBindings[0]);
+
+	std::vector<vk::DescriptorSetLayout> descriptorLayouts;
+	if (hasDescriptor)
+		descriptorLayouts.push_back(core.getDescriptorSet(outPassHandles->descriptorSet).layout);
+
 	vkcv::PipelineConfig pipelineConfig{
 		shaderProgram,
 		UINT32_MAX,
 		UINT32_MAX,
 		outPassHandles->renderPass,
 		{ vertexLayout },
-		{},
+		descriptorLayouts,
 		true };
 	pipelineConfig.m_depthTest  = depthTest;
 	outPassHandles->pipeline    = core.createGraphicsPipeline(pipelineConfig);
diff --git a/projects/indirect_dispatch/src/AppSetup.hpp b/projects/indirect_dispatch/src/AppSetup.hpp
index f4aaad0aea735d30f3d742721880ece398de4a8e..3125bc516b553de715d6e51bbda259e3e16f758f 100644
--- a/projects/indirect_dispatch/src/AppSetup.hpp
+++ b/projects/indirect_dispatch/src/AppSetup.hpp
@@ -8,8 +8,9 @@ struct AppRenderTargets {
 };
 
 struct GraphicPassHandles {
-	vkcv::PipelineHandle    pipeline;
-	vkcv::PassHandle        renderPass;
+	vkcv::PipelineHandle        pipeline;
+	vkcv::PassHandle            renderPass;
+	vkcv::DescriptorSetHandle   descriptorSet;
 };
 
 struct ComputePassHandles {
@@ -23,9 +24,11 @@ struct MeshResources {
 	vkcv::BufferHandle  indexBuffer;
 };
 
-// loads position and normal of the first mesh in a scene
+// loads position, uv and normal of the first mesh in a scene
 bool loadMesh(vkcv::Core& core, const std::filesystem::path& path, MeshResources* outMesh);
 
+bool loadImage(vkcv::Core& core, const std::filesystem::path& path, vkcv::ImageHandle* outImage);
+
 bool loadGraphicPass(
 	vkcv::Core& core,
 	const std::filesystem::path vertexPath,