diff --git a/modules/camera/src/vkcv/camera/CameraManager.cpp b/modules/camera/src/vkcv/camera/CameraManager.cpp
index c8aa4f7e0e493a2aaf5bfd6d93768e169cd255b9..4c713c29e5dad7b72bbdbb1372e4cf0f5bb3d162 100644
--- a/modules/camera/src/vkcv/camera/CameraManager.cpp
+++ b/modules/camera/src/vkcv/camera/CameraManager.cpp
@@ -130,7 +130,7 @@ namespace vkcv::camera {
     }
 
     Camera& CameraManager::getCamera(uint32_t cameraIndex) {
-        if (cameraIndex < 0 || cameraIndex > m_cameras.size() - 1) {
+        if (cameraIndex < 0 || cameraIndex >= m_cameras.size()) {
         	vkcv_log(LogLevel::ERROR, "Invalid camera index: The index must range from 0 to %lu", m_cameras.size());
         	return getActiveCamera();
         }
@@ -143,7 +143,7 @@ namespace vkcv::camera {
     }
 
     void CameraManager::setActiveCamera(uint32_t cameraIndex) {
-        if (cameraIndex < 0 || cameraIndex > m_cameras.size() - 1) {
+        if (cameraIndex < 0 || cameraIndex >= m_cameras.size()) {
 			vkcv_log(LogLevel::ERROR, "Invalid camera index: The index must range from 0 to %lu", m_cameras.size());
 			return;
         }
@@ -156,7 +156,7 @@ namespace vkcv::camera {
     }
 
     void CameraManager::setControllerType(uint32_t cameraIndex, ControllerType controllerType) {
-        if (cameraIndex < 0 || cameraIndex > m_cameras.size() - 1) {
+        if (cameraIndex < 0 || cameraIndex >= m_cameras.size()) {
 			vkcv_log(LogLevel::ERROR, "Invalid camera index: The index must range from 0 to %lu", m_cameras.size());
 			return;
         }
@@ -165,7 +165,7 @@ namespace vkcv::camera {
     }
 
     ControllerType CameraManager::getControllerType(uint32_t cameraIndex) {
-        if (cameraIndex < 0 || cameraIndex > m_cameras.size() - 1) {
+        if (cameraIndex < 0 || cameraIndex >= m_cameras.size()) {
 			vkcv_log(LogLevel::ERROR, "Invalid camera index: The index must range from 0 to %lu", m_cameras.size());
 			return ControllerType::NONE;
         }
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 437021a6b2ea52ae4a3fccff625f0c7e2ef8ddd0..1fffd2dab6cd34368d7140c857e7b885cea998cb 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -2,18 +2,19 @@
 include(${vkcv_config_ext}/Project.cmake)
 
 # Add new projects/examples here:
+add_subdirectory(bindless_textures)
+add_subdirectory(fire_works)
 add_subdirectory(first_triangle)
 add_subdirectory(first_mesh)
 add_subdirectory(first_scene)
 add_subdirectory(head_demo)
+add_subdirectory(indirect_dispatch)
+add_subdirectory(indirect_draw)
+add_subdirectory(mesh_shader)
 add_subdirectory(particle_simulation)
+add_subdirectory(path_tracer)
 add_subdirectory(rtx_ambient_occlusion)
+add_subdirectory(saf_r)
 add_subdirectory(sph)
 add_subdirectory(voxelization)
-add_subdirectory(mesh_shader)
-add_subdirectory(indirect_draw)
-add_subdirectory(bindless_textures)
-add_subdirectory(saf_r)
-add_subdirectory(indirect_dispatch)
-add_subdirectory(path_tracer)
 add_subdirectory(wobble_bobble)
\ No newline at end of file
diff --git a/projects/fire_works/.gitignore b/projects/fire_works/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a991f1c077c11db780beb6e3d01c5bd561336690
--- /dev/null
+++ b/projects/fire_works/.gitignore
@@ -0,0 +1 @@
+fire_works
diff --git a/projects/fire_works/CMakeLists.txt b/projects/fire_works/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7f9fd1fdd30bff0b331138821f10035f4b7dc64d
--- /dev/null
+++ b/projects/fire_works/CMakeLists.txt
@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.16)
+project(fire_works)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# adding source files to the project
+add_project(fire_works
+		src/main.cpp)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(fire_works SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_camera_include}
+		${vkcv_gui_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_effects_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(fire_works
+		vkcv
+		vkcv_camera
+		vkcv_gui
+		vkcv_shader_compiler
+		vkcv_effects)
diff --git a/projects/fire_works/shaders/motion.comp b/projects/fire_works/shaders/motion.comp
new file mode 100644
index 0000000000000000000000000000000000000000..87cea7fa842a20d5ebd3e55be6bf28d93590508e
--- /dev/null
+++ b/projects/fire_works/shaders/motion.comp
@@ -0,0 +1,41 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) coherent buffer particleBuffer {
+    particle_t particles [];
+};
+
+layout( push_constant ) uniform constants{
+    float dt;
+};
+
+const float g = 9.81;
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if (id >= particles.length()) {
+        return;
+    }
+
+    vec3 position = particles[id].position;
+    float lifetime = particles[id].lifetime;
+    vec3 velocity = particles[id].velocity;
+
+    if (lifetime > dt) {
+        lifetime -= dt;
+    } else {
+        lifetime = 0.0f;
+    }
+
+    position = position + velocity * dt;
+    velocity = velocity + vec3(0.0f, -g, 0.0f) * dt;
+
+    particles[id].position = position;
+    particles[id].velocity = velocity;
+}
diff --git a/projects/fire_works/shaders/particle.frag b/projects/fire_works/shaders/particle.frag
new file mode 100644
index 0000000000000000000000000000000000000000..a13b295a7543f489b23b97a93e904e60e3dac334
--- /dev/null
+++ b/projects/fire_works/shaders/particle.frag
@@ -0,0 +1,16 @@
+#version 450
+
+layout(location = 0) in vec2 passPos;
+layout(location = 1) in vec3 passColor;
+
+layout(location = 0) out vec3 outColor;
+
+void main()	{
+    const float value = length(passPos);
+
+    if (value < 0.5f) {
+        outColor = passColor;
+    } else {
+        discard;
+    }
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/particle.inc b/projects/fire_works/shaders/particle.inc
new file mode 100644
index 0000000000000000000000000000000000000000..f3da76332452a17256a53dabb0ade6abb0b4caea
--- /dev/null
+++ b/projects/fire_works/shaders/particle.inc
@@ -0,0 +1,13 @@
+#ifndef PARTICLE_INC
+#define PARTICLE_INC
+
+struct particle_t {
+	vec3 position;
+	float lifetime;
+	vec3 velocity;
+	float size;
+	vec3 color;
+	float mass;
+};
+
+#endif // PARTICLE_INC
\ No newline at end of file
diff --git a/projects/fire_works/shaders/particle.vert b/projects/fire_works/shaders/particle.vert
new file mode 100644
index 0000000000000000000000000000000000000000..ba4dbdb2cfdaf929e74a8668b8adb619b6a548fa
--- /dev/null
+++ b/projects/fire_works/shaders/particle.vert
@@ -0,0 +1,30 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) readonly buffer particleBuffer {
+    particle_t particles [];
+};
+
+layout(location = 0) in vec2 vertexPos;
+
+layout(location = 0) out vec2 passPos;
+layout(location = 1) out vec3 passColor;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main()	{
+    vec3 position = particles[gl_InstanceIndex].position;
+    float size = particles[gl_InstanceIndex].size;
+    vec3 color = particles[gl_InstanceIndex].color;
+
+    passPos = vertexPos;
+    passColor = color;
+
+    // align particle to face camera
+    gl_Position = mvp * vec4(position, 1);      // transform position into projected view space
+    gl_Position.xy += vertexPos * size * 2.0f;  // move position directly in view space
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/tonemapping.comp b/projects/fire_works/shaders/tonemapping.comp
new file mode 100644
index 0000000000000000000000000000000000000000..5e6cc8412a77939888fc8e961ea5e9ef29534a81
--- /dev/null
+++ b/projects/fire_works/shaders/tonemapping.comp
@@ -0,0 +1,21 @@
+#version 440
+
+layout(set=0, binding=0, rgba16f) readonly uniform image2D inImage;
+layout(set=0, binding=1, rgba8) writeonly uniform image2D outImage;
+
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main() {
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(inImage)))){
+        return;
+    }
+
+    ivec2 uv            = ivec2(gl_GlobalInvocationID.xy);
+    vec3 linearColor    = imageLoad(inImage, uv).rgb;
+
+    vec3 tonemapped     = linearColor / (dot(linearColor, vec3(0.21, 0.71, 0.08)) + 1); // reinhard tonemapping
+    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
+
+    imageStore(outImage, uv, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/fire_works/src/main.cpp b/projects/fire_works/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..93d820f6680c54f6ecb88c5b1dda760dc7b8840b
--- /dev/null
+++ b/projects/fire_works/src/main.cpp
@@ -0,0 +1,318 @@
+
+#include <vkcv/Core.hpp>
+#include <vkcv/DrawcallRecording.hpp>
+
+#include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/gui/GUI.hpp>
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
+
+struct particle_t {
+	glm::vec3 position;
+	float lifetime;
+	glm::vec3 velocity;
+	float size;
+	glm::vec3 color;
+	float pad2;
+};
+
+int main(int argc, const char **argv) {
+	vkcv::Core core = vkcv::Core::create(
+		"Firework",
+		VK_MAKE_VERSION(0, 0, 1),
+		{vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
+		{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
+	);
+	
+	vkcv::WindowHandle windowHandle = core.createWindow("Firework", 800, 600, true);
+	vkcv::Window& window = core.getWindow (windowHandle);
+	vkcv::camera::CameraManager cameraManager (window);
+	
+	uint32_t trackballIdx = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+	cameraManager.getCamera(trackballIdx).setCenter(glm::vec3(0.0f, 0.0f, 0.0f));   // set camera to look at the center of the particle volume
+	cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	
+	cameraManager.getCamera(trackballIdx).setNearFar(0.1f, 50.0f);
+	cameraManager.getCamera(trackballIdx).setPosition(glm::vec3(0, 0, -25));
+	
+	vkcv::gui::GUI gui (core, windowHandle);
+	
+	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
+	
+	const vk::Format depthFormat = vk::Format::eD32Sfloat;
+	
+	vkcv::ImageHandle depthBuffer = core.createImage(
+		depthFormat,
+		swapchainExtent.width,
+		swapchainExtent.height
+	).getHandle();
+	
+	const vk::Format colorFormat = vk::Format::eR16G16B16A16Sfloat;
+	
+	vkcv::ImageHandle colorBuffer = core.createImage(
+		colorFormat,
+		swapchainExtent.width,
+		swapchainExtent.height,
+		1, false, true, true
+	).getHandle();
+	
+	vkcv::shader::GLSLCompiler compiler;
+	vkcv::ShaderProgram particleShaderProgram;
+	
+	compiler.compile(vkcv::ShaderStage::VERTEX, "shaders/particle.vert", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		particleShaderProgram.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, "shaders/particle.frag", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		particleShaderProgram.addShader(shaderStage, path);
+	});
+	
+	vkcv::DescriptorBindings descriptorBindings;
+	vkcv::DescriptorBinding binding {
+		0,
+		vkcv::DescriptorType::STORAGE_BUFFER,
+		1,
+		vkcv::ShaderStage::VERTEX | vkcv::ShaderStage::COMPUTE,
+		false,
+		false
+	};
+	
+	descriptorBindings.insert(std::make_pair(0, binding));
+	
+	vkcv::DescriptorSetLayoutHandle descriptorSetLayout = core.createDescriptorSetLayout(descriptorBindings);
+	vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(descriptorSetLayout);
+	
+	std::vector<particle_t> particles;
+	
+	for (size_t i = 0; i < 1024; i++) {
+		particle_t particle;
+		particle.position = glm::vec3(
+			2.0f * (std::rand() % RAND_MAX) / RAND_MAX - 1.0f,
+			2.0f * (std::rand() % RAND_MAX) / RAND_MAX - 1.0f,
+			2.0f * (std::rand() % RAND_MAX) / RAND_MAX - 1.0f
+		);
+		
+		particle.lifetime = 0.0f;
+		particle.velocity = glm::vec3(0.0f);
+		particle.size = 0.01f;
+		particle.color = glm::vec3(1.0f, 0.0f, 0.0f);
+		
+		particles.push_back(particle);
+	}
+	
+	vkcv::Buffer<particle_t> particleBuffer = core.createBuffer<particle_t>(
+		vkcv::BufferType::STORAGE,
+		particles.size()
+	);
+	
+	particleBuffer.fill(particles);
+	
+	vkcv::DescriptorWrites writes;
+	writes.writeStorageBuffer(0, particleBuffer.getHandle());
+	
+	core.writeDescriptorSet(descriptorSet, writes);
+	
+	vkcv::Buffer<glm::vec2> trianglePositions = core.createBuffer<glm::vec2>(vkcv::BufferType::VERTEX, 3);
+	trianglePositions.fill({
+		glm::vec2(-1.0f, -1.0f),
+		glm::vec2(+0.0f, +1.5f),
+		glm::vec2(+1.0f, -1.0f)
+	});
+	
+	vkcv::Buffer<uint16_t> triangleIndices = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3);
+	triangleIndices.fill({
+		0, 1, 2
+	});
+	
+	vkcv::Mesh triangleMesh (
+		{ vkcv::VertexBufferBinding(0, trianglePositions.getVulkanHandle()) },
+		triangleIndices.getVulkanHandle(),
+		triangleIndices.getCount()
+	);
+	
+	const std::vector<vkcv::VertexAttachment> vertexAttachments = particleShaderProgram.getVertexAttachments();
+	
+	const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
+		vkcv::VertexBufferBinding(0, trianglePositions.getVulkanHandle())
+	};
+	
+	std::vector<vkcv::VertexBinding> bindings;
+	for (size_t i = 0; i < vertexAttachments.size(); i++) {
+		bindings.push_back(vkcv::createVertexBinding(i, {vertexAttachments[i]}));
+	}
+
+	const vkcv::VertexLayout particleLayout { bindings };
+	
+	const vkcv::AttachmentDescription present_color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		colorFormat
+	);
+	
+	const vkcv::AttachmentDescription depth_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		depthFormat
+	);
+	
+	vkcv::PassConfig particlePassDefinition({present_color_attachment, depth_attachment}, vkcv::Multisampling::None);
+	vkcv::PassHandle particlePass = core.createPass(particlePassDefinition);
+
+	vkcv::GraphicsPipelineConfig particlePipelineDefinition{
+		particleShaderProgram,
+		UINT32_MAX,
+		UINT32_MAX,
+		particlePass,
+		{particleLayout},
+		{descriptorSetLayout},
+		true
+	};
+	
+	// particlePipelineDefinition.m_blendMode = vkcv::BlendMode::Additive;
+	
+	vkcv::GraphicsPipelineHandle particlePipeline = core.createGraphicsPipeline(particlePipelineDefinition);
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsParticles;
+	
+	drawcallsParticles.push_back(vkcv::DrawcallInfo(
+		triangleMesh,
+		{ vkcv::DescriptorSetUsage(0, descriptorSet) },
+		particleBuffer.getCount()
+	));
+	
+	vkcv::ShaderProgram motionShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/motion.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		motionShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::ComputePipelineHandle motionPipeline = core.createComputePipeline({
+		motionShader,
+		{ descriptorSetLayout }
+	});
+	
+	vkcv::effects::BloomAndFlaresEffect bloomAndFlares (core);
+	bloomAndFlares.setUpsamplingLimit(3);
+	
+	vkcv::ShaderProgram tonemappingShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/tonemapping.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		tonemappingShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::DescriptorSetLayoutHandle tonemappingDescriptorLayout = core.createDescriptorSetLayout(tonemappingShader.getReflectedDescriptors().at(0));
+	vkcv::DescriptorSetHandle tonemappingDescriptor = core.createDescriptorSet(tonemappingDescriptorLayout);
+	vkcv::ComputePipelineHandle tonemappingPipe = core.createComputePipeline({
+		tonemappingShader,
+		{ tonemappingDescriptorLayout }
+	});
+	
+	vkcv::ImageHandle swapchainImage = vkcv::ImageHandle::createSwapchainImageHandle();
+	
+	auto start = std::chrono::system_clock::now();
+	auto current = start;
+	
+	while (vkcv::Window::hasOpenWindow()) {
+		vkcv::Window::pollEvents();
+		
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight, windowHandle)) {
+			continue;
+		}
+	
+		if ((core.getImageWidth(colorBuffer) != swapchainWidth) ||
+			(core.getImageHeight(colorBuffer) != swapchainHeight)) {
+			colorBuffer = core.createImage(
+				  colorFormat,
+				  swapchainWidth,
+				  swapchainHeight,
+				  1, false, true, true
+		  	).getHandle();
+		}
+		
+		if ((core.getImageWidth(depthBuffer) != swapchainWidth) ||
+			(core.getImageHeight(depthBuffer) != swapchainHeight)) {
+			depthBuffer = core.createImage(
+				  depthFormat,
+				  swapchainWidth,
+				  swapchainHeight
+			).getHandle();
+		}
+		
+		auto next = std::chrono::system_clock::now();
+		
+		auto time = std::chrono::duration_cast<std::chrono::microseconds>(next - start);
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(next - current);
+		
+		current = next;
+		
+		// auto t = static_cast<float>(0.000001 * static_cast<double>(time.count()));
+		auto dt = static_cast<float>(0.000001 * static_cast<double>(deltatime.count()));
+		
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		
+		uint32_t motionDispatchCount[3];
+		motionDispatchCount[0] = std::ceil(particleBuffer.getCount() / 256.f);
+		motionDispatchCount[1] = 1;
+		motionDispatchCount[2] = 1;
+		
+		vkcv::PushConstants pushConstantsTime (sizeof(float));
+		pushConstantsTime.appendDrawcall(dt);
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			motionPipeline,
+			motionDispatchCount,
+			{vkcv::DescriptorSetUsage(0, descriptorSet) },
+			pushConstantsTime
+		);
+		
+		cameraManager.update(dt);
+		
+		const auto& camera = cameraManager.getActiveCamera();
+		
+		vkcv::PushConstants pushConstantsDraw (sizeof(glm::mat4));
+		pushConstantsDraw.appendDrawcall(camera.getMVP());
+		
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			particlePass,
+			particlePipeline,
+			pushConstantsDraw,
+			{drawcallsParticles},
+			{ colorBuffer, depthBuffer },
+			windowHandle
+		);
+		
+		bloomAndFlares.recordEffect(cmdStream, colorBuffer, colorBuffer);
+		
+		core.prepareImageForStorage(cmdStream, colorBuffer);
+		core.prepareImageForStorage(cmdStream, swapchainImage);
+		
+		vkcv::DescriptorWrites tonemappingDescriptorWrites;
+		tonemappingDescriptorWrites.writeStorageImage(
+			0, colorBuffer
+		).writeStorageImage(
+			1, swapchainImage
+		);
+		
+		core.writeDescriptorSet(tonemappingDescriptor, tonemappingDescriptorWrites);
+
+		uint32_t tonemappingDispatchCount[3];
+		tonemappingDispatchCount[0] = std::ceil(swapchainWidth / 8.f);
+		tonemappingDispatchCount[1] = std::ceil(swapchainHeight / 8.f);
+		tonemappingDispatchCount[2] = 1;
+
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			tonemappingPipe,
+			tonemappingDispatchCount,
+			{vkcv::DescriptorSetUsage(0, tonemappingDescriptor) },
+			vkcv::PushConstants(0)
+		);
+		
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		
+		core.endFrame(windowHandle);
+	}
+	
+	return 0;
+}