diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2b74cee67be765d9d60850ad2c28c5510f3eadcc..1b69b6d5947b04efa72e1699e74c7022791693da 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,6 +9,10 @@ option(BUILD_DOXYGEN_DOCS "Enables building the VkCV doxygen documentation" OFF)
 option(BUILD_SHARED "Enables building VkCV as shared libraries" OFF)
 option(BUILD_VMA_VULKAN_VERSION "Enforce a specific Vulkan version for VMA" OFF)
 
+if ((WIN32) AND (NOT BUILD_VMA_VULKAN_VERSION))
+	set(BUILD_VMA_VULKAN_VERSION "1.3.0")
+endif()
+
 if (BUILD_PROJECTS)
 	set(BUILD_MODULES ${BUILD_PROJECTS})
 endif()
diff --git a/modules/algorithm/src/vkcv/algorithm/SinglePassDownsampler.cpp b/modules/algorithm/src/vkcv/algorithm/SinglePassDownsampler.cpp
index 3a8408eb4434dbb0c065ef7a16842e8fc73275a1..eee9f361493c35c5214ac661448f3cc80384e4ee 100644
--- a/modules/algorithm/src/vkcv/algorithm/SinglePassDownsampler.cpp
+++ b/modules/algorithm/src/vkcv/algorithm/SinglePassDownsampler.cpp
@@ -3,6 +3,7 @@
 
 #include <cstdint>
 #include <cmath>
+#include <vector>
 
 #define A_CPU 1
 #include <ffx_a.h>
@@ -237,8 +238,9 @@ namespace vkcv::algorithm {
 			{ m_descriptorSetLayout }
 		));
 		
-		uint32_t zeroes [m_globalCounter.getCount()];
-		memset(zeroes, 0, m_globalCounter.getSize());
+		std::vector<uint32_t> zeroes;
+		zeroes.resize(m_globalCounter.getCount());
+		memset(zeroes.data(), 0, m_globalCounter.getSize());
 		m_globalCounter.fill(zeroes);
 	}
 	
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/add.comp b/projects/fire_works/shaders/add.comp
new file mode 100644
index 0000000000000000000000000000000000000000..737a1f3fd69e237f7692be8a6b36b1e14f1fd50a
--- /dev/null
+++ b/projects/fire_works/shaders/add.comp
@@ -0,0 +1,64 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+layout(set=0, binding=0) uniform texture2D voxelTexture;
+layout(set=0, binding=1) uniform sampler voxelSampler;
+
+layout(set=0, binding=2, rgba16f) restrict readonly uniform image2D inParticles;
+layout(set=0, binding=3, rgba16f) restrict readonly uniform image2D inSmoke;
+layout(set=0, binding=4, rgba16f) restrict readonly uniform image2D inTrails;
+layout(set=0, binding=5, rgba16f) restrict writeonly uniform image2D outImage;
+
+layout(set=1, binding=0, std430) readonly buffer randomBuffer {
+    float randomData [];
+};
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+#include "physics.inc"
+#include "smoke.inc"
+
+#define NUM_VOXEL_SAMPLES 32
+
+shared vec2 sc_data [NUM_VOXEL_SAMPLES];
+
+void main() {
+    const float localRadian = 0.25f * pi * randomData[gl_LocalInvocationIndex % randomData.length()];
+
+    sc_data[gl_LocalInvocationIndex % NUM_VOXEL_SAMPLES] = vec2(
+        sin(localRadian), cos(localRadian)
+    );
+
+    memoryBarrierShared();
+    barrier();
+
+    const ivec2 res = imageSize(outImage);
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, res))){
+        return;
+    }
+
+    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
+
+    vec4 outParticles = imageLoad(inParticles, uv);
+    vec4 outSmoke = imageLoad(inSmoke, uv);
+    vec4 outTrails = imageLoad(inTrails, uv);
+
+    vec2 pos = (vec2(uv) + vec2(0.5f)) / vec2(res);
+
+    vec4 outSamples = texture(sampler2D(voxelTexture, voxelSampler), pos);
+
+    vec4 result = vec4(0.0f);
+
+    result = smokeBlend(result, outParticles);
+    result = smokeBlend(result, outTrails);
+    result = smokeBlend(result, outSmoke);
+    result = smokeBlend(result, outSamples * 0.1f);
+
+    result.r = clamp(result.r, 0, 1);
+    result.g = clamp(result.g, 0, 1);
+    result.b = clamp(result.b, 0, 1);
+    result.a = clamp(result.a, 0, 1);
+
+    imageStore(outImage, uv, result);
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/clear.comp b/projects/fire_works/shaders/clear.comp
new file mode 100644
index 0000000000000000000000000000000000000000..4668538c4a38aefec868f1817a126b5278f8fd78
--- /dev/null
+++ b/projects/fire_works/shaders/clear.comp
@@ -0,0 +1,25 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "physics.inc"
+#include "voxel.inc"
+
+layout(set=0, binding=0, r32ui) restrict writeonly uniform uimage3D voxelRed;
+layout(set=0, binding=1, r32ui) restrict writeonly uniform uimage3D voxelGreen;
+layout(set=0, binding=2, r32ui) restrict writeonly uniform uimage3D voxelBlue;
+layout(set=0, binding=3, r32ui) restrict writeonly uniform uimage3D voxelDensity;
+
+layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
+
+void main() {
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xyz, imageSize(voxelDensity)))){
+        return;
+    }
+
+    ivec3 pos = ivec3(gl_GlobalInvocationID.xyz);
+
+    voxel_write(voxelRed, pos, 0.0f);
+    voxel_write(voxelGreen, pos, 0.0f);
+    voxel_write(voxelBlue, pos, 0.0f);
+    voxel_write(voxelDensity, pos, mediumDensity);
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/event.inc b/projects/fire_works/shaders/event.inc
new file mode 100644
index 0000000000000000000000000000000000000000..a2ab170c0894da68638b0e61e4af90a7a5fcdd64
--- /dev/null
+++ b/projects/fire_works/shaders/event.inc
@@ -0,0 +1,21 @@
+#ifndef EVENT_INC
+#define EVENT_INC
+
+struct event_t {
+	vec3 direction;
+	float startTime;
+	vec3 color;
+	float velocity;
+	
+	uint count;
+	uint index;
+	uint parent;
+	uint continuous;
+	
+	float lifetime;
+	float mass;
+	float size;
+	uint contCount;
+};
+
+#endif // EVENT_INC
\ No newline at end of file
diff --git a/projects/fire_works/shaders/fluid.comp b/projects/fire_works/shaders/fluid.comp
new file mode 100644
index 0000000000000000000000000000000000000000..076a280739e542c0579a7b80eaeaf4993df13edf
--- /dev/null
+++ b/projects/fire_works/shaders/fluid.comp
@@ -0,0 +1,80 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
+
+#include "physics.inc"
+#include "voxel.inc"
+
+layout(set=0, binding=0) uniform texture3D voxelTexture;
+layout(set=0, binding=1) uniform sampler voxelSampler;
+layout(set=0, binding=2, rgba16) restrict writeonly uniform image3D fluidImage;
+
+vec4 getDataFrom(vec3 position, vec3 offset) {
+    return texture(
+        sampler3D(
+            voxelTexture,
+            voxelSampler
+        ),
+        position + offset
+    );
+}
+
+shared vec4 cachedData [4][4][4];
+
+void storeCachedData(vec3 position) {
+    uvec3 localId = gl_LocalInvocationID;
+    cachedData[localId.x][localId.y][localId.z] = getDataFrom(position, vec3(0));
+}
+
+vec4 getCachedData() {
+    uvec3 localId = gl_LocalInvocationID;
+    return cachedData[localId.x][localId.y][localId.z];
+}
+
+vec4 loadCachedData(vec3 position, ivec3 offset, ivec3 size) {
+    uvec3 localId = gl_LocalInvocationID;
+    ivec3 index = ivec3(localId) + offset;
+
+    // TOO SPECIAL FOR GPU TO WORK..!
+    //if ((any(lessThan(index, ivec3(0)))) || (any(greaterThan(index, ivec3(gl_WorkGroupSize))))) {
+    return getDataFrom(position, vec3(offset) / vec3(size));
+    //} else {
+    //    return cachedData[index.x][index.y][index.z];
+    //}
+}
+
+void main() {
+    uvec3 id = gl_GlobalInvocationID;
+    ivec3 size = imageSize(fluidImage);
+
+    if (any(greaterThanEqual(id, size))) {
+        return;
+    }
+
+    vec3 position = (vec3(id) + vec3(0.5f)) / vec3(size);
+
+    storeCachedData(position);
+    memoryBarrierShared();
+    barrier();
+
+    vec4 extData [6];
+
+    extData[0] = loadCachedData(position, ivec3(+1, 0, 0), size);
+    extData[1] = loadCachedData(position, ivec3(-1, 0, 0), size);
+    extData[2] = loadCachedData(position, ivec3(0, +1, 0), size);
+    extData[3] = loadCachedData(position, ivec3(0, -1, 0), size);
+    extData[4] = loadCachedData(position, ivec3(0, 0, +1), size);
+    extData[5] = loadCachedData(position, ivec3(0, 0, -1), size);
+
+    vec4 data = vec4(0);
+
+    for (uint i = 0; i < 6; i++) {
+        data += extData[i];
+    }
+
+    data = mix(getCachedData(), (data / 6), flowRate);
+
+    imageStore(fluidImage, ivec3(id), data);
+}
diff --git a/projects/fire_works/shaders/generation.comp b/projects/fire_works/shaders/generation.comp
new file mode 100644
index 0000000000000000000000000000000000000000..eb585236d993099350d0c0e6b97adcbfd28fdf57
--- /dev/null
+++ b/projects/fire_works/shaders/generation.comp
@@ -0,0 +1,179 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "physics.inc"
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) buffer particleBuffer {
+    particle_t particles [];
+};
+
+layout(set=0, binding=1, std430) readonly buffer particleBufferCopy {
+    particle_t particlesCopy [];
+};
+
+layout(set=1, binding=0, std430) readonly buffer randomBuffer {
+    float randomData [];
+};
+
+#include "event.inc"
+
+layout(set=1, binding=1, std430) buffer eventBuffer {
+    event_t events [];
+};
+
+layout(set=1, binding=2, std430) buffer startIndexBuffer {
+    uint startIndex [];
+};
+
+#include "smoke.inc"
+
+layout(set=2, binding=0, std430) writeonly buffer smokeBuffer {
+    smoke_t smokes [];
+};
+
+layout(set=2, binding=1, std430) buffer smokeIndexBuffer {
+    uint smokeIndex;
+    uint trailIndex;
+    uint pointIndex;
+};
+
+#include "trail.inc"
+
+layout(set=3, binding=0, std430) writeonly buffer trailBuffer {
+    trail_t trails [];
+};
+
+#include "point.inc"
+
+layout(set=3, binding=1, std430) readonly buffer pointBuffer {
+    point_t points [];
+};
+
+layout( push_constant ) uniform constants{
+    float t;
+    float dt;
+};
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if (id >= particles.length()) {
+        return;
+    }
+
+    float lifetime = particles[id].lifetime;
+
+    if (lifetime > 0.0f) {
+        return;
+    }
+
+    uint event_id = events.length();
+    uint index = 0;
+
+    for (uint i = 0; i < events.length(); i++) {
+        const float start = events[i].startTime;
+
+        if ((events[i].continuous < 1) && (t < start)) {
+            continue;
+        }
+
+        index = atomicAdd(events[i].index, 1);
+
+        if (events[i].continuous < 1) {
+            if (events[i].count > index) {
+                event_id = i;
+                break;
+            } else {
+                atomicAdd(events[i].index, -1);
+            }
+        } else {
+            if (events[i].continuous > index){
+                event_id = i;
+                break;
+            } else {
+                if (events[i].contCount > 0) {
+                    atomicAdd(events[i].contCount, -1);
+                    events[i].index = 0;
+                }
+
+                atomicAdd(events[i].index, -1);
+            }
+        }
+    }
+
+    if (event_id >= events.length()) {
+        return;
+    }
+
+    lifetime = events[event_id].lifetime * (1.0f + 0.1f * randomData[(id + 1) % randomData.length()]);
+
+    vec3 direction;
+    if (dot(events[event_id].direction, events[event_id].direction) <= 0.0f) {
+        direction = vec3(
+            randomData[(id * 3 + 0) % randomData.length()],
+            randomData[(id * 3 + 1) % randomData.length()],
+            randomData[(id * 3 + 2) % randomData.length()]
+        );
+    } else {
+        direction = events[event_id].direction;
+    }
+
+    vec3 color = normalize(events[event_id].color);
+    const float v = events[event_id].velocity;
+
+    vec3 velocity = vec3(0.0f);
+    float size = events[event_id].size;
+
+    const uint pid = events[event_id].parent;
+
+    if (pid < events.length()) {
+        const uint spawnCount = events[pid].count;
+        const uint spawnId = startIndex[pid] + (id % spawnCount);
+
+        if (spawnId < particlesCopy.length()) {
+            particles[id].position = particlesCopy[spawnId].position;
+            velocity += particlesCopy[spawnId].velocity;
+            size = particlesCopy[spawnId].size;
+        }
+    }
+
+    if ((0 == index) && (events[event_id].continuous < 1)) {
+        const uint sid = atomicAdd(smokeIndex, 1) % smokes.length();
+
+        smokes[sid].position = particles[id].position;
+        smokes[sid].size = size * (1.0f + friction);
+        smokes[sid].velocity = velocity;
+        smokes[sid].scaling = v;
+        smokes[sid].color = mix(color, vec3(1.0f), 0.5f);
+        smokes[sid].eventID = event_id;
+    }
+
+    velocity += normalize(direction) * v * (1.0f + 0.1f * randomData[(id + 2) % randomData.length()]);;
+
+    const float split = pow(1.0f / events[event_id].count, 1.0f / 3.0f);
+
+    particles[id].lifetime = lifetime;
+    particles[id].velocity = velocity;
+    particles[id].size = size * split;
+    particles[id].color = color;
+    particles[id].mass = events[event_id].mass / events[event_id].count;
+    particles[id].eventId = event_id;
+
+    {
+        const uint tid = atomicAdd(trailIndex, 1) % trails.length();
+        const uint trailLen = 96 + int(randomData[(tid + id) % randomData.length()] * 32);
+
+        const uint startIndex = atomicAdd(pointIndex, trailLen) % points.length();
+
+        trails[tid].particleIndex = id;
+        trails[tid].startIndex = startIndex;
+        trails[tid].endIndex = (startIndex + trailLen - 1) % points.length();
+        trails[tid].useCount = 0;
+        trails[tid].color = mix(color, vec3(1.0f), 0.75f);
+        trails[tid].lifetime = lifetime + (dt * trailLen) * 0.5f;
+    }
+}
diff --git a/projects/fire_works/shaders/motion.comp b/projects/fire_works/shaders/motion.comp
new file mode 100644
index 0000000000000000000000000000000000000000..51d6fc5f60f2eff380376fead6cc189af68b52e6
--- /dev/null
+++ b/projects/fire_works/shaders/motion.comp
@@ -0,0 +1,49 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "physics.inc"
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) coherent buffer particleBuffer {
+    particle_t particles [];
+};
+
+layout( push_constant ) uniform constants{
+    float t;
+    float dt;
+};
+
+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;
+    }
+
+    const float fading = 1.0f / (1.0f + friction);
+
+    position = position + velocity * dt;
+
+    if (particles[id].mass > 0){
+        velocity = velocity * fading + vec3(0.0f, -g, 0.0f) * dt;
+    } else {
+        velocity = velocity * fading;
+    }
+
+    particles[id].position = position;
+    particles[id].lifetime = lifetime;
+    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..173c83ef5d7a79bfd8446c5586b7a2f487fe7546
--- /dev/null
+++ b/projects/fire_works/shaders/particle.frag
@@ -0,0 +1,21 @@
+#version 450
+
+layout(location = 0) in vec2 passPos;
+layout(location = 1) in flat vec3 passColor;
+layout(location = 2) in flat float passLifetime;
+
+layout(location = 0) out vec4 outColor;
+
+void main()	{
+    if (passLifetime <= 0.0f) {
+        discard;
+    }
+
+    const float value = length(passPos);
+
+    if (value < 0.5f) {
+        outColor = vec4(passColor, 1.0f - max(value * 2.0f, 0.0f));
+    } 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..488c9349d72ac04d48e523d5c9a97057bdfb1f78
--- /dev/null
+++ b/projects/fire_works/shaders/particle.inc
@@ -0,0 +1,15 @@
+#ifndef PARTICLE_INC
+#define PARTICLE_INC
+
+struct particle_t {
+	vec3 position;
+	float lifetime;
+	vec3 velocity;
+	float size;
+	vec3 color;
+	float mass;
+	vec3 pad0;
+	uint eventId;
+};
+
+#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..ff0e862ebcf0f66c3412e9ce04b610cea760151d
--- /dev/null
+++ b/projects/fire_works/shaders/particle.vert
@@ -0,0 +1,40 @@
+#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 flat vec3 passColor;
+layout(location = 2) out flat float passLifetime;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    uint width;
+    uint height;
+};
+
+void main()	{
+    vec3 position = particles[gl_InstanceIndex].position;
+    float lifetime = particles[gl_InstanceIndex].lifetime;
+    float size = particles[gl_InstanceIndex].size;
+    vec3 color = particles[gl_InstanceIndex].color;
+
+    if (width > height) {
+        passPos = vertexPos * vec2(1.0f * width / height, 1.0f);
+    } else {
+        passPos = vertexPos * vec2(1.0f, 1.0f * height / width);
+    }
+
+    passColor = color;
+    passLifetime = lifetime;
+
+    // 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/physics.inc b/projects/fire_works/shaders/physics.inc
new file mode 100644
index 0000000000000000000000000000000000000000..e14c62e33a6fd42ebfa8d31591394989de68a8d2
--- /dev/null
+++ b/projects/fire_works/shaders/physics.inc
@@ -0,0 +1,13 @@
+#ifndef PHYSICS_INC
+#define PHYSICS_INC
+
+const float pi = 3.14159f;
+
+const float g = 9.81f;
+const float friction = 0.004f;
+
+const float flowRate = 0.75f;
+const float mediumDensity = 0.0002f;
+const float trailWidth = 0.25f;
+
+#endif // PHYSICS_INC
\ No newline at end of file
diff --git a/projects/fire_works/shaders/point.inc b/projects/fire_works/shaders/point.inc
new file mode 100644
index 0000000000000000000000000000000000000000..54663c1e2eae4c641fbe5485d4c0ac41cb323df6
--- /dev/null
+++ b/projects/fire_works/shaders/point.inc
@@ -0,0 +1,11 @@
+#ifndef POINT_INC
+#define POINT_INC
+
+struct point_t {
+    vec3 position;
+    float size;
+	vec3 velocity;
+	float scaling;
+};
+
+#endif // POINT_INC
\ No newline at end of file
diff --git a/projects/fire_works/shaders/sample.comp b/projects/fire_works/shaders/sample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..1eef49df639cfbc9f35c53ace21b1ce50ab62459
--- /dev/null
+++ b/projects/fire_works/shaders/sample.comp
@@ -0,0 +1,39 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "voxel.inc"
+#include "smoke.inc"
+
+layout(set=0, binding=0, rgba16) restrict readonly uniform image3D voxelImage;
+layout(set=1, binding=0, rgba16f) restrict writeonly uniform image2D outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main() {
+    const ivec2 res = imageSize(outImage);
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, res))){
+        return;
+    }
+
+    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
+
+    const ivec3 voxelRes = imageSize(voxelImage);
+
+    vec4 voxel = vec4(0.0f);
+
+    for (int i = 0; i < voxelRes.z; i++) {
+        const ivec3 voxelPos = ivec3(uv, i);
+
+        vec4 data = imageLoad(voxelImage, voxelPos);
+
+        voxel = smokeBlend(voxel, data);
+    }
+
+    voxel.r = clamp(voxel.r, 0, 1);
+    voxel.g = clamp(voxel.g, 0, 1);
+    voxel.b = clamp(voxel.b, 0, 1);
+    voxel.a = clamp(voxel.a, 0, 1);
+
+    imageStore(outImage, uv, voxel);
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/scale.comp b/projects/fire_works/shaders/scale.comp
new file mode 100644
index 0000000000000000000000000000000000000000..44cfb8e48ad078f5ee8f63785b513a20f27ef542
--- /dev/null
+++ b/projects/fire_works/shaders/scale.comp
@@ -0,0 +1,40 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "physics.inc"
+#include "smoke.inc"
+
+layout(set=0, binding=0, std430) buffer smokeBuffer {
+    smoke_t smokes [];
+};
+
+layout( push_constant ) uniform constants{
+    float t;
+    float dt;
+};
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if (id >= smokes.length()) {
+        return;
+    }
+
+    vec3 position = smokes[id].position;
+    float size = smokes[id].size;
+    vec3 velocity = smokes[id].velocity;
+
+    const float scaling = smokes[id].scaling;
+    const float fading = 1.0f / (1.0f + friction);
+
+    position = position + velocity * dt;
+    velocity = velocity * fading + vec3(0.0f, 0.2f, 0.0f) * dt; //smoke is lighter than air right? + vec3(0.0f, -g, 0.0f) * dt;
+    size = size + scaling * dt;
+
+    smokes[id].position = position;
+    smokes[id].size = size;
+    smokes[id].velocity = velocity;
+}
diff --git a/projects/fire_works/shaders/smoke.frag b/projects/fire_works/shaders/smoke.frag
new file mode 100644
index 0000000000000000000000000000000000000000..8ded98ac5fbae554959a83df5bbf7451d286e832
--- /dev/null
+++ b/projects/fire_works/shaders/smoke.frag
@@ -0,0 +1,57 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "physics.inc"
+#include "smoke.inc"
+
+layout(location = 0) in vec3 passPos;
+layout(location = 1) in vec3 passDir;
+layout(location = 2) in vec3 passColor;
+layout(location = 3) in float passDensity;
+layout(location = 4) in flat int passSmokeIndex;
+
+layout(location = 0) out vec4 outColor;
+
+layout(set=1, binding=0, std430) readonly buffer randomBuffer {
+    float randomData [];
+};
+
+#define NUM_SMOKE_SAMPLES 16
+
+void main()	{
+    if (passDensity <= mediumDensity) {
+        discard;
+    }
+
+    vec3 start = passPos;
+    vec3 end = start + normalize(passDir) * 3.5f;
+
+    vec4 result = vec4(0);
+
+    for (uint i = 0; i < NUM_SMOKE_SAMPLES; i++) {
+        vec3 position = (
+            end + (start - end) * i / (NUM_SMOKE_SAMPLES - 1)
+        );
+
+        vec4 data = vec4(passColor, passDensity);
+
+        float fallOff = max(1.0f - length(position), 0.0f);
+
+        const uint randomIndex = (passSmokeIndex * NUM_SMOKE_SAMPLES + i) % randomData.length();
+        const float alpha = (1.0f + randomData[randomIndex] * 0.1f) * data.a * fallOff;
+
+        result = smokeBlend(result, vec4(data.rgb, alpha));
+    }
+
+    result.r = clamp(result.r, 0, 1);
+    result.g = clamp(result.g, 0, 1);
+    result.b = clamp(result.b, 0, 1);
+    result.a = clamp(result.a, 0, 1);
+
+    if (result.a < 1.0f) {
+        outColor = result;
+    } else {
+        discard;
+    }
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/smoke.inc b/projects/fire_works/shaders/smoke.inc
new file mode 100644
index 0000000000000000000000000000000000000000..8886a788273cd5fa5c871749e0c9012f2722ed41
--- /dev/null
+++ b/projects/fire_works/shaders/smoke.inc
@@ -0,0 +1,31 @@
+#ifndef SMOKE_INC
+#define SMOKE_INC
+
+struct smoke_t {
+	vec3 position;
+	float size;
+	vec3 velocity;
+	float scaling;
+	vec3 color;
+	uint eventID;
+};
+
+float smokeDensity(float size) {
+	if (size > 0.0f) {
+		return 0.025f / size;
+	} else {
+		return 0.0f;
+	}
+}
+
+vec4 smokeBlend(vec4 dst, vec4 src) {
+	const float f = max(1.0f - dst.a, 0.0f);
+	const float a = clamp(0.0f, 1.0f, src.a);
+
+	return vec4(
+		dst.rgb + src.rgb * a * f,
+		dst.a + a * f
+	);
+}
+
+#endif // SMOKE_INC
\ No newline at end of file
diff --git a/projects/fire_works/shaders/smoke.vert b/projects/fire_works/shaders/smoke.vert
new file mode 100644
index 0000000000000000000000000000000000000000..634117f79789797cd68e00cac3dbd59fb6515f79
--- /dev/null
+++ b/projects/fire_works/shaders/smoke.vert
@@ -0,0 +1,38 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+#include "smoke.inc"
+
+layout(set=0, binding=0, std430) readonly buffer smokeBuffer {
+    smoke_t smokes [];
+};
+
+layout(location = 0) in vec3 vertexPos;
+
+layout(location = 0) out vec3 passPos;
+layout(location = 1) out vec3 passDir;
+layout(location = 2) out vec3 passColor;
+layout(location = 3) out float passDensity;
+layout(location = 4) out flat int passSmokeIndex;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    vec3 camera;
+};
+
+void main()	{
+    vec3 position = smokes[gl_InstanceIndex].position;
+    float size = smokes[gl_InstanceIndex].size;
+    vec3 color = smokes[gl_InstanceIndex].color;
+
+    vec3 pos = position + vertexPos * size;
+
+    passPos = vertexPos;
+    passDir = pos - camera;
+    passColor = color;
+    passDensity = smokeDensity(size);
+    passSmokeIndex = gl_InstanceIndex;
+
+    // transform position into projected view space
+    gl_Position = mvp * vec4(pos, 1);
+}
\ 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/shaders/trail.comp b/projects/fire_works/shaders/trail.comp
new file mode 100644
index 0000000000000000000000000000000000000000..8e811a5251948e979fcacc0d2c1e95e27411fa87
--- /dev/null
+++ b/projects/fire_works/shaders/trail.comp
@@ -0,0 +1,98 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "physics.inc"
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) readonly buffer particleBuffer {
+    particle_t particles [];
+};
+
+#include "trail.inc"
+
+layout(set=1, binding=0, std430) coherent buffer trailBuffer {
+    trail_t trails [];
+};
+
+#include "point.inc"
+
+layout(set=1, binding=1, std430) buffer pointBuffer {
+    point_t points [];
+};
+
+layout( push_constant ) uniform constants{
+    float t;
+    float dt;
+};
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if (id >= trails.length()) {
+        return;
+    }
+
+    const uint particleIndex = trails[id].particleIndex;
+    const uint startIndex = trails[id].startIndex;
+    const uint endIndex = trails[id].endIndex;
+
+    uint useCount = trails[id].useCount;
+    float lifetime = trails[id].lifetime;
+
+    if (lifetime > dt) {
+        lifetime -= dt;
+    } else {
+        lifetime = 0.0f;
+    }
+
+    const uint available = (endIndex - startIndex) % points.length();
+
+    float trailLife = dt * available;
+    float fading = 1.0f / (1.0f + friction);
+
+    if (lifetime <= trailLife) {
+        fading *= (lifetime / trailLife);
+
+        if (useCount > 0) {
+            useCount--;
+        }
+    } else
+    if (available > useCount) {
+        useCount++;
+    }
+
+    for (uint i = useCount; i > 1; i--) {
+        const uint x = (startIndex + (i - 1)) % points.length();
+        const uint y = (startIndex + (i - 2)) % points.length();
+
+        vec3 position = points[y].position;
+        float size = points[y].size;
+        vec3 velocity = points[y].velocity;
+
+        const float scaling = points[y].scaling;
+
+        size = size * fading + scaling * dt;
+
+        points[x].position = position;
+        points[x].size = size;
+        points[x].velocity = velocity;
+        points[x].scaling = scaling;
+    }
+
+    vec3 position = particles[particleIndex].position;
+    float size = particles[particleIndex].size;
+    vec3 velocity = particles[particleIndex].velocity;
+
+    const float trailFactor = mediumDensity / friction;
+
+    points[startIndex].position = position * fading;
+    points[startIndex].size = trailWidth * size * fading;
+    points[startIndex].velocity = velocity * fading;
+    points[startIndex].scaling = trailFactor * length(velocity) * fading;
+
+    trails[id].useCount = useCount;
+    trails[id].lifetime = lifetime;
+}
diff --git a/projects/fire_works/shaders/trail.geom b/projects/fire_works/shaders/trail.geom
new file mode 100644
index 0000000000000000000000000000000000000000..027943473d05cddc9db6019c7ee36771fcb91d2e
--- /dev/null
+++ b/projects/fire_works/shaders/trail.geom
@@ -0,0 +1,96 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#define INSTANCE_LEN (16)
+
+layout(points) in;
+layout (triangle_strip, max_vertices = (INSTANCE_LEN * 2)) out;
+layout(invocations = 8) in;
+
+#include "physics.inc"
+#include "point.inc"
+
+layout(set=0, binding=1, std430) readonly buffer pointBuffer {
+    point_t points [];
+};
+
+layout(location = 0) in vec3 geomColor [1];
+layout(location = 1) in uint geomTrailIndex [1];
+layout(location = 2) in vec3 geomTrailColor [1];
+layout(location = 3) in uint geomStartIndex [1];
+layout(location = 4) in uint geomUseCount [1];
+
+layout(location = 0) out vec3 passPos;
+layout(location = 1) out vec3 passDir;
+layout(location = 2) out vec3 passColor;
+layout(location = 3) out float passDensity;
+layout(location = 4) out flat int passSmokeIndex;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    vec3 camera;
+};
+
+void main() {
+    const vec3 color = geomColor[0];
+    const uint id = geomTrailIndex[0];
+
+    const vec3 trailColor = geomTrailColor[0];
+
+    const uint startIndex = geomStartIndex[0];
+    const uint useCount = geomUseCount[0];
+
+    const uint indexOffset = (gl_InvocationID * (INSTANCE_LEN - 1));
+    const uint instanceIndex = startIndex + indexOffset;
+
+    uint count = min(INSTANCE_LEN, useCount);
+
+    if ((indexOffset >= useCount) && (indexOffset + INSTANCE_LEN > useCount)) {
+        count = indexOffset - useCount;
+    }
+
+    if (count <= 1) {
+        return;
+    }
+
+    const float trailFactor = mediumDensity / friction;
+
+    for (uint i = 0; i < count; i++) {
+        const float u = float(indexOffset + i + 1) / float(useCount);
+
+        const uint index = (instanceIndex + i) % points.length();
+
+        const vec3 position = points[index].position;
+        const float size = points[index].size;
+        const vec3 velocity = points[index].velocity;
+
+        const vec3 dir = normalize(cross(abs(velocity), position - camera));
+
+        vec3 offset = dir * size;
+        float density = trailFactor * (1.0f - u * u) / size;
+
+        const vec3 p0 = position - offset;
+        const vec3 p1 = position + offset;
+
+        passPos = vec3(u, -1.0f, -1.0f);
+        passDir = vec3(-0.1f * u, +0.2f, 2.0f);
+        passColor = mix(color, trailColor, u);
+        passDensity = density;
+        passSmokeIndex = int(id);
+
+        gl_Position = mvp * vec4(p0, 1);
+        EmitVertex();
+
+        passPos = vec3(u, +1.0f, -1.0f);
+        passDir = vec3(-0.1f * u, -0.2f, 2.0f);
+        passColor = mix(color, trailColor, u);
+        passDensity = density;
+        passSmokeIndex = int(id);
+
+        gl_Position = mvp * vec4(p1, 1);
+        EmitVertex();
+    }
+
+    EndPrimitive();
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/trail.inc b/projects/fire_works/shaders/trail.inc
new file mode 100644
index 0000000000000000000000000000000000000000..75cc49ad4038f28f27890e088fe4dd4e511d1f6b
--- /dev/null
+++ b/projects/fire_works/shaders/trail.inc
@@ -0,0 +1,13 @@
+#ifndef TRAIL_INC
+#define TRAIL_INC
+
+struct trail_t {
+    uint particleIndex;
+    uint startIndex;
+    uint endIndex;
+	uint useCount;
+	vec3 color;
+    float lifetime;
+};
+
+#endif // TRAIL_INC
\ No newline at end of file
diff --git a/projects/fire_works/shaders/trail.vert b/projects/fire_works/shaders/trail.vert
new file mode 100644
index 0000000000000000000000000000000000000000..871beb9544c33ec790d079a050b4780efbec363f
--- /dev/null
+++ b/projects/fire_works/shaders/trail.vert
@@ -0,0 +1,40 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+#include "trail.inc"
+
+layout(set=0, binding=0, std430) readonly buffer trailBuffer {
+    trail_t trails [];
+};
+
+#include "particle.inc"
+
+layout(set=2, binding=0, std430) readonly buffer particleBuffer {
+    particle_t particles [];
+};
+
+layout(location = 0) out vec3 geomColor;
+layout(location = 1) out uint geomTrailIndex;
+layout(location = 2) out vec3 geomTrailColor;
+layout(location = 3) out uint geomStartIndex;
+layout(location = 4) out uint geomUseCount;
+
+void main()	{
+    const uint particleIndex = trails[gl_InstanceIndex].particleIndex;
+    const float lifetime = trails[gl_InstanceIndex].lifetime;
+
+    geomColor = particles[particleIndex].color;
+    geomTrailIndex = gl_InstanceIndex;
+    geomTrailColor = trails[gl_InstanceIndex].color;
+    geomStartIndex = trails[gl_InstanceIndex].startIndex;
+
+    const uint useCount = trails[gl_InstanceIndex].useCount;
+
+    if (lifetime > 0.0f) {
+        geomUseCount = useCount;
+    } else {
+        geomUseCount = 0;
+    }
+
+    gl_Position = vec4(gl_InstanceIndex, lifetime, useCount, 0.0f);
+}
\ No newline at end of file
diff --git a/projects/fire_works/shaders/voxel.comp b/projects/fire_works/shaders/voxel.comp
new file mode 100644
index 0000000000000000000000000000000000000000..459c06f381c5c7c9cc26074cd22ecf2869e30350
--- /dev/null
+++ b/projects/fire_works/shaders/voxel.comp
@@ -0,0 +1,36 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
+
+#include "physics.inc"
+#include "voxel.inc"
+
+layout(set=0, binding=0, r32ui) restrict readonly uniform uimage3D voxelRed;
+layout(set=0, binding=1, r32ui) restrict readonly uniform uimage3D voxelGreen;
+layout(set=0, binding=2, r32ui) restrict readonly uniform uimage3D voxelBlue;
+layout(set=0, binding=3, r32ui) restrict readonly uniform uimage3D voxelDensity;
+
+layout(set=1, binding=0, rgba16) restrict writeonly uniform image3D voxelImage;
+
+void main() {
+    ivec3 pos = ivec3(gl_GlobalInvocationID);
+    ivec3 size = imageSize(voxelImage);
+
+    if (any(greaterThanEqual(pos, size))) {
+        return;
+    }
+
+    const float red = voxel_read(voxelRed, pos);
+    const float green = voxel_read(voxelGreen, pos);
+    const float blue = voxel_read(voxelBlue, pos);
+    const float density = voxel_read(voxelDensity, pos);
+
+    imageStore(voxelImage, pos, vec4(
+        red,
+        green,
+        blue,
+        density
+    ));
+}
diff --git a/projects/fire_works/shaders/voxel.inc b/projects/fire_works/shaders/voxel.inc
new file mode 100644
index 0000000000000000000000000000000000000000..9e2c895e4a0c688262de7404039aefe6650191bd
--- /dev/null
+++ b/projects/fire_works/shaders/voxel.inc
@@ -0,0 +1,18 @@
+#ifndef VOXEL_INC
+#define VOXEL_INC
+
+#define VOXEL_NORM_VALUE 0xFF
+
+#define voxel_add(img, pos, value) imageAtomicAdd(img, ivec3((imageSize(img) - ivec3(1)) * pos), uint(VOXEL_NORM_VALUE * value))
+
+#define voxel_write(img, pos, value) imageStore(img, pos, uvec4(VOXEL_NORM_VALUE * value));
+#define voxel_read(img, pos) imageLoad(img, pos).r / float(VOXEL_NORM_VALUE);
+
+// https://stackoverflow.com/questions/51108596/linearize-depth
+float linearize_depth(float d,float zNear,float zFar) {
+    return zNear * zFar / (zFar + d * (zNear - zFar));
+}
+
+#define voxel_pos(pos) vec3((pos.xy + vec2(1.0f)) * 0.5f, linearize_depth(pos.y, 0.1f, 50.0f))
+
+#endif // VOXEL_INC
\ No newline at end of file
diff --git a/projects/fire_works/shaders/voxel_particle.comp b/projects/fire_works/shaders/voxel_particle.comp
new file mode 100644
index 0000000000000000000000000000000000000000..12b68eed09746f38286a5b1f1b4e9e56d4bfe105
--- /dev/null
+++ b/projects/fire_works/shaders/voxel_particle.comp
@@ -0,0 +1,59 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "physics.inc"
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) readonly buffer particleBuffer {
+    particle_t particles [];
+};
+
+#include "voxel.inc"
+
+layout(set=1, binding=0, r32ui) uniform uimage3D voxelRed;
+layout(set=1, binding=1, r32ui) uniform uimage3D voxelGreen;
+layout(set=1, binding=2, r32ui) uniform uimage3D voxelBlue;
+layout(set=1, binding=3, r32ui) uniform uimage3D voxelDensity;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if (id >= particles.length()) {
+        return;
+    }
+
+    vec3 position = particles[id].position;
+    float lifetime = particles[id].lifetime;
+
+    if (lifetime <= 0.0f) {
+        return;
+    }
+
+    vec4 cs_pos = mvp * vec4(position, 1);
+
+    if (abs(cs_pos.w) <= 0.0f) {
+        return;
+    }
+
+    vec3 ndc_pos = cs_pos.xyz / cs_pos.w;
+    vec3 pos = voxel_pos(ndc_pos);
+
+    if ((any(greaterThanEqual(pos, vec3(1.5f)))) || (any(lessThanEqual(pos, vec3(-0.5f))))) {
+        return;
+    }
+
+    float size = particles[id].size;
+    vec3 color = particles[id].color;
+
+    voxel_add(voxelRed, pos, color.r);
+    voxel_add(voxelGreen, pos, color.g);
+    voxel_add(voxelBlue, pos, color.b);
+    voxel_add(voxelDensity, pos, 1.0f);
+}
diff --git a/projects/fire_works/shaders/voxel_smoke.comp b/projects/fire_works/shaders/voxel_smoke.comp
new file mode 100644
index 0000000000000000000000000000000000000000..de216ab29a6671e1547600d072edaf510b775154
--- /dev/null
+++ b/projects/fire_works/shaders/voxel_smoke.comp
@@ -0,0 +1,72 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "physics.inc"
+#include "smoke.inc"
+
+layout(set=0, binding=0, std430) readonly buffer smokeBuffer {
+    smoke_t smokes [];
+};
+
+#include "voxel.inc"
+
+layout(set=1, binding=0, r32ui) uniform uimage3D voxelRed;
+layout(set=1, binding=1, r32ui) uniform uimage3D voxelGreen;
+layout(set=1, binding=2, r32ui) uniform uimage3D voxelBlue;
+layout(set=1, binding=3, r32ui) uniform uimage3D voxelDensity;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+#define NUM_SMOKE_SAMPLES 4
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if (id >= smokes.length()) {
+        return;
+    }
+
+    vec3 position = smokes[id].position;
+    float size = smokes[id].size;
+
+    const float density = smokeDensity(size);
+
+    if (density <= mediumDensity) {
+        return;
+    }
+
+    vec3 offset = vec3(-size);
+
+    for (;offset.x <= size; offset.x += size / NUM_SMOKE_SAMPLES) {
+        for (;offset.y <= size; offset.y += size / NUM_SMOKE_SAMPLES) {
+            for (;offset.z <= size; offset.z += size / NUM_SMOKE_SAMPLES) {
+                vec4 cs_pos = mvp * vec4(position + offset, 1);
+
+                if (abs(cs_pos.w) <= 0.0f) {
+                    return;
+                }
+
+                vec3 ndc_pos = cs_pos.xyz / cs_pos.w;
+                vec3 pos = voxel_pos(ndc_pos);
+
+                if ((any(greaterThanEqual(pos, vec3(1.5f)))) || (any(lessThanEqual(pos, vec3(-0.5f))))) {
+                    return;
+                }
+
+                vec3 color = smokes[id].color;
+
+                float local_density = density * max(1.0f - length(offset / size), 0.0f);
+
+                voxel_add(voxelRed, pos, color.r);
+                voxel_add(voxelGreen, pos, color.g);
+                voxel_add(voxelBlue, pos, color.b);
+                voxel_add(voxelDensity, pos, local_density);
+            }
+        }
+    }
+}
diff --git a/projects/fire_works/shaders/voxel_trail.comp b/projects/fire_works/shaders/voxel_trail.comp
new file mode 100644
index 0000000000000000000000000000000000000000..56971a1719075e0dd9a6a5bb6b52e89c11f8489c
--- /dev/null
+++ b/projects/fire_works/shaders/voxel_trail.comp
@@ -0,0 +1,87 @@
+#version 450 core
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+#include "physics.inc"
+
+#include "trail.inc"
+
+layout(set=0, binding=0, std430) coherent buffer trailBuffer {
+    trail_t trails [];
+};
+
+#include "point.inc"
+
+layout(set=0, binding=1, std430) buffer pointBuffer {
+    point_t points [];
+};
+
+#include "voxel.inc"
+
+layout(set=1, binding=0, r32ui) uniform uimage3D voxelRed;
+layout(set=1, binding=1, r32ui) uniform uimage3D voxelGreen;
+layout(set=1, binding=2, r32ui) uniform uimage3D voxelBlue;
+layout(set=1, binding=3, r32ui) uniform uimage3D voxelDensity;
+
+#include "smoke.inc"
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if (id >= trails.length()) {
+        return;
+    }
+
+    const uint particleIndex = trails[id].particleIndex;
+    const uint startIndex = trails[id].startIndex;
+
+    uint useCount = trails[id].useCount;
+
+    if (useCount <= 0) {
+        return;
+    }
+
+    vec3 color = trails[id].color;
+    float lifetime = trails[id].lifetime;
+
+    if (lifetime <= 0.0f) {
+        return;
+    }
+
+    for (uint i = 0; i < useCount; i++) {
+        const uint x = (startIndex + i) % points.length();
+
+        vec3 position = points[x].position;
+        float size = points[x].size;
+
+        const float density = smokeDensity(size);
+
+        if (density <= mediumDensity) {
+            break;
+        }
+
+        vec4 cs_pos = mvp * vec4(position, 1);
+
+        if (abs(cs_pos.w) <= 0.0f) {
+            return;
+        }
+
+        vec3 ndc_pos = cs_pos.xyz / cs_pos.w;
+        vec3 pos = voxel_pos(ndc_pos);
+
+        if ((any(greaterThanEqual(pos, vec3(1.5f)))) || (any(lessThanEqual(pos, vec3(-0.5f))))) {
+            continue;
+        }
+
+        voxel_add(voxelRed, pos, color.r);
+        voxel_add(voxelGreen, pos, color.g);
+        voxel_add(voxelBlue, pos, color.b);
+        voxel_add(voxelDensity, pos, density);
+    }
+}
diff --git a/projects/fire_works/src/main.cpp b/projects/fire_works/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d430216f864ac3e7c5e4d4fa1f6339710b64cb93
--- /dev/null
+++ b/projects/fire_works/src/main.cpp
@@ -0,0 +1,1389 @@
+
+#include <array>
+
+#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 mass;
+	glm::vec3 pad0;
+	uint32_t eventId;
+};
+
+struct event_t {
+	glm::vec3 direction;
+	float startTime;
+	glm::vec3 color;
+	float velocity;
+	
+	uint32_t count;
+	uint32_t index;
+	uint32_t parent;
+	uint32_t continuous;
+	
+	float lifetime;
+	float mass;
+	float size;
+	uint32_t contCount;
+};
+
+struct smoke_t {
+	glm::vec3 position;
+	float size;
+	glm::vec3 velocity;
+	float scaling;
+	glm::vec3 color;
+	float eventID;
+};
+
+struct trail_t {
+	uint32_t particleIndex;
+	uint32_t startIndex;
+	uint32_t endIndex;
+	uint32_t useCount;
+	glm::vec3 color;
+	float lifetime;
+};
+
+struct point_t {
+	glm::vec3 position;
+	float size;
+	glm::vec3 velocity;
+	float scaling;
+};
+
+struct draw_particles_t {
+	glm::mat4 mvp;
+	uint32_t width;
+	uint32_t height;
+};
+
+struct draw_smoke_t {
+	glm::mat4 mvp;
+	glm::vec3 camera;
+};
+
+#define PARTICLE_COUNT (1024)
+#define SMOKE_COUNT (512)
+#define TRAIL_COUNT (2048)
+#define RANDOM_DATA_LENGTH (4096)
+#define POINT_COUNT (2048 * 256)
+
+void InitializeParticles(std::vector<particle_t> &particles) {
+	for (size_t i = 0; i < particles.size(); 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 [i] = particle;
+	}
+}
+
+void InitializeFireworkEvents(std::vector<event_t>& events) {
+	events.emplace_back(glm::vec3(0, 1, 0), 0.5f, glm::vec3(0.0f, 1.0f, 0.0f), 12.5f,
+
+						1, 0, UINT_MAX, 0,
+
+						1.0f, 1.0f, 0.5f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 1.5f, glm::vec3(0.0f, 1.0f, 1.0f), 10.0f,
+
+						100, 0, events.size() - 1, 0,
+
+						10.0f, 1.0f, 0.0f, 0);
+
+	events.emplace_back(glm::vec3(0.5, 1, 0), 0.25f, glm::vec3(0.0f, 1.5f, 0.0f), 15.0f,
+
+						1, 0, UINT_MAX, 0,
+
+						0.5f, 1.0f, 0.5f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 0.75f, glm::vec3(0.0f, 1.5f, 1.0f), 8.0f,
+
+						150, 0, events.size() - 1, 0,
+
+						10.0f, 1.0f, 0.0f, 0);
+
+	events.emplace_back(glm::vec3(-2.5, 3, 0.5), 1.0f, glm::vec3(246.0f, 189.0f, 255.0f), 12.5f,
+
+						1, 0, UINT_MAX, 0,
+
+						1.0f, 1.0f, 0.5f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 2.0f, glm::vec3(235.0f, 137.0f, 250.0f), 8.0f,
+
+						75, 0, events.size() - 1, 0,
+
+						10.0f, 1.0f, 0.0f, 0);
+}
+
+void InitializeSparklerEvents(std::vector<event_t> &events) {
+	events.emplace_back(glm::vec3(0, 1, 0), 0.0f, glm::vec3(251.0f, 255.0f, 145.0f), 1.0f,
+
+						1, 0, UINT_MAX, 0,
+
+						8.0f, 0.0f, 0.5f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 0.0f, glm::vec3(251.0f, 255.0f, 145.0f), 10.0f,
+
+						1000, 1, events.size() - 1, 10,
+
+						0.5f, -1.0f, 0.0f, 100);
+}
+
+void InitializeNestedFireworkEvents(std::vector<event_t>& events) {
+	events.emplace_back(glm::vec3(0, 2, 0), 0.0f, glm::vec3(0.0f, 1.0f, 0.0f), 12.5f,
+
+						1, 0, UINT_MAX, 0,
+
+						1.0f, 1.0f, 0.5f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 0.9f, glm::vec3(0.0f, 1.0f, 1.0f), 7.0f,
+
+						100, 0, events.size() - 1, 0,
+
+						10.1f, 1.0f, 0.0f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 2.0f, glm::vec3(0.0f, 0.0f, 0.0f), 10.0f,
+
+						100, 0, events.size() - 1, 0,
+
+						10.0f, 1.0f, 0.0f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 1.0f, glm::vec3(42.0f,0.0f, 1.0f), 12.5f,
+
+						100, 0, events.size() - 2, 0,
+
+						1.0f, 1.0f, 0.5f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 1.5f, glm::vec3(42.0f, 0.0f, 1.0f), 10.0f,
+
+						100, 0, events.size() - 1, 0,
+
+						10.0f, 1.0f, 0.0f, 0);
+
+	events.emplace_back(glm::vec3(0.0f), 2.0f, glm::vec3(42.0f, 0.0f, 1.0f), 10.0f,
+
+						100, 0, events.size() - 1, 0,
+
+						10.0f, 1.0f, 0.0f, 0);
+}
+
+void ChangeColor(std::vector<event_t>& events, glm::vec3 color) {
+	for (int i = 0; i < events.size(); i++) {
+		events [i].color = color;
+	}
+}
+
+int main(int argc, const char **argv) {
+	vkcv::Features features;
+	
+	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+	
+	vkcv::Core core = vkcv::Core::create(
+		"Firework",
+		VK_MAKE_VERSION(0, 0, 1),
+		{vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
+		features
+	);
+	
+	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
+	uint32_t pilotIdx = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	
+	cameraManager.getCamera(trackballIdx).setNearFar(0.1f, 50.0f);
+	cameraManager.getCamera(trackballIdx).setPosition(glm::vec3(0, 0, -25));
+	
+	cameraManager.getCamera(pilotIdx).setNearFar(0.1f, 50.0f);
+	cameraManager.getCamera(pilotIdx).setPosition(glm::vec3(0, 0, 25));
+	
+	cameraManager.setActiveCamera(pilotIdx);
+	
+	vkcv::gui::GUI gui (core, windowHandle);
+	vkcv::shader::GLSLCompiler compiler;
+	
+	vkcv::DescriptorBindings descriptorBindings0;
+	vkcv::DescriptorBinding binding0 {
+		0,
+		vkcv::DescriptorType::STORAGE_BUFFER,
+		1,
+		vkcv::ShaderStage::VERTEX | vkcv::ShaderStage::COMPUTE,
+		false,
+		false
+	};
+	vkcv::DescriptorBinding binding1 { 
+		1,     
+		vkcv::DescriptorType::STORAGE_BUFFER,
+									   
+		1,     
+		vkcv::ShaderStage::COMPUTE,
+		false, 
+		false 
+	};
+	
+	descriptorBindings0.insert(std::make_pair(0, binding0));
+	descriptorBindings0.insert(std::make_pair(1, binding1));
+	
+	vkcv::DescriptorSetLayoutHandle descriptorSetLayout = core.createDescriptorSetLayout(descriptorBindings0);
+	vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(descriptorSetLayout);
+	
+	vkcv::ShaderProgram generationShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/generation.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		generationShader.addShader(shaderStage, path);
+	});
+	
+	auto generationBindings = generationShader.getReflectedDescriptors().at(1);
+	generationBindings[0].shaderStages |= vkcv::ShaderStage::FRAGMENT;
+	
+	vkcv::DescriptorSetLayoutHandle generationDescriptorLayout = core.createDescriptorSetLayout(
+		generationBindings
+	);
+	
+	vkcv::DescriptorSetHandle generationDescriptorSet = core.createDescriptorSet(generationDescriptorLayout);
+	
+	vkcv::DescriptorBindings descriptorBindings1;
+	
+	descriptorBindings1.insert(std::make_pair(0, binding0));
+	descriptorBindings1.insert(std::make_pair(1, binding1));
+	
+	vkcv::DescriptorSetLayoutHandle smokeDescriptorLayout = core.createDescriptorSetLayout(descriptorBindings1);
+	vkcv::DescriptorSetHandle smokeDescriptorSet = core.createDescriptorSet(smokeDescriptorLayout);
+	
+	vkcv::DescriptorBindings descriptorBindings2;
+	vkcv::DescriptorBinding binding2 {
+		1,
+		vkcv::DescriptorType::STORAGE_BUFFER,
+		1,
+		vkcv::ShaderStage::GEOMETRY | vkcv::ShaderStage::COMPUTE,
+		false,
+		false
+	};
+	
+	descriptorBindings2.insert(std::make_pair(0, binding0));
+	descriptorBindings2.insert(std::make_pair(1, binding2));
+	
+	vkcv::DescriptorSetLayoutHandle trailDescriptorLayout = core.createDescriptorSetLayout(
+		descriptorBindings2
+	);
+	
+	vkcv::DescriptorSetHandle trailDescriptorSet = core.createDescriptorSet(trailDescriptorLayout);
+	
+	vkcv::ComputePipelineHandle generationPipeline = core.createComputePipeline({
+		generationShader,
+		{
+          descriptorSetLayout,
+          generationDescriptorLayout,
+          smokeDescriptorLayout,
+          trailDescriptorLayout
+		}
+	});
+	
+	vkcv::ShaderProgram trailShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/trail.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		trailShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::ComputePipelineHandle trailComputePipeline = core.createComputePipeline({
+		trailShader,
+		{ descriptorSetLayout, trailDescriptorLayout }
+	});
+	
+	vkcv::ShaderProgram scaleShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/scale.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		scaleShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::ComputePipelineHandle scalePipeline = core.createComputePipeline({
+		scaleShader,
+		{ smokeDescriptorLayout }
+	});
+	
+	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
+	
+	const vk::Format colorFormat = vk::Format::eR16G16B16A16Sfloat;
+	
+	std::array<vkcv::ImageHandle, 4> colorBuffers;
+	for (size_t i = 0; i < colorBuffers.size(); i++) {
+		colorBuffers[i] = core.createImage(
+				colorFormat,
+				swapchainExtent.width,
+				swapchainExtent.height,
+				1, false, true, true
+		).getHandle();
+	}
+	
+	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::ShaderProgram trailShaderProgram;
+	compiler.compile(vkcv::ShaderStage::VERTEX, "shaders/trail.vert", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		trailShaderProgram.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::GEOMETRY, "shaders/trail.geom", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		trailShaderProgram.addShader(shaderStage, path);
+	});
+	
+	vkcv::ShaderProgram smokeShaderProgram;
+	compiler.compile(vkcv::ShaderStage::VERTEX, "shaders/smoke.vert", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		smokeShaderProgram.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, "shaders/smoke.frag", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		smokeShaderProgram.addShader(shaderStage, path);
+		trailShaderProgram.addShader(shaderStage, path);
+	});
+	
+	std::vector<particle_t> particles;
+	particles.resize(PARTICLE_COUNT);
+	InitializeParticles(particles);
+	
+	vkcv::Buffer<particle_t> particleBuffer = core.createBuffer<particle_t>(
+		vkcv::BufferType::STORAGE,
+		particles.size(),
+		vkcv::BufferMemoryType::DEVICE_LOCAL,
+		false,
+		true
+	);
+	
+	particleBuffer.fill(particles);
+	
+	vkcv::Buffer<particle_t> particleBufferCopy =
+		core.createBuffer<particle_t>(vkcv::BufferType::STORAGE, particles.size());
+
+	particleBufferCopy.fill(particles);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageBuffer(0, particleBuffer.getHandle());
+		writes.writeStorageBuffer(1, particleBufferCopy.getHandle());
+		core.writeDescriptorSet(descriptorSet, writes);
+	}
+	
+	std::vector<float> randomData;
+	randomData.reserve(RANDOM_DATA_LENGTH);
+	
+	for (size_t i = 0; i < RANDOM_DATA_LENGTH; i++) {
+		randomData.push_back(
+			2.0f * static_cast<float>(std::rand() % RAND_MAX) / static_cast<float>(RAND_MAX) - 1.0f
+		);
+	}
+	
+	vkcv::Buffer<float> randomBuffer = core.createBuffer<float>(
+		vkcv::BufferType::STORAGE,
+		randomData.size()
+	);
+	
+	randomBuffer.fill(randomData);
+	
+	std::vector<event_t> events;
+	InitializeFireworkEvents(events);
+	
+	vkcv::Buffer<event_t> eventBuffer = core.createBuffer<event_t>(
+		vkcv::BufferType::STORAGE,
+		events.size()
+	);
+	
+	eventBuffer.fill(events);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageBuffer(0, randomBuffer.getHandle());
+		writes.writeStorageBuffer(1, eventBuffer.getHandle());
+		core.writeDescriptorSet(generationDescriptorSet, writes);
+	}
+
+	vkcv::Buffer<uint32_t> startIndexBuffer =
+		core.createBuffer<uint32_t>(vkcv::BufferType::STORAGE, eventBuffer.getCount());
+
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageBuffer(2, startIndexBuffer.getHandle());
+		core.writeDescriptorSet(generationDescriptorSet, writes);
+	}
+
+	std::vector<smoke_t> smokes;
+	smokes.reserve(SMOKE_COUNT);
+	
+	for (size_t i = 0; i < SMOKE_COUNT; i++) {
+		smoke_t smoke;
+		smoke.position = glm::vec3(0.0f);
+		smoke.size = 0.0f;
+		
+		smoke.velocity = glm::vec3(0.0f);
+		smoke.scaling = 0.0f;
+		
+		smoke.color = glm::vec3(0.0f);
+		
+		smokes.push_back(smoke);
+	}
+	
+	vkcv::Buffer<smoke_t> smokeBuffer = core.createBuffer<smoke_t>(
+		vkcv::BufferType::STORAGE,
+		smokes.size()
+	);
+	
+	smokeBuffer.fill(smokes);
+	
+	vkcv::Buffer<uint32_t> smokeIndexBuffer = core.createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE, 3, vkcv::BufferMemoryType::HOST_VISIBLE
+	);
+	
+	uint32_t* smokeIndices = smokeIndexBuffer.map();
+	memset(smokeIndices, 0, smokeIndexBuffer.getSize());
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageBuffer(0, smokeBuffer.getHandle());
+		writes.writeStorageBuffer(1, smokeIndexBuffer.getHandle());
+		core.writeDescriptorSet(smokeDescriptorSet, writes);
+	}
+	
+	std::vector<trail_t> trails;
+	trails.reserve(TRAIL_COUNT);
+	
+	for (size_t i = 0; i < TRAIL_COUNT; i++) {
+		trail_t trail;
+		
+		trail.particleIndex = 0;
+		trail.startIndex = 0;
+		trail.endIndex = 0;
+		trail.useCount = 0;
+		trail.color = glm::vec3(0.0f);
+		trail.lifetime = 0.0f;
+		
+		trails.push_back(trail);
+	}
+	
+	vkcv::Buffer<trail_t> trailBuffer = core.createBuffer<trail_t>(
+		vkcv::BufferType::STORAGE,
+		trails.size()
+	);
+	
+	trailBuffer.fill(trails);
+	
+	std::vector<point_t> points;
+	points.reserve(POINT_COUNT);
+	
+	for (size_t i = 0; i < POINT_COUNT; i++) {
+		point_t point;
+		
+		point.position = glm::vec3(0.0f);
+		point.size = 0.0f;
+		point.velocity = glm::vec3(0.0f);
+		point.scaling = 0.0f;
+		
+		points.push_back(point);
+	}
+	
+	vkcv::Buffer<point_t> pointBuffer = core.createBuffer<point_t>(
+		vkcv::BufferType::STORAGE,
+		points.size()
+	);
+	
+	pointBuffer.fill(points);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageBuffer(0, trailBuffer.getHandle());
+		writes.writeStorageBuffer(1, pointBuffer.getHandle());
+		core.writeDescriptorSet(trailDescriptorSet, writes);
+	}
+	
+	vkcv::Buffer<glm::vec3> cubePositions = core.createBuffer<glm::vec3>(vkcv::BufferType::VERTEX, 8);
+	cubePositions.fill({
+		glm::vec3(-1.0f, -1.0f, -1.0f),
+		glm::vec3(+1.0f, -1.0f, -1.0f),
+		glm::vec3(-1.0f, +1.0f, -1.0f),
+		glm::vec3(+1.0f, +1.0f, -1.0f),
+		glm::vec3(-1.0f, -1.0f, +1.0f),
+		glm::vec3(+1.0f, -1.0f, +1.0f),
+		glm::vec3(-1.0f, +1.0f, +1.0f),
+		glm::vec3(+1.0f, +1.0f, +1.0f)
+	});
+	
+	vkcv::Buffer<uint16_t> cubeIndices = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 36);
+	cubeIndices.fill({
+		0, 2, 3,
+		0, 3, 1,
+		1, 3, 7,
+		1, 7, 5,
+		
+		5, 7, 6,
+		5, 6, 4,
+		4, 6, 2,
+		4, 2, 0,
+		
+		2, 6, 7,
+		2, 7, 3,
+		1, 5, 4,
+		1, 4, 0
+	});
+	
+	vkcv::Mesh cubeMesh (
+		{ vkcv::VertexBufferBinding(0, cubePositions.getVulkanHandle()) },
+		cubeIndices.getVulkanHandle(),
+		cubeIndices.getCount()
+	);
+	
+	const std::vector<vkcv::VertexAttachment> vaSmoke = smokeShaderProgram.getVertexAttachments();
+	
+	std::vector<vkcv::VertexBinding> vbSmoke;
+	for (size_t i = 0; i < vaSmoke.size(); i++) {
+		vbSmoke.push_back(vkcv::createVertexBinding(i, { vaSmoke[i] }));
+	}
+	
+	const vkcv::VertexLayout smokeLayout { vbSmoke };
+	
+	vkcv::PassHandle renderPass = core.createPass(vkcv::PassConfig(
+		{
+			vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				colorFormat
+				)
+		},
+		vkcv::Multisampling::None
+	));
+	
+	vkcv::GraphicsPipelineConfig smokePipelineDefinition{
+		smokeShaderProgram,
+		UINT32_MAX,
+		UINT32_MAX,
+		renderPass,
+		{smokeLayout},
+		{smokeDescriptorLayout, generationDescriptorLayout},
+		true
+	};
+	
+	smokePipelineDefinition.m_blendMode = vkcv::BlendMode::Additive;
+	
+	vkcv::GraphicsPipelineHandle smokePipeline = core.createGraphicsPipeline(smokePipelineDefinition);
+	
+	const std::vector<vkcv::VertexAttachment> vaTrail = trailShaderProgram.getVertexAttachments();
+	
+	std::vector<vkcv::VertexBinding> vbTrail;
+	for (size_t i = 0; i < vaTrail.size(); i++) {
+		vbTrail.push_back(vkcv::createVertexBinding(i, { vaTrail[i] }));
+	}
+	
+	const vkcv::VertexLayout trailLayout { vbTrail };
+	
+	vkcv::GraphicsPipelineConfig trailPipelineDefinition{
+		trailShaderProgram,
+		UINT32_MAX,
+		UINT32_MAX,
+		renderPass,
+		{trailLayout},
+		{trailDescriptorLayout, generationDescriptorLayout, descriptorSetLayout},
+		true
+	};
+	
+	trailPipelineDefinition.m_PrimitiveTopology = vkcv::PrimitiveTopology::PointList;
+	trailPipelineDefinition.m_blendMode = vkcv::BlendMode::Additive;
+	
+	vkcv::GraphicsPipelineHandle trailPipeline = core.createGraphicsPipeline(trailPipelineDefinition);
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsSmokes;
+	
+	drawcallsSmokes.push_back(vkcv::DrawcallInfo(
+		cubeMesh,
+		{
+			vkcv::DescriptorSetUsage(0, smokeDescriptorSet),
+			vkcv::DescriptorSetUsage(1, generationDescriptorSet),
+		},
+		smokeBuffer.getCount()
+	));
+	
+	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()
+	);
+	
+	vkcv::Mesh trailMesh (
+		{},
+		triangleIndices.getVulkanHandle(),
+		1
+	);
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsTrails;
+	
+	drawcallsTrails.push_back(vkcv::DrawcallInfo(
+		trailMesh,
+		{
+			vkcv::DescriptorSetUsage(0, trailDescriptorSet),
+			vkcv::DescriptorSetUsage(1, generationDescriptorSet),
+			vkcv::DescriptorSetUsage(2, descriptorSet)
+		},
+		trailBuffer.getCount()
+	));
+	
+	const std::vector<vkcv::VertexAttachment> vaParticles = particleShaderProgram.getVertexAttachments();
+	
+	std::vector<vkcv::VertexBinding> vbParticles;
+	for (size_t i = 0; i < vaParticles.size(); i++) {
+		vbParticles.push_back(vkcv::createVertexBinding(i, { vaParticles[i] }));
+	}
+
+	const vkcv::VertexLayout particleLayout { vbParticles };
+
+	vkcv::GraphicsPipelineConfig particlePipelineDefinition{
+		particleShaderProgram,
+		UINT32_MAX,
+		UINT32_MAX,
+		renderPass,
+		{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 }
+	});
+	
+	const uint32_t voxelWidth = 160;
+	const uint32_t voxelHeight = 90;
+	const uint32_t voxelDepth = 64;
+	
+	std::vector<uint32_t> zeroVoxel;
+	zeroVoxel.resize(voxelWidth * voxelHeight * voxelDepth, 0);
+	
+	vkcv::Image voxelRed = core.createImage(
+		vk::Format::eR32Uint,
+		voxelWidth,
+		voxelHeight,
+		voxelDepth,
+		false, true
+	);
+	
+	vkcv::Image voxelGreen = core.createImage(
+		vk::Format::eR32Uint,
+		voxelWidth,
+		voxelHeight,
+		voxelDepth,
+		false, true
+	);
+	
+	vkcv::Image voxelBlue = core.createImage(
+		vk::Format::eR32Uint,
+		voxelWidth,
+		voxelHeight,
+		voxelDepth,
+		false, true
+	);
+	
+	vkcv::Image voxelDensity = core.createImage(
+		vk::Format::eR32Uint,
+		voxelWidth,
+		voxelHeight,
+		voxelDepth,
+		false, true
+	);
+	
+	std::array<vkcv::ImageHandle, 2> voxelData {
+		core.createImage(
+			vk::Format::eR16G16B16A16Sfloat,
+			voxelWidth,
+			voxelHeight,
+			voxelDepth,
+			false, true
+		).getHandle(),
+		core.createImage(
+			vk::Format::eR16G16B16A16Sfloat,
+			voxelWidth,
+			voxelHeight,
+			voxelDepth,
+			false, true
+		).getHandle()
+	};
+	
+	vkcv::Image voxelSamples = core.createImage(
+		colorFormat,
+		voxelWidth,
+		voxelHeight,
+		1, false, true
+   	);
+	
+	vkcv::SamplerHandle voxelSampler = core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
+	
+	vkcv::ShaderProgram voxelClearShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/clear.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		voxelClearShader.addShader(shaderStage, path);
+	});
+	
+	const auto& voxelBindings = voxelClearShader.getReflectedDescriptors().at(0);
+	auto voxelDescriptorSetLayout = core.createDescriptorSetLayout(voxelBindings);
+	
+	vkcv::ComputePipelineHandle voxelClearPipeline = core.createComputePipeline({
+		voxelClearShader,
+		{ voxelDescriptorSetLayout }
+	});
+	
+	vkcv::ShaderProgram voxelParticleShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/voxel_particle.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		voxelParticleShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::ComputePipelineHandle voxelParticlePipeline = core.createComputePipeline({
+		voxelParticleShader,
+		{ descriptorSetLayout, voxelDescriptorSetLayout }
+	});
+	
+	vkcv::ShaderProgram voxelSmokeShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/voxel_smoke.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		voxelSmokeShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::ComputePipelineHandle voxelSmokePipeline = core.createComputePipeline({
+		voxelSmokeShader,
+		{ smokeDescriptorLayout, voxelDescriptorSetLayout }
+	});
+	
+	vkcv::ShaderProgram voxelTrailShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/voxel_trail.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		voxelTrailShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::ComputePipelineHandle voxelTrailPipeline = core.createComputePipeline({
+		voxelTrailShader,
+		{ trailDescriptorLayout, voxelDescriptorSetLayout }
+	});
+	
+	vkcv::ShaderProgram voxelShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/voxel.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		voxelShader.addShader(shaderStage, path);
+	});
+	
+	const auto& voxelOutBindings = voxelShader.getReflectedDescriptors().at(1);
+	auto voxelOutDescriptorSetLayout = core.createDescriptorSetLayout(voxelOutBindings);
+	
+	vkcv::ComputePipelineHandle voxelPipeline = core.createComputePipeline({
+		voxelShader,
+		{ voxelDescriptorSetLayout, voxelOutDescriptorSetLayout }
+	});
+	
+	vkcv::ShaderProgram fluidShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/fluid.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		fluidShader.addShader(shaderStage, path);
+	});
+	
+	const auto& fluidBindings = fluidShader.getReflectedDescriptors().at(0);
+	auto fluidDescriptorSetLayout = core.createDescriptorSetLayout(fluidBindings);
+	
+	vkcv::ComputePipelineHandle fluidPipeline = core.createComputePipeline({
+		fluidShader,
+		{ fluidDescriptorSetLayout }
+	});
+	
+	vkcv::ShaderProgram voxelSampleShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/sample.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		voxelSampleShader.addShader(shaderStage, path);
+	});
+	
+	const auto& sampleBindings = voxelSampleShader.getReflectedDescriptors().at(1);
+	auto samplesDescriptorSetLayout = core.createDescriptorSetLayout(sampleBindings);
+	
+	vkcv::ComputePipelineHandle voxelSamplePipeline = core.createComputePipeline({
+		voxelSampleShader,
+		{ voxelOutDescriptorSetLayout, samplesDescriptorSetLayout }
+	});
+	
+	auto voxelDescriptorSet = core.createDescriptorSet(voxelDescriptorSetLayout);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageImage(0, voxelRed.getHandle());
+		writes.writeStorageImage(1, voxelGreen.getHandle());
+		writes.writeStorageImage(2, voxelBlue.getHandle());
+		writes.writeStorageImage(3, voxelDensity.getHandle());
+		core.writeDescriptorSet(voxelDescriptorSet, writes);
+	}
+	
+	auto voxelOutDescriptorSet = core.createDescriptorSet(voxelOutDescriptorSetLayout);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageImage(0, voxelData[0]);
+		core.writeDescriptorSet(voxelOutDescriptorSet, writes);
+	}
+	
+	std::array<vkcv::DescriptorSetHandle, 2> fluidDescriptorSet {
+		core.createDescriptorSet(fluidDescriptorSetLayout),
+		core.createDescriptorSet(fluidDescriptorSetLayout)
+	};
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeSampledImage(0, voxelData[0]);
+		writes.writeSampler(1, voxelSampler);
+		writes.writeStorageImage(2, voxelData[1]);
+		core.writeDescriptorSet(fluidDescriptorSet[0], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeSampledImage(0, voxelData[1]);
+		writes.writeSampler(1, voxelSampler);
+		writes.writeStorageImage(2, voxelData[0]);
+		core.writeDescriptorSet(fluidDescriptorSet[1], writes);
+	}
+	
+	auto samplesDescriptorSet = core.createDescriptorSet(samplesDescriptorSetLayout);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageImage(0, voxelSamples.getHandle());
+		core.writeDescriptorSet(samplesDescriptorSet, writes);
+	}
+	
+	vkcv::effects::BloomAndFlaresEffect bloomAndFlares (core);
+	bloomAndFlares.setUpsamplingLimit(3);
+	
+	vkcv::ShaderProgram addShader;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/add.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		addShader.addShader(shaderStage, path);
+	});
+	
+	vkcv::DescriptorSetLayoutHandle addDescriptorLayout = core.createDescriptorSetLayout(addShader.getReflectedDescriptors().at(0));
+	vkcv::DescriptorSetHandle addDescriptor = core.createDescriptorSet(addDescriptorLayout);
+	
+	vkcv::ComputePipelineHandle addPipe = core.createComputePipeline({
+		addShader,
+		{ addDescriptorLayout, generationDescriptorLayout }
+	});
+	
+	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;
+		}
+	
+		for (size_t i = 0; i < colorBuffers.size(); i++) {
+			if ((core.getImageWidth(colorBuffers[i]) != swapchainWidth) ||
+				(core.getImageHeight(colorBuffers[i]) != swapchainHeight)) {
+				colorBuffers[i] = core.createImage(
+					colorFormat,
+					swapchainWidth,
+					swapchainHeight,
+					1, false, true, true
+				).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;
+		
+		float time_values [2];
+		time_values[0] = 0.000001f * static_cast<float>(time.count());
+		time_values[1] = 0.000001f * static_cast<float>(deltatime.count());
+		
+		std::cout << time_values[0] << " " << time_values[1] << std::endl;
+		
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		
+		uint32_t voxelDispatchCount[3];
+		voxelDispatchCount[0] = std::ceil(voxelWidth / 4.f);
+		voxelDispatchCount[1] = std::ceil(voxelHeight / 4.f);
+		voxelDispatchCount[2] = std::ceil(voxelDepth / 4.f);
+		
+		core.recordBeginDebugLabel(cmdStream, "Voxel clear", { 0.5f, 0.25f, 0.8f, 1.0f });
+		core.prepareImageForStorage(cmdStream, voxelRed.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelGreen.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelBlue.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelDensity.getHandle());
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			voxelClearPipeline,
+			voxelDispatchCount,
+			{ vkcv::DescriptorSetUsage(0, voxelDescriptorSet) },
+			vkcv::PushConstants(0)
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBufferMemoryBarrier(cmdStream, eventBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, smokeBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, smokeIndexBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, trailBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, pointBuffer.getHandle());
+		
+		uint32_t particleDispatchCount[3];
+		particleDispatchCount[0] = std::ceil(particleBuffer.getCount() / 256.f);
+		particleDispatchCount[1] = 1;
+		particleDispatchCount[2] = 1;
+		
+		vkcv::PushConstants pushConstantsTime (2 * sizeof(float));
+		pushConstantsTime.appendDrawcall(time_values);
+		
+		core.recordBeginDebugLabel(cmdStream, "Generation", { 0.0f, 0.0f, 1.0f, 1.0f });
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			generationPipeline,
+			particleDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, descriptorSet),
+				vkcv::DescriptorSetUsage(1, generationDescriptorSet),
+				vkcv::DescriptorSetUsage(2, smokeDescriptorSet),
+				vkcv::DescriptorSetUsage(3, trailDescriptorSet)
+			},
+			pushConstantsTime
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBufferMemoryBarrier(cmdStream, smokeBuffer.getHandle());
+		
+		uint32_t smokeDispatchCount[3];
+		smokeDispatchCount[0] = std::ceil(smokeBuffer.getCount() / 256.f);
+		smokeDispatchCount[1] = 1;
+		smokeDispatchCount[2] = 1;
+		
+		core.recordBeginDebugLabel(cmdStream, "Smoke scaling", { 0.0f, 0.0f, 1.0f, 1.0f });
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			scalePipeline,
+			smokeDispatchCount,
+			{ vkcv::DescriptorSetUsage(0, smokeDescriptorSet) },
+			pushConstantsTime
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
+		
+		core.recordBeginDebugLabel(cmdStream, "Particle motion", { 0.0f, 0.0f, 1.0f, 1.0f });
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			motionPipeline,
+			particleDispatchCount,
+			{ vkcv::DescriptorSetUsage(0, descriptorSet) },
+			pushConstantsTime
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, trailBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, pointBuffer.getHandle());
+		
+		uint32_t trailDispatchCount[3];
+		trailDispatchCount[0] = std::ceil(trailBuffer.getCount() / 256.f);
+		trailDispatchCount[1] = 1;
+		trailDispatchCount[2] = 1;
+		
+		core.recordBeginDebugLabel(cmdStream, "Trail update", { 0.0f, 0.0f, 1.0f, 1.0f });
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			trailComputePipeline,
+			trailDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, descriptorSet),
+				vkcv::DescriptorSetUsage(1, trailDescriptorSet)
+			},
+			pushConstantsTime
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		cameraManager.update(time_values[1]);
+		
+		const auto& camera = cameraManager.getActiveCamera();
+		
+		core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
+		
+		draw_particles_t draw_particles {
+			camera.getMVP(),
+			swapchainWidth,
+			swapchainHeight
+		};
+		
+		vkcv::PushConstants pushConstantsDraw0 (sizeof(draw_particles_t));
+		pushConstantsDraw0.appendDrawcall(draw_particles);
+		
+		core.recordBeginDebugLabel(cmdStream, "Draw particles", { 1.0f, 0.0f, 1.0f, 1.0f });
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			renderPass,
+			particlePipeline,
+			pushConstantsDraw0,
+			{ drawcallsParticles },
+			{ colorBuffers[0] },
+			windowHandle
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		vkcv::PushConstants pushConstantsVoxel (sizeof(glm::mat4));
+		pushConstantsVoxel.appendDrawcall(camera.getMVP());
+		
+		core.recordBeginDebugLabel(cmdStream, "Particle voxel update", { 1.0f, 0.5f, 0.8f, 1.0f });
+		core.prepareImageForStorage(cmdStream, voxelRed.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelGreen.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelBlue.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelDensity.getHandle());
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			voxelParticlePipeline,
+			particleDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, descriptorSet),
+				vkcv::DescriptorSetUsage(1, voxelDescriptorSet)
+			},
+			pushConstantsVoxel
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBufferMemoryBarrier(cmdStream, smokeBuffer.getHandle());
+		
+		draw_smoke_t draw_smoke {
+			camera.getMVP(),
+			camera.getPosition()
+		};
+		
+		core.recordBeginDebugLabel(cmdStream, "Draw smoke", { 1.0f, 0.5f, 1.0f, 1.0f });
+		vkcv::PushConstants pushConstantsDraw1 (sizeof(draw_smoke_t));
+		pushConstantsDraw1.appendDrawcall(draw_smoke);
+		
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			renderPass,
+			smokePipeline,
+			pushConstantsDraw1,
+			{ drawcallsSmokes },
+			{ colorBuffers[1] },
+			windowHandle
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBeginDebugLabel(cmdStream, "Smoke voxel update", { 1.0f, 0.7f, 0.8f, 1.0f });
+		core.prepareImageForStorage(cmdStream, voxelRed.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelGreen.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelBlue.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelDensity.getHandle());
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			voxelSmokePipeline,
+			smokeDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, smokeDescriptorSet),
+				vkcv::DescriptorSetUsage(1, voxelDescriptorSet)
+			},
+			pushConstantsVoxel
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBufferMemoryBarrier(cmdStream, trailBuffer.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, pointBuffer.getHandle());
+		
+		core.recordBeginDebugLabel(cmdStream, "Draw trails", { 0.75f, 0.5f, 1.0f, 1.0f });
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			renderPass,
+			trailPipeline,
+			pushConstantsDraw1,
+			{ drawcallsTrails },
+			{ colorBuffers[2] },
+			windowHandle
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBeginDebugLabel(cmdStream, "Trail voxel update", { 1.0f, 0.9f, 0.8f, 1.0f });
+		core.prepareImageForStorage(cmdStream, voxelRed.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelGreen.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelBlue.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelDensity.getHandle());
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			voxelTrailPipeline,
+			trailDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, trailDescriptorSet),
+				vkcv::DescriptorSetUsage(1, voxelDescriptorSet)
+			},
+			pushConstantsVoxel
+		);
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBeginDebugLabel(cmdStream, "Combine voxel data", { 0.5f, 0.5f, 0.5f, 1.0f });
+		
+		core.prepareImageForStorage(cmdStream, voxelRed.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelGreen.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelBlue.getHandle());
+		core.prepareImageForStorage(cmdStream, voxelDensity.getHandle());
+		
+		core.prepareImageForStorage(cmdStream, voxelData[0]);
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			voxelPipeline,
+			voxelDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, voxelDescriptorSet),
+				vkcv::DescriptorSetUsage(1, voxelOutDescriptorSet)
+			},
+			vkcv::PushConstants(0)
+		);
+		
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBeginDebugLabel(cmdStream, "Fluid voxel data", { 0.2f, 0.2f, 0.9f, 1.0f });
+		
+		for (size_t i = 0; i < 8; i++) {
+			core.prepareImageForSampling(cmdStream, voxelData[i % 2]);
+			core.prepareImageForStorage(cmdStream, voxelData[(i + 1) % 2]);
+		
+			core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				fluidPipeline,
+				voxelDispatchCount,
+				{ vkcv::DescriptorSetUsage(0, fluidDescriptorSet[i % 2]) },
+				vkcv::PushConstants(0)
+			);
+		}
+		
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBeginDebugLabel(cmdStream, "Sample voxels", { 0.5f, 0.5f, 1.0f, 1.0f });
+		
+		core.prepareImageForStorage(cmdStream, voxelData[0]);
+		core.prepareImageForStorage(cmdStream, voxelSamples.getHandle());
+		
+		uint32_t sampleDispatchCount[3];
+		sampleDispatchCount[0] = std::ceil(voxelWidth / 8.f);
+		sampleDispatchCount[1] = std::ceil(voxelHeight / 8.f);
+		sampleDispatchCount[2] = 1;
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			voxelSamplePipeline,
+			sampleDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, voxelOutDescriptorSet),
+				vkcv::DescriptorSetUsage(1, samplesDescriptorSet)
+			},
+			vkcv::PushConstants(0)
+		);
+		
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.recordBeginDebugLabel(cmdStream, "Add rendered images", { 0.5f, 0.5f, 1.0f, 1.0f });
+		
+		vkcv::DescriptorWrites addDescriptorWrites;
+		addDescriptorWrites.writeSampledImage(0, voxelSamples.getHandle());
+		addDescriptorWrites.writeSampler(1, voxelSampler);
+		
+		for (size_t i = 0; i < colorBuffers.size(); i++) {
+			addDescriptorWrites.writeStorageImage(2 + i, colorBuffers[i]);
+			core.prepareImageForStorage(cmdStream, colorBuffers[i]);
+		}
+		
+		core.writeDescriptorSet(addDescriptor, addDescriptorWrites);
+		core.prepareImageForSampling(cmdStream, voxelSamples.getHandle());
+		
+		uint32_t colorDispatchCount[3];
+		colorDispatchCount[0] = std::ceil(swapchainWidth / 8.f);
+		colorDispatchCount[1] = std::ceil(swapchainHeight / 8.f);
+		colorDispatchCount[2] = 1;
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			addPipe,
+			colorDispatchCount,
+			{
+				vkcv::DescriptorSetUsage(0, addDescriptor),
+				vkcv::DescriptorSetUsage(1, generationDescriptorSet)
+			},
+			vkcv::PushConstants(0)
+		);
+		
+		core.recordEndDebugLabel(cmdStream);
+		
+		bloomAndFlares.recordEffect(cmdStream, colorBuffers.back(), colorBuffers.back());
+		
+		core.recordBeginDebugLabel(cmdStream, "Tonemapping", { 0.0f, 1.0f, 0.0f, 1.0f });
+		core.prepareImageForStorage(cmdStream, colorBuffers.back());
+		core.prepareImageForStorage(cmdStream, swapchainImage);
+		
+		vkcv::DescriptorWrites tonemappingDescriptorWrites;
+		tonemappingDescriptorWrites.writeStorageImage(
+			0, colorBuffers.back()
+		).writeStorageImage(
+			1, swapchainImage
+		);
+		
+		core.writeDescriptorSet(tonemappingDescriptor, tonemappingDescriptorWrites);
+		
+		core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			tonemappingPipe,
+			colorDispatchCount,
+			{vkcv::DescriptorSetUsage(0, tonemappingDescriptor) },
+			vkcv::PushConstants(0)
+		);
+		
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		
+		gui.beginGUI();
+		ImGui::Begin("Settings");
+		
+		bool listbox = ImGui::BeginListBox(" ");
+		bool firework = ImGui::Selectable("Firework");
+		bool sparkler = ImGui::Selectable("Sparkler");
+		bool nested = ImGui::Selectable("Nested Firework");
+		ImGui::EndListBox();
+		bool resetTime = ImGui::Button("Reset");
+		auto color = glm::vec3(0.0f);
+		
+		if (!events.empty()) {
+			color = events[0].color;
+		}
+		
+		bool colorChanged = ImGui::ColorPicker3("Color", (float*) & color);
+		
+		ImGui::End();
+		gui.endGUI();
+		
+		core.endFrame(windowHandle);
+
+		particleBuffer.read(particles);
+		sort(particles.begin(), particles.end(),
+			 [](const particle_t p1, const particle_t p2) {
+				 return p1.eventId < p2.eventId;
+			 });
+
+		std::vector<uint32_t> startingIndex;
+		startingIndex.resize(events.size());
+		uint32_t eventIdCheck = std::numeric_limits<uint32_t>::max();
+		
+		for (size_t i = 0; i < particles.size(); i++) {
+			if (particles[i].eventId != eventIdCheck) {
+				eventIdCheck = particles [i].eventId;
+				if (eventIdCheck < startingIndex.size()) {
+					startingIndex [eventIdCheck] = i;
+				}
+			}
+		}
+
+		startIndexBuffer.fill(startingIndex);
+
+		if (firework) {
+			events.clear();
+			InitializeFireworkEvents(events);
+			resetTime = true;
+		} else if (sparkler) {
+			events.clear();
+			InitializeSparklerEvents(events);
+			resetTime = true;
+		} else if (nested) {
+			events.clear();
+			InitializeNestedFireworkEvents(events);
+			resetTime = true;
+		}
+
+		if (colorChanged) {
+			ChangeColor(events, color);
+			resetTime = true;
+		}
+		
+		if (resetTime) {
+			start = std::chrono::system_clock::now();	
+			InitializeParticles(particles);
+			particleBuffer.fill(particles);
+			eventBuffer.fill(events);
+			smokeBuffer.fill(smokes);
+			trailBuffer.fill(trails);
+			pointBuffer.fill(points);
+			
+			memset(smokeIndices, 0, smokeIndexBuffer.getSize());
+		}
+
+		particleBufferCopy.fill(particles);
+	}
+	
+	smokeIndexBuffer.unmap();
+	return 0;
+}
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index c853f38395eda1445da3205eebadafe0cc9fa74f..176e07ae073135b0311226f825cc639cc34e9610 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -364,9 +364,9 @@ namespace vkcv
 
 				clearValues.emplace_back(std::array<float, 4>{
 					clear,
-						clear,
-						clear,
-						1.f
+					clear,
+					clear,
+					0.f
 				});
 			}
 		}
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index 83246db6ecb28c5a513d36dc37d4ad5faa2e4b5f..17ed1b8338e219ec12e094b836a3ec2208bf1bb1 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -506,8 +506,10 @@ namespace vkcv {
 		switch (format) {
 			case vk::Format::eR8Unorm:
 				return 1;
+			case vk::Format::eR16Unorm:
+				return 2;
+			case vk::Format::eR32Uint:
 			case vk::Format::eR8G8B8A8Srgb:
-				return 4;
 			case vk::Format::eR8G8B8A8Unorm:
 				return 4;
 			case vk::Format::eR16G16B16A16Sfloat: