ADD: Particle system
This commit is contained in:
@@ -44,6 +44,7 @@ foreach(GLSL ${GLSL_SOURCE_FILES})
|
|||||||
list(APPEND SPIRV_BINARY_FILES ${SPIRV})
|
list(APPEND SPIRV_BINARY_FILES ${SPIRV})
|
||||||
endforeach(GLSL)
|
endforeach(GLSL)
|
||||||
|
|
||||||
add_custom_target(
|
add_custom_target(shaders_spirv DEPENDS ${SPIRV_BINARY_FILES})
|
||||||
DEPENDS ${SPIRV_BINARY_FILES}
|
|
||||||
)
|
# Ensure shaders are built alongside the executable.
|
||||||
|
add_dependencies(vulkan_engine shaders_spirv)
|
||||||
|
|||||||
26
shaders/particles.frag
Normal file
26
shaders/particles.frag
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 v_color;
|
||||||
|
layout(location = 1) in vec2 v_uv;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// Soft circular sprite.
|
||||||
|
vec2 p = v_uv * 2.0 - 1.0;
|
||||||
|
float r = length(p);
|
||||||
|
float mask = smoothstep(1.0, 0.0, r);
|
||||||
|
|
||||||
|
vec4 c = v_color;
|
||||||
|
c.rgb *= mask;
|
||||||
|
c.a *= mask;
|
||||||
|
|
||||||
|
if (c.a <= 0.001)
|
||||||
|
{
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
outColor = c;
|
||||||
|
}
|
||||||
|
|
||||||
66
shaders/particles.vert
Normal file
66
shaders/particles.vert
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform SceneData
|
||||||
|
{
|
||||||
|
mat4 view;
|
||||||
|
mat4 proj;
|
||||||
|
mat4 viewproj;
|
||||||
|
} sceneData;
|
||||||
|
|
||||||
|
struct Particle
|
||||||
|
{
|
||||||
|
vec4 pos_age;
|
||||||
|
vec4 vel_life;
|
||||||
|
vec4 color;
|
||||||
|
vec4 misc;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(std430, set = 1, binding = 0) readonly buffer ParticlePool
|
||||||
|
{
|
||||||
|
Particle particles[];
|
||||||
|
} pool;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 v_color;
|
||||||
|
layout(location = 1) out vec2 v_uv;
|
||||||
|
|
||||||
|
vec2 quad_corner(uint vidx)
|
||||||
|
{
|
||||||
|
// Two triangles (6 verts) in a unit quad centered at origin.
|
||||||
|
const vec2 corners[6] = vec2[6](
|
||||||
|
vec2(-0.5, -0.5),
|
||||||
|
vec2( 0.5, -0.5),
|
||||||
|
vec2( 0.5, 0.5),
|
||||||
|
vec2(-0.5, -0.5),
|
||||||
|
vec2( 0.5, 0.5),
|
||||||
|
vec2(-0.5, 0.5)
|
||||||
|
);
|
||||||
|
return corners[vidx % 6u];
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
uint particle_index = gl_InstanceIndex;
|
||||||
|
Particle p = pool.particles[particle_index];
|
||||||
|
|
||||||
|
float life = max(p.vel_life.w, 1e-6);
|
||||||
|
float remaining = clamp(p.pos_age.w, 0.0, life);
|
||||||
|
float t = remaining / life; // remaining fraction
|
||||||
|
|
||||||
|
float fade_out = smoothstep(0.0, 0.15, t);
|
||||||
|
float fade_in = smoothstep(0.0, 0.05, 1.0 - t);
|
||||||
|
float fade = fade_in * fade_out;
|
||||||
|
|
||||||
|
vec2 corner = quad_corner(uint(gl_VertexIndex));
|
||||||
|
v_uv = corner + vec2(0.5);
|
||||||
|
|
||||||
|
// Camera right/up in world-local space from view matrix rows.
|
||||||
|
vec3 cam_right = vec3(sceneData.view[0][0], sceneData.view[1][0], sceneData.view[2][0]);
|
||||||
|
vec3 cam_up = vec3(sceneData.view[0][1], sceneData.view[1][1], sceneData.view[2][1]);
|
||||||
|
|
||||||
|
float size = max(p.misc.x, 0.0);
|
||||||
|
vec3 pos = p.pos_age.xyz + (cam_right * corner.x + cam_up * corner.y) * size;
|
||||||
|
|
||||||
|
v_color = vec4(p.color.rgb * fade, p.color.a * fade);
|
||||||
|
gl_Position = sceneData.viewproj * vec4(pos, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
158
shaders/particles_update.comp
Normal file
158
shaders/particles_update.comp
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// v1 particle update: one SSBO, per-system dispatch.
|
||||||
|
// Particles store "remaining life" in pos_age.w and total lifetime in vel_life.w.
|
||||||
|
|
||||||
|
layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
|
||||||
|
struct Particle
|
||||||
|
{
|
||||||
|
vec4 pos_age; // xyz = local position, w = remaining life (seconds)
|
||||||
|
vec4 vel_life; // xyz = local velocity, w = lifetime (seconds)
|
||||||
|
vec4 color; // rgba
|
||||||
|
vec4 misc; // x=size, y=seed, z=unused, w=flags
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 0) buffer ParticlePool
|
||||||
|
{
|
||||||
|
Particle particles[];
|
||||||
|
} pool;
|
||||||
|
|
||||||
|
layout(push_constant) uniform Push
|
||||||
|
{
|
||||||
|
uvec4 header; // x=base, y=count, z=reset
|
||||||
|
vec4 sim; // x=dt, y=time, z=drag, w=gravity (m/s^2, pulls down -Y)
|
||||||
|
vec4 origin_delta; // xyz origin delta (local)
|
||||||
|
vec4 emitter_pos_radius;// xyz emitter pos (local), w=spawn radius
|
||||||
|
vec4 emitter_dir_cone; // xyz emitter dir (local), w=cone angle radians (<=0 => sphere)
|
||||||
|
vec4 ranges; // x=minSpeed, y=maxSpeed, z=minLife, w=maxLife
|
||||||
|
vec4 size_range; // x=minSize, y=maxSize
|
||||||
|
vec4 color; // rgba
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
uint hash_u32(uint x)
|
||||||
|
{
|
||||||
|
x ^= x >> 16;
|
||||||
|
x *= 0x7feb352du;
|
||||||
|
x ^= x >> 15;
|
||||||
|
x *= 0x846ca68bu;
|
||||||
|
x ^= x >> 16;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
float rand01(inout uint state)
|
||||||
|
{
|
||||||
|
state = hash_u32(state);
|
||||||
|
// 0..1 (exclusive 1)
|
||||||
|
return float(state) * (1.0 / 4294967296.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 random_unit_vector(inout uint state)
|
||||||
|
{
|
||||||
|
float z = rand01(state) * 2.0 - 1.0;
|
||||||
|
float a = rand01(state) * 6.28318530718;
|
||||||
|
float r = sqrt(max(0.0, 1.0 - z * z));
|
||||||
|
return vec3(r * cos(a), z, r * sin(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 random_in_sphere(inout uint state)
|
||||||
|
{
|
||||||
|
vec3 dir = random_unit_vector(state);
|
||||||
|
float u = rand01(state);
|
||||||
|
float radius = pow(u, 1.0 / 3.0);
|
||||||
|
return dir * radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 random_in_cone(inout uint state, vec3 axis, float cone_angle)
|
||||||
|
{
|
||||||
|
axis = normalize(axis);
|
||||||
|
|
||||||
|
float cos_max = clamp(cos(cone_angle), -1.0, 1.0);
|
||||||
|
float cos_t = mix(cos_max, 1.0, rand01(state));
|
||||||
|
float sin_t = sqrt(max(0.0, 1.0 - cos_t * cos_t));
|
||||||
|
|
||||||
|
float phi = rand01(state) * 6.28318530718;
|
||||||
|
vec3 local_dir = vec3(cos(phi) * sin_t, sin(phi) * sin_t, cos_t);
|
||||||
|
|
||||||
|
vec3 up = (abs(axis.y) < 0.999) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
|
||||||
|
vec3 u = normalize(cross(up, axis));
|
||||||
|
vec3 v = cross(axis, u);
|
||||||
|
|
||||||
|
return u * local_dir.x + v * local_dir.y + axis * local_dir.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
void respawn(uint idx)
|
||||||
|
{
|
||||||
|
uint seed = idx ^ (hash_u32(floatBitsToUint(pc.sim.y)) * 1664525u) ^ 1013904223u;
|
||||||
|
|
||||||
|
float life = mix(pc.ranges.z, pc.ranges.w, rand01(seed));
|
||||||
|
life = max(life, 0.01);
|
||||||
|
|
||||||
|
float speed = mix(pc.ranges.x, pc.ranges.y, rand01(seed));
|
||||||
|
speed = max(speed, 0.0);
|
||||||
|
|
||||||
|
float size = mix(pc.size_range.x, pc.size_range.y, rand01(seed));
|
||||||
|
size = max(size, 0.001);
|
||||||
|
|
||||||
|
vec3 spawn_pos = pc.emitter_pos_radius.xyz;
|
||||||
|
float radius = max(pc.emitter_pos_radius.w, 0.0);
|
||||||
|
if (radius > 0.0)
|
||||||
|
{
|
||||||
|
spawn_pos += random_in_sphere(seed) * radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 dir;
|
||||||
|
float cone = pc.emitter_dir_cone.w;
|
||||||
|
if (cone > 0.0)
|
||||||
|
{
|
||||||
|
dir = random_in_cone(seed, pc.emitter_dir_cone.xyz, cone);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dir = random_unit_vector(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Particle p;
|
||||||
|
p.pos_age = vec4(spawn_pos, life);
|
||||||
|
p.vel_life = vec4(dir * speed, life);
|
||||||
|
p.color = pc.color;
|
||||||
|
p.misc = vec4(size, rand01(seed), 0.0, 0.0);
|
||||||
|
pool.particles[idx] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
uint i = gl_GlobalInvocationID.x;
|
||||||
|
if (i >= pc.header.y) return;
|
||||||
|
|
||||||
|
uint idx = pc.header.x + i;
|
||||||
|
|
||||||
|
Particle p = pool.particles[idx];
|
||||||
|
|
||||||
|
// Keep particles stable under floating-origin recenters.
|
||||||
|
p.pos_age.xyz -= pc.origin_delta.xyz;
|
||||||
|
|
||||||
|
bool reset = (pc.header.z != 0u);
|
||||||
|
bool dead = (p.pos_age.w <= 0.0) || (p.vel_life.w <= 0.0);
|
||||||
|
|
||||||
|
if (reset || dead)
|
||||||
|
{
|
||||||
|
respawn(idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dt = pc.sim.x;
|
||||||
|
float drag = max(pc.sim.z, 0.0);
|
||||||
|
float gravity = pc.sim.w;
|
||||||
|
|
||||||
|
vec3 vel = p.vel_life.xyz;
|
||||||
|
vel += vec3(0.0, -gravity, 0.0) * dt;
|
||||||
|
vel *= exp(-drag * dt);
|
||||||
|
|
||||||
|
p.pos_age.xyz += vel * dt;
|
||||||
|
p.vel_life.xyz = vel;
|
||||||
|
p.pos_age.w -= dt;
|
||||||
|
|
||||||
|
pool.particles[idx] = p;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -83,6 +83,8 @@ add_executable (vulkan_engine
|
|||||||
render/passes/fxaa.cpp
|
render/passes/fxaa.cpp
|
||||||
render/passes/ssr.h
|
render/passes/ssr.h
|
||||||
render/passes/ssr.cpp
|
render/passes/ssr.cpp
|
||||||
|
render/passes/particles.h
|
||||||
|
render/passes/particles.cpp
|
||||||
render/passes/transparent.h
|
render/passes/transparent.h
|
||||||
render/passes/transparent.cpp
|
render/passes/transparent.cpp
|
||||||
render/passes/imgui_pass.h
|
render/passes/imgui_pass.h
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
#include "render/passes/geometry.h"
|
#include "render/passes/geometry.h"
|
||||||
#include "render/passes/imgui_pass.h"
|
#include "render/passes/imgui_pass.h"
|
||||||
#include "render/passes/lighting.h"
|
#include "render/passes/lighting.h"
|
||||||
|
#include "render/passes/particles.h"
|
||||||
#include "render/passes/transparent.h"
|
#include "render/passes/transparent.h"
|
||||||
#include "render/passes/fxaa.h"
|
#include "render/passes/fxaa.h"
|
||||||
#include "render/passes/tonemap.h"
|
#include "render/passes/tonemap.h"
|
||||||
@@ -1187,10 +1188,16 @@ void VulkanEngine::draw()
|
|||||||
hSSR);
|
hSSR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Downstream passes draw on top of either the SSR output or the raw HDR draw.
|
||||||
|
RGImageHandle hdrTarget = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw;
|
||||||
|
|
||||||
|
if (auto *particles = _renderPassManager->getPass<ParticlePass>())
|
||||||
|
{
|
||||||
|
particles->register_graph(_renderGraph.get(), hdrTarget, hDepth);
|
||||||
|
}
|
||||||
|
|
||||||
if (auto *transparent = _renderPassManager->getPass<TransparentPass>())
|
if (auto *transparent = _renderPassManager->getPass<TransparentPass>())
|
||||||
{
|
{
|
||||||
// Transparent objects draw on top of either the SSR output or the raw HDR draw.
|
|
||||||
RGImageHandle hdrTarget = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw;
|
|
||||||
transparent->register_graph(_renderGraph.get(), hdrTarget, hDepth);
|
transparent->register_graph(_renderGraph.get(), hdrTarget, hDepth);
|
||||||
}
|
}
|
||||||
imguiPass = _renderPassManager->getImGuiPass();
|
imguiPass = _renderPassManager->getImGuiPass();
|
||||||
@@ -1198,8 +1205,7 @@ void VulkanEngine::draw()
|
|||||||
// Optional Tonemap pass: sample HDR draw -> LDR intermediate
|
// Optional Tonemap pass: sample HDR draw -> LDR intermediate
|
||||||
if (auto *tonemap = _renderPassManager->getPass<TonemapPass>())
|
if (auto *tonemap = _renderPassManager->getPass<TonemapPass>())
|
||||||
{
|
{
|
||||||
RGImageHandle hdrInput = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw;
|
finalColor = tonemap->register_graph(_renderGraph.get(), hdrTarget);
|
||||||
finalColor = tonemap->register_graph(_renderGraph.get(), hdrInput);
|
|
||||||
|
|
||||||
// Optional FXAA pass: runs on LDR tonemapped output.
|
// Optional FXAA pass: runs on LDR tonemapped output.
|
||||||
if (auto *fxaa = _renderPassManager->getPass<FxaaPass>())
|
if (auto *fxaa = _renderPassManager->getPass<FxaaPass>())
|
||||||
@@ -1210,7 +1216,7 @@ void VulkanEngine::draw()
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If tonemapping is disabled, present whichever HDR buffer we ended up with.
|
// If tonemapping is disabled, present whichever HDR buffer we ended up with.
|
||||||
finalColor = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw;
|
finalColor = hdrTarget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "render/passes/tonemap.h"
|
#include "render/passes/tonemap.h"
|
||||||
#include "render/passes/fxaa.h"
|
#include "render/passes/fxaa.h"
|
||||||
#include "render/passes/background.h"
|
#include "render/passes/background.h"
|
||||||
|
#include "render/passes/particles.h"
|
||||||
#include <glm/gtx/euler_angles.hpp>
|
#include <glm/gtx/euler_angles.hpp>
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
#include "render/graph/graph.h"
|
#include "render/graph/graph.h"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
#include "context.h"
|
#include "context.h"
|
||||||
#include <core/types.h>
|
#include <core/types.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include "mesh_bvh.h"
|
#include "mesh_bvh.h"
|
||||||
@@ -258,6 +260,138 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ui_particles(VulkanEngine *eng)
|
||||||
|
{
|
||||||
|
if (!eng || !eng->_renderPassManager) return;
|
||||||
|
auto *pass = eng->_renderPassManager->getPass<ParticlePass>();
|
||||||
|
if (!pass)
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted("Particle pass not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t freeCount = pass->free_particles();
|
||||||
|
const uint32_t allocCount = pass->allocated_particles();
|
||||||
|
ImGui::Text("Pool: %u allocated / %u free (max %u)", allocCount, freeCount, ParticlePass::k_max_particles);
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
static int newCount = 32768;
|
||||||
|
newCount = std::max(newCount, 1);
|
||||||
|
ImGui::InputInt("New System Particles", &newCount);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Create"))
|
||||||
|
{
|
||||||
|
const uint32_t want = static_cast<uint32_t>(std::max(1, newCount));
|
||||||
|
pass->create_system(want);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Create 32k"))
|
||||||
|
{
|
||||||
|
pass->create_system(32768);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Create 128k"))
|
||||||
|
{
|
||||||
|
pass->create_system(ParticlePass::k_max_particles);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
auto &systems = pass->systems();
|
||||||
|
if (systems.empty())
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted("No particle systems. Create one above.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int selected = 0;
|
||||||
|
selected = std::clamp(selected, 0, (int)systems.size() - 1);
|
||||||
|
|
||||||
|
if (ImGui::BeginListBox("Systems"))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < (int)systems.size(); ++i)
|
||||||
|
{
|
||||||
|
const auto &s = systems[i];
|
||||||
|
char label[128];
|
||||||
|
std::snprintf(label, sizeof(label), "#%u base=%u count=%u %s",
|
||||||
|
s.id, s.base, s.count, s.enabled ? "on" : "off");
|
||||||
|
const bool isSelected = (selected == i);
|
||||||
|
if (ImGui::Selectable(label, isSelected))
|
||||||
|
{
|
||||||
|
selected = i;
|
||||||
|
}
|
||||||
|
if (isSelected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndListBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = std::clamp(selected, 0, (int)systems.size() - 1);
|
||||||
|
auto &s = systems[(size_t)selected];
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
ImGui::Text("Selected: id=%u base=%u count=%u", s.id, s.base, s.count);
|
||||||
|
ImGui::Checkbox("Enabled", &s.enabled);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Reset (Respawn)"))
|
||||||
|
{
|
||||||
|
s.reset = true;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Destroy"))
|
||||||
|
{
|
||||||
|
const uint32_t id = s.id;
|
||||||
|
pass->destroy_system(id);
|
||||||
|
selected = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *blendItems[] = {"Additive", "Alpha (unsorted)"};
|
||||||
|
int blend = (s.blend == ParticlePass::BlendMode::Alpha) ? 1 : 0;
|
||||||
|
if (ImGui::Combo("Blend", &blend, blendItems, 2))
|
||||||
|
{
|
||||||
|
s.blend = (blend == 1) ? ParticlePass::BlendMode::Alpha : ParticlePass::BlendMode::Additive;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
static int pendingResizeCount = 0;
|
||||||
|
if (!ImGui::IsAnyItemActive())
|
||||||
|
{
|
||||||
|
pendingResizeCount = (int)s.count;
|
||||||
|
}
|
||||||
|
ImGui::InputInt("Resize Count", &pendingResizeCount);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Apply Resize"))
|
||||||
|
{
|
||||||
|
const uint32_t want = static_cast<uint32_t>(std::max(0, pendingResizeCount));
|
||||||
|
pass->resize_system(s.id, want);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("Emitter");
|
||||||
|
ImGui::InputFloat3("Position (local)", reinterpret_cast<float *>(&s.params.emitter_pos_local));
|
||||||
|
ImGui::SliderFloat("Spawn Radius", &s.params.spawn_radius, 0.0f, 10.0f, "%.3f");
|
||||||
|
ImGui::InputFloat3("Direction (local)", reinterpret_cast<float *>(&s.params.emitter_dir_local));
|
||||||
|
ImGui::SliderFloat("Cone Angle (deg)", &s.params.cone_angle_degrees, 0.0f, 89.0f, "%.1f");
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("Motion");
|
||||||
|
ImGui::InputFloat("Min Speed", &s.params.min_speed);
|
||||||
|
ImGui::InputFloat("Max Speed", &s.params.max_speed);
|
||||||
|
ImGui::InputFloat("Min Life (s)", &s.params.min_life);
|
||||||
|
ImGui::InputFloat("Max Life (s)", &s.params.max_life);
|
||||||
|
ImGui::InputFloat("Min Size", &s.params.min_size);
|
||||||
|
ImGui::InputFloat("Max Size", &s.params.max_size);
|
||||||
|
ImGui::SliderFloat("Drag", &s.params.drag, 0.0f, 10.0f, "%.3f");
|
||||||
|
ImGui::SliderFloat("Gravity (m/s^2)", &s.params.gravity, 0.0f, 30.0f, "%.2f");
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("Color");
|
||||||
|
ImGui::ColorEdit4("Tint", reinterpret_cast<float *>(&s.params.color), ImGuiColorEditFlags_Float);
|
||||||
|
}
|
||||||
|
|
||||||
// IBL test grid spawner (spheres varying metallic/roughness)
|
// IBL test grid spawner (spheres varying metallic/roughness)
|
||||||
static void spawn_ibl_test(VulkanEngine *eng)
|
static void spawn_ibl_test(VulkanEngine *eng)
|
||||||
{
|
{
|
||||||
@@ -1533,6 +1667,11 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng)
|
|||||||
ui_background(eng);
|
ui_background(eng);
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
if (ImGui::BeginTabItem("Particles"))
|
||||||
|
{
|
||||||
|
ui_particles(eng);
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
if (ImGui::BeginTabItem("Shadows"))
|
if (ImGui::BeginTabItem("Shadows"))
|
||||||
{
|
{
|
||||||
ui_shadows(eng);
|
ui_shadows(eng);
|
||||||
|
|||||||
@@ -374,11 +374,13 @@ bool RenderGraph::compile()
|
|||||||
info.access = VK_ACCESS_2_UNIFORM_READ_BIT;
|
info.access = VK_ACCESS_2_UNIFORM_READ_BIT;
|
||||||
break;
|
break;
|
||||||
case RGBufferUsage::StorageRead:
|
case RGBufferUsage::StorageRead:
|
||||||
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
|
// Storage buffers can be read from compute and any graphics stage (including vertex).
|
||||||
|
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT;
|
||||||
info.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT;
|
info.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT;
|
||||||
break;
|
break;
|
||||||
case RGBufferUsage::StorageReadWrite:
|
case RGBufferUsage::StorageReadWrite:
|
||||||
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
|
// Storage buffers can be read/write from compute and read in graphics stages.
|
||||||
|
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT;
|
||||||
info.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
|
info.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
|
||||||
break;
|
break;
|
||||||
case RGBufferUsage::IndirectArgs:
|
case RGBufferUsage::IndirectArgs:
|
||||||
|
|||||||
539
src/render/passes/particles.cpp
Normal file
539
src/render/passes/particles.cpp
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
#include "particles.h"
|
||||||
|
|
||||||
|
#include "compute/vk_compute.h"
|
||||||
|
#include "core/assets/manager.h"
|
||||||
|
#include "core/context.h"
|
||||||
|
#include "core/descriptor/descriptors.h"
|
||||||
|
#include "core/descriptor/manager.h"
|
||||||
|
#include "core/device/device.h"
|
||||||
|
#include "core/device/resource.h"
|
||||||
|
#include "core/device/swapchain.h"
|
||||||
|
#include "core/frame/resources.h"
|
||||||
|
#include "core/pipeline/manager.h"
|
||||||
|
#include "render/graph/graph.h"
|
||||||
|
#include "render/pipelines.h"
|
||||||
|
#include "scene/vk_scene.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct ParticleGPU
|
||||||
|
{
|
||||||
|
glm::vec4 pos_age;
|
||||||
|
glm::vec4 vel_life;
|
||||||
|
glm::vec4 color;
|
||||||
|
glm::vec4 misc;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(ParticleGPU) == 64);
|
||||||
|
|
||||||
|
constexpr uint32_t k_local_size_x = 256;
|
||||||
|
|
||||||
|
struct ParticleUpdatePushConstants
|
||||||
|
{
|
||||||
|
glm::uvec4 header; // x=base, y=count, z=reset, w=unused
|
||||||
|
glm::vec4 sim; // x=dt, y=time, z=drag, w=gravity
|
||||||
|
glm::vec4 origin_delta; // xyz origin delta local
|
||||||
|
glm::vec4 emitter_pos_radius; // xyz emitter pos local, w=spawn radius
|
||||||
|
glm::vec4 emitter_dir_cone; // xyz emitter dir local, w=cone angle (radians)
|
||||||
|
glm::vec4 ranges; // x=minSpeed, y=maxSpeed, z=minLife, w=maxLife
|
||||||
|
glm::vec4 size_range; // x=minSize, y=maxSize, z/w unused
|
||||||
|
glm::vec4 color; // rgba
|
||||||
|
};
|
||||||
|
static_assert(sizeof(ParticleUpdatePushConstants) == 128);
|
||||||
|
|
||||||
|
static glm::vec3 safe_normalize(const glm::vec3 &v, const glm::vec3 &fallback)
|
||||||
|
{
|
||||||
|
const float len2 = glm::dot(v, v);
|
||||||
|
if (len2 <= 1e-10f || !std::isfinite(len2))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return v * (1.0f / std::sqrt(len2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static float clamp_nonnegative(float v)
|
||||||
|
{
|
||||||
|
if (!std::isfinite(v)) return 0.0f;
|
||||||
|
return std::max(0.0f, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParticlePass::init(EngineContext *context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
if (!_context || !_context->getDevice() || !_context->getResources() || !_context->getAssets() ||
|
||||||
|
!_context->pipelines || !_context->getDescriptorLayouts())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_free_ranges.clear();
|
||||||
|
_free_ranges.push_back(FreeRange{0u, k_max_particles});
|
||||||
|
|
||||||
|
_particle_pool_size = VkDeviceSize(sizeof(ParticleGPU)) * VkDeviceSize(k_max_particles);
|
||||||
|
_particle_pool = _context->getResources()->create_buffer(
|
||||||
|
_particle_pool_size,
|
||||||
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||||
|
VMA_MEMORY_USAGE_GPU_ONLY);
|
||||||
|
|
||||||
|
// Zero the pool once so all particles start "dead" and get respawned deterministically by the compute update.
|
||||||
|
if (_particle_pool.buffer != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
VkBuffer buf = _particle_pool.buffer;
|
||||||
|
VkDeviceSize size = _particle_pool_size;
|
||||||
|
_context->getResources()->immediate_submit([buf, size](VkCommandBuffer cmd) {
|
||||||
|
vkCmdFillBuffer(cmd, buf, 0, size, 0u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
VkDevice device = _context->getDevice()->device();
|
||||||
|
|
||||||
|
// Set=1 layout for graphics: particle pool SSBO.
|
||||||
|
{
|
||||||
|
DescriptorLayoutBuilder builder;
|
||||||
|
builder.add_binding(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
|
||||||
|
_particle_set_layout = builder.build(device, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute update pipeline + instance.
|
||||||
|
{
|
||||||
|
ComputePipelineCreateInfo ci{};
|
||||||
|
ci.shaderPath = _context->getAssets()->shaderPath("particles_update.comp.spv");
|
||||||
|
ci.descriptorTypes = {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER};
|
||||||
|
ci.pushConstantSize = sizeof(ParticleUpdatePushConstants);
|
||||||
|
ci.pushConstantStages = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||||
|
_context->pipelines->createComputePipeline("particles.update", ci);
|
||||||
|
|
||||||
|
_context->pipelines->createComputeInstance("particles.update", "particles.update");
|
||||||
|
_context->pipelines->setComputeInstanceBuffer("particles.update", 0, _particle_pool.buffer, _particle_pool_size,
|
||||||
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graphics pipelines for render (additive + optional alpha).
|
||||||
|
{
|
||||||
|
const std::string vert = _context->getAssets()->shaderPath("particles.vert.spv");
|
||||||
|
const std::string frag = _context->getAssets()->shaderPath("particles.frag.spv");
|
||||||
|
|
||||||
|
GraphicsPipelineCreateInfo base{};
|
||||||
|
base.vertexShaderPath = vert;
|
||||||
|
base.fragmentShaderPath = frag;
|
||||||
|
base.setLayouts = {
|
||||||
|
_context->getDescriptorLayouts()->gpuSceneDataLayout(), // set = 0
|
||||||
|
_particle_set_layout, // set = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
base.configure = [this](PipelineBuilder &b) {
|
||||||
|
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
|
||||||
|
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
|
||||||
|
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
|
||||||
|
b.set_multisampling_none();
|
||||||
|
b.enable_depthtest(false, VK_COMPARE_OP_GREATER_OR_EQUAL);
|
||||||
|
if (_context && _context->getSwapchain())
|
||||||
|
{
|
||||||
|
b.set_color_attachment_format(_context->getSwapchain()->drawImage().imageFormat);
|
||||||
|
b.set_depth_format(_context->getSwapchain()->depthImage().imageFormat);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphicsPipelineCreateInfo additive = base;
|
||||||
|
additive.configure = [baseCfg = base.configure](PipelineBuilder &b) {
|
||||||
|
baseCfg(b);
|
||||||
|
b.enable_blending_additive();
|
||||||
|
};
|
||||||
|
_context->pipelines->createGraphicsPipeline("particles.additive", additive);
|
||||||
|
|
||||||
|
GraphicsPipelineCreateInfo alpha = base;
|
||||||
|
alpha.configure = [baseCfg = base.configure](PipelineBuilder &b) {
|
||||||
|
baseCfg(b);
|
||||||
|
b.enable_blending_alphablend();
|
||||||
|
};
|
||||||
|
_context->pipelines->createGraphicsPipeline("particles.alpha", alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParticlePass::cleanup()
|
||||||
|
{
|
||||||
|
if (_context && _context->getDevice())
|
||||||
|
{
|
||||||
|
if (_particle_set_layout != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _particle_set_layout, nullptr);
|
||||||
|
_particle_set_layout = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_context && _context->getResources())
|
||||||
|
{
|
||||||
|
if (_particle_pool.buffer != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
_context->getResources()->destroy_buffer(_particle_pool);
|
||||||
|
_particle_pool = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_systems.clear();
|
||||||
|
_free_ranges.clear();
|
||||||
|
_context = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParticlePass::execute(VkCommandBuffer)
|
||||||
|
{
|
||||||
|
// Executed via RenderGraph.
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ParticlePass::free_particles() const
|
||||||
|
{
|
||||||
|
uint64_t sum = 0;
|
||||||
|
for (const auto &r : _free_ranges) sum += r.count;
|
||||||
|
if (sum > k_max_particles) sum = k_max_particles;
|
||||||
|
return static_cast<uint32_t>(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ParticlePass::allocated_particles() const
|
||||||
|
{
|
||||||
|
return k_max_particles - free_particles();
|
||||||
|
}
|
||||||
|
|
||||||
|
ParticlePass::System *ParticlePass::find_system(uint32_t id)
|
||||||
|
{
|
||||||
|
for (auto &s : _systems)
|
||||||
|
{
|
||||||
|
if (s.id == id) return &s;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParticlePass::allocate_range(uint32_t count, uint32_t &out_base)
|
||||||
|
{
|
||||||
|
if (count == 0) return false;
|
||||||
|
for (auto it = _free_ranges.begin(); it != _free_ranges.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->count < count) continue;
|
||||||
|
|
||||||
|
out_base = it->base;
|
||||||
|
it->base += count;
|
||||||
|
it->count -= count;
|
||||||
|
if (it->count == 0) _free_ranges.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParticlePass::merge_free_ranges()
|
||||||
|
{
|
||||||
|
if (_free_ranges.size() < 2) return;
|
||||||
|
|
||||||
|
std::sort(_free_ranges.begin(), _free_ranges.end(), [](const FreeRange &a, const FreeRange &b) {
|
||||||
|
return a.base < b.base;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<FreeRange> merged;
|
||||||
|
merged.reserve(_free_ranges.size());
|
||||||
|
|
||||||
|
FreeRange cur = _free_ranges.front();
|
||||||
|
for (size_t i = 1; i < _free_ranges.size(); ++i)
|
||||||
|
{
|
||||||
|
const FreeRange &n = _free_ranges[i];
|
||||||
|
const uint32_t cur_end = cur.base + cur.count;
|
||||||
|
if (n.base <= cur_end)
|
||||||
|
{
|
||||||
|
const uint32_t n_end = n.base + n.count;
|
||||||
|
cur.count = std::max(cur_end, n_end) - cur.base;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
merged.push_back(cur);
|
||||||
|
cur = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.push_back(cur);
|
||||||
|
_free_ranges = std::move(merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParticlePass::free_range(uint32_t base, uint32_t count)
|
||||||
|
{
|
||||||
|
if (count == 0) return;
|
||||||
|
_free_ranges.push_back(FreeRange{base, count});
|
||||||
|
merge_free_ranges();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ParticlePass::create_system(uint32_t count)
|
||||||
|
{
|
||||||
|
if (count == 0) return 0;
|
||||||
|
count = std::min(count, free_particles());
|
||||||
|
|
||||||
|
uint32_t base = 0;
|
||||||
|
if (!allocate_range(count, base)) return 0;
|
||||||
|
|
||||||
|
System s{};
|
||||||
|
s.id = _next_system_id++;
|
||||||
|
s.base = base;
|
||||||
|
s.count = count;
|
||||||
|
s.enabled = true;
|
||||||
|
s.reset = true;
|
||||||
|
s.blend = BlendMode::Additive;
|
||||||
|
s.params = Params{};
|
||||||
|
|
||||||
|
_systems.push_back(s);
|
||||||
|
return s.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParticlePass::destroy_system(uint32_t id)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < _systems.size(); ++i)
|
||||||
|
{
|
||||||
|
if (_systems[i].id != id) continue;
|
||||||
|
free_range(_systems[i].base, _systems[i].count);
|
||||||
|
_systems.erase(_systems.begin() + static_cast<std::ptrdiff_t>(i));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParticlePass::resize_system(uint32_t id, uint32_t new_count)
|
||||||
|
{
|
||||||
|
System *s = find_system(id);
|
||||||
|
if (!s) return false;
|
||||||
|
if (new_count == s->count) return true;
|
||||||
|
if (new_count == 0) return destroy_system(id);
|
||||||
|
|
||||||
|
const uint32_t old_base = s->base;
|
||||||
|
const uint32_t old_count = s->count;
|
||||||
|
|
||||||
|
// Shrink in place: keep base and return the tail to the freelist.
|
||||||
|
if (new_count < old_count)
|
||||||
|
{
|
||||||
|
const uint32_t tail_base = old_base + new_count;
|
||||||
|
const uint32_t tail_count = old_count - new_count;
|
||||||
|
s->count = new_count;
|
||||||
|
free_range(tail_base, tail_count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to grow in place if the next range is free.
|
||||||
|
const uint32_t extra = new_count - old_count;
|
||||||
|
const uint32_t want_base = old_base + old_count;
|
||||||
|
for (auto it = _free_ranges.begin(); it != _free_ranges.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->base != want_base) continue;
|
||||||
|
if (it->count < extra) break;
|
||||||
|
|
||||||
|
it->base += extra;
|
||||||
|
it->count -= extra;
|
||||||
|
if (it->count == 0) _free_ranges.erase(it);
|
||||||
|
|
||||||
|
s->count = new_count;
|
||||||
|
s->reset = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: allocate a new range and recycle the old one.
|
||||||
|
uint32_t base = 0;
|
||||||
|
if (!allocate_range(new_count, base)) return false;
|
||||||
|
|
||||||
|
s->base = base;
|
||||||
|
s->count = new_count;
|
||||||
|
s->reset = true;
|
||||||
|
|
||||||
|
free_range(old_base, old_count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParticlePass::register_graph(RenderGraph *graph, RGImageHandle hdrTarget, RGImageHandle depthHandle)
|
||||||
|
{
|
||||||
|
if (!graph || !_context || !_context->pipelines || _particle_pool.buffer == VK_NULL_HANDLE ||
|
||||||
|
!hdrTarget.valid() || !depthHandle.valid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-frame dt/time and floating-origin delta
|
||||||
|
_dt_sec = 0.0f;
|
||||||
|
if (_context->scene)
|
||||||
|
{
|
||||||
|
_dt_sec = _context->scene->getDeltaTime();
|
||||||
|
}
|
||||||
|
if (!std::isfinite(_dt_sec)) _dt_sec = 0.0f;
|
||||||
|
_dt_sec = std::clamp(_dt_sec, 0.0f, 0.1f);
|
||||||
|
_time_sec += _dt_sec;
|
||||||
|
|
||||||
|
_origin_delta_local = glm::vec3(0.0f);
|
||||||
|
if (_context->scene)
|
||||||
|
{
|
||||||
|
WorldVec3 origin_world = _context->scene->get_world_origin();
|
||||||
|
if (_has_prev_origin)
|
||||||
|
{
|
||||||
|
const WorldVec3 delta_world = origin_world - _prev_origin_world;
|
||||||
|
_origin_delta_local = glm::vec3(delta_world);
|
||||||
|
}
|
||||||
|
_prev_origin_world = origin_world;
|
||||||
|
_has_prev_origin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool any_active = false;
|
||||||
|
for (const auto &s : _systems)
|
||||||
|
{
|
||||||
|
if (s.enabled && s.count > 0)
|
||||||
|
{
|
||||||
|
any_active = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any_active) return;
|
||||||
|
|
||||||
|
VkBuffer pool = _particle_pool.buffer;
|
||||||
|
VkDeviceSize poolSize = _particle_pool_size;
|
||||||
|
|
||||||
|
graph->add_pass(
|
||||||
|
"Particles.Update",
|
||||||
|
RGPassType::Compute,
|
||||||
|
[pool, poolSize](RGPassBuilder &builder, EngineContext *) {
|
||||||
|
builder.write_buffer(pool, RGBufferUsage::StorageReadWrite, poolSize, "particles.pool");
|
||||||
|
},
|
||||||
|
[this](VkCommandBuffer cmd, const RGPassResources &, EngineContext *ctx) {
|
||||||
|
EngineContext *ctxLocal = ctx ? ctx : _context;
|
||||||
|
if (!ctxLocal || !ctxLocal->pipelines) return;
|
||||||
|
|
||||||
|
for (auto &sys : _systems)
|
||||||
|
{
|
||||||
|
if (!sys.enabled || sys.count == 0) continue;
|
||||||
|
|
||||||
|
ParticleUpdatePushConstants pc{};
|
||||||
|
pc.header = glm::uvec4(sys.base, sys.count, sys.reset ? 1u : 0u, 0u);
|
||||||
|
|
||||||
|
float minSpeed = sys.params.min_speed;
|
||||||
|
float maxSpeed = sys.params.max_speed;
|
||||||
|
if (!std::isfinite(minSpeed)) minSpeed = 0.0f;
|
||||||
|
if (!std::isfinite(maxSpeed)) maxSpeed = 0.0f;
|
||||||
|
if (maxSpeed < minSpeed) std::swap(minSpeed, maxSpeed);
|
||||||
|
|
||||||
|
float minLife = sys.params.min_life;
|
||||||
|
float maxLife = sys.params.max_life;
|
||||||
|
if (!std::isfinite(minLife)) minLife = 0.1f;
|
||||||
|
if (!std::isfinite(maxLife)) maxLife = 0.1f;
|
||||||
|
if (maxLife < minLife) std::swap(minLife, maxLife);
|
||||||
|
|
||||||
|
float minSize = sys.params.min_size;
|
||||||
|
float maxSize = sys.params.max_size;
|
||||||
|
if (!std::isfinite(minSize)) minSize = 0.01f;
|
||||||
|
if (!std::isfinite(maxSize)) maxSize = 0.01f;
|
||||||
|
if (maxSize < minSize) std::swap(minSize, maxSize);
|
||||||
|
|
||||||
|
const float radius = clamp_nonnegative(sys.params.spawn_radius);
|
||||||
|
const float drag = clamp_nonnegative(sys.params.drag);
|
||||||
|
const float gravity = sys.params.gravity;
|
||||||
|
|
||||||
|
pc.sim = glm::vec4(_dt_sec, _time_sec, drag, gravity);
|
||||||
|
pc.origin_delta = glm::vec4(_origin_delta_local, 0.0f);
|
||||||
|
pc.emitter_pos_radius = glm::vec4(sys.params.emitter_pos_local, radius);
|
||||||
|
|
||||||
|
glm::vec3 dir = safe_normalize(sys.params.emitter_dir_local, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||||
|
const float coneRad = glm::radians(sys.params.cone_angle_degrees);
|
||||||
|
pc.emitter_dir_cone = glm::vec4(dir, coneRad);
|
||||||
|
|
||||||
|
pc.ranges = glm::vec4(minSpeed, maxSpeed, minLife, maxLife);
|
||||||
|
pc.size_range = glm::vec4(minSize, maxSize, 0.0f, 0.0f);
|
||||||
|
pc.color = sys.params.color;
|
||||||
|
|
||||||
|
ComputeDispatchInfo di{};
|
||||||
|
di.groupCountX = ComputeManager::calculateGroupCount(sys.count, k_local_size_x);
|
||||||
|
di.groupCountY = 1;
|
||||||
|
di.groupCountZ = 1;
|
||||||
|
di.pushConstants = &pc;
|
||||||
|
di.pushConstantSize = sizeof(pc);
|
||||||
|
|
||||||
|
ctxLocal->pipelines->dispatchComputeInstance(cmd, "particles.update", di);
|
||||||
|
|
||||||
|
sys.reset = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
graph->add_pass(
|
||||||
|
"Particles.Render",
|
||||||
|
RGPassType::Graphics,
|
||||||
|
[pool, poolSize, hdrTarget, depthHandle](RGPassBuilder &builder, EngineContext *) {
|
||||||
|
builder.read_buffer(pool, RGBufferUsage::StorageRead, poolSize, "particles.pool");
|
||||||
|
builder.write_color(hdrTarget);
|
||||||
|
builder.write_depth(depthHandle, false /*load existing depth*/);
|
||||||
|
},
|
||||||
|
[this](VkCommandBuffer cmd, const RGPassResources &, EngineContext *ctx) {
|
||||||
|
EngineContext *ctxLocal = ctx ? ctx : _context;
|
||||||
|
if (!ctxLocal || !ctxLocal->currentFrame) return;
|
||||||
|
|
||||||
|
ResourceManager *rm = ctxLocal->getResources();
|
||||||
|
DeviceManager *dev = ctxLocal->getDevice();
|
||||||
|
DescriptorManager *layouts = ctxLocal->getDescriptorLayouts();
|
||||||
|
PipelineManager *pipes = ctxLocal->pipelines;
|
||||||
|
if (!rm || !dev || !layouts || !pipes || _particle_set_layout == VK_NULL_HANDLE) return;
|
||||||
|
|
||||||
|
// Per-frame SceneData UBO (set=0 binding=0)
|
||||||
|
AllocatedBuffer sceneBuf = rm->create_buffer(sizeof(GPUSceneData),
|
||||||
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||||
|
VMA_MEMORY_USAGE_CPU_TO_GPU);
|
||||||
|
ctxLocal->currentFrame->_deletionQueue.push_function([rm, sceneBuf]() { rm->destroy_buffer(sceneBuf); });
|
||||||
|
|
||||||
|
VmaAllocationInfo ai{};
|
||||||
|
vmaGetAllocationInfo(dev->allocator(), sceneBuf.allocation, &ai);
|
||||||
|
*reinterpret_cast<GPUSceneData *>(ai.pMappedData) = ctxLocal->getSceneData();
|
||||||
|
vmaFlushAllocation(dev->allocator(), sceneBuf.allocation, 0, sizeof(GPUSceneData));
|
||||||
|
|
||||||
|
VkDescriptorSet globalSet = ctxLocal->currentFrame->_frameDescriptors.allocate(
|
||||||
|
dev->device(), layouts->gpuSceneDataLayout());
|
||||||
|
{
|
||||||
|
DescriptorWriter w;
|
||||||
|
w.write_buffer(0, sceneBuf.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
|
||||||
|
w.update_set(dev->device(), globalSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Particle pool descriptor (set=1 binding=0)
|
||||||
|
VkDescriptorSet particleSet = ctxLocal->currentFrame->_frameDescriptors.allocate(
|
||||||
|
dev->device(), _particle_set_layout);
|
||||||
|
{
|
||||||
|
DescriptorWriter w;
|
||||||
|
w.write_buffer(0, _particle_pool.buffer, _particle_pool_size, 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
|
||||||
|
w.update_set(dev->device(), particleSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkExtent2D extent = ctxLocal->getDrawExtent();
|
||||||
|
VkViewport vp{0.0f, 0.0f, float(extent.width), float(extent.height), 0.0f, 1.0f};
|
||||||
|
VkRect2D sc{{0, 0}, extent};
|
||||||
|
vkCmdSetViewport(cmd, 0, 1, &vp);
|
||||||
|
vkCmdSetScissor(cmd, 0, 1, &sc);
|
||||||
|
|
||||||
|
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||||
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
|
BlendMode boundBlend = BlendMode::Additive;
|
||||||
|
bool hasBound = false;
|
||||||
|
|
||||||
|
auto bind_pipeline = [&](BlendMode blend) {
|
||||||
|
const char *name = (blend == BlendMode::Alpha) ? "particles.alpha" : "particles.additive";
|
||||||
|
if (!pipes->getGraphics(name, pipeline, layout)) return false;
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &globalSet, 0, nullptr);
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 1, 1, &particleSet, 0, nullptr);
|
||||||
|
boundBlend = blend;
|
||||||
|
hasBound = true;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &sys : _systems)
|
||||||
|
{
|
||||||
|
if (!sys.enabled || sys.count == 0) continue;
|
||||||
|
if (!hasBound || sys.blend != boundBlend)
|
||||||
|
{
|
||||||
|
if (!bind_pipeline(sys.blend)) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instanced quad draw. gl_InstanceIndex includes firstInstance, so it becomes the particle index.
|
||||||
|
vkCmdDraw(cmd, 6, sys.count, 0, sys.base);
|
||||||
|
if (ctxLocal->stats)
|
||||||
|
{
|
||||||
|
ctxLocal->stats->drawcall_count += 1;
|
||||||
|
ctxLocal->stats->triangle_count += static_cast<int>(sys.count) * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
105
src/render/passes/particles.h
Normal file
105
src/render/passes/particles.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/world.h"
|
||||||
|
#include "render/graph/types.h"
|
||||||
|
#include "render/renderpass.h"
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class RenderGraph;
|
||||||
|
|
||||||
|
class ParticlePass : public IRenderPass
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr uint32_t k_max_particles = 128u * 1024u;
|
||||||
|
|
||||||
|
enum class BlendMode : uint32_t
|
||||||
|
{
|
||||||
|
Additive = 0,
|
||||||
|
Alpha = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Params
|
||||||
|
{
|
||||||
|
glm::vec3 emitter_pos_local{0.0f, 0.0f, 0.0f};
|
||||||
|
float spawn_radius{0.1f};
|
||||||
|
|
||||||
|
glm::vec3 emitter_dir_local{0.0f, 1.0f, 0.0f};
|
||||||
|
float cone_angle_degrees{20.0f};
|
||||||
|
|
||||||
|
float min_speed{2.0f};
|
||||||
|
float max_speed{8.0f};
|
||||||
|
|
||||||
|
float min_life{0.5f};
|
||||||
|
float max_life{1.5f};
|
||||||
|
|
||||||
|
float min_size{0.05f};
|
||||||
|
float max_size{0.15f};
|
||||||
|
|
||||||
|
float drag{1.0f};
|
||||||
|
float gravity{0.0f}; // positive pulls down -Y in local space
|
||||||
|
|
||||||
|
glm::vec4 color{1.0f, 0.5f, 0.1f, 1.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct System
|
||||||
|
{
|
||||||
|
uint32_t id{0};
|
||||||
|
uint32_t base{0};
|
||||||
|
uint32_t count{0};
|
||||||
|
bool enabled{true};
|
||||||
|
bool reset{true};
|
||||||
|
BlendMode blend{BlendMode::Additive};
|
||||||
|
Params params{};
|
||||||
|
};
|
||||||
|
|
||||||
|
void init(EngineContext *context) override;
|
||||||
|
void cleanup() override;
|
||||||
|
void execute(VkCommandBuffer cmd) override;
|
||||||
|
const char *getName() const override { return "Particles"; }
|
||||||
|
|
||||||
|
void register_graph(RenderGraph *graph, RGImageHandle hdrTarget, RGImageHandle depthHandle);
|
||||||
|
|
||||||
|
uint32_t create_system(uint32_t count);
|
||||||
|
bool destroy_system(uint32_t id);
|
||||||
|
bool resize_system(uint32_t id, uint32_t new_count);
|
||||||
|
|
||||||
|
std::vector<System> &systems() { return _systems; }
|
||||||
|
const std::vector<System> &systems() const { return _systems; }
|
||||||
|
|
||||||
|
uint32_t allocated_particles() const;
|
||||||
|
uint32_t free_particles() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct FreeRange
|
||||||
|
{
|
||||||
|
uint32_t base{0};
|
||||||
|
uint32_t count{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
bool allocate_range(uint32_t count, uint32_t &out_base);
|
||||||
|
void free_range(uint32_t base, uint32_t count);
|
||||||
|
void merge_free_ranges();
|
||||||
|
|
||||||
|
System *find_system(uint32_t id);
|
||||||
|
|
||||||
|
EngineContext *_context = nullptr;
|
||||||
|
|
||||||
|
AllocatedBuffer _particle_pool{};
|
||||||
|
VkDeviceSize _particle_pool_size = 0;
|
||||||
|
|
||||||
|
VkDescriptorSetLayout _particle_set_layout = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
uint32_t _next_system_id = 1;
|
||||||
|
std::vector<System> _systems;
|
||||||
|
std::vector<FreeRange> _free_ranges;
|
||||||
|
|
||||||
|
float _time_sec = 0.0f;
|
||||||
|
float _dt_sec = 0.0f;
|
||||||
|
glm::vec3 _origin_delta_local{0.0f};
|
||||||
|
|
||||||
|
WorldVec3 _prev_origin_world{0.0, 0.0, 0.0};
|
||||||
|
bool _has_prev_origin = false;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "passes/imgui_pass.h"
|
#include "passes/imgui_pass.h"
|
||||||
#include "passes/lighting.h"
|
#include "passes/lighting.h"
|
||||||
#include "passes/ssr.h"
|
#include "passes/ssr.h"
|
||||||
|
#include "passes/particles.h"
|
||||||
#include "passes/fxaa.h"
|
#include "passes/fxaa.h"
|
||||||
#include "passes/transparent.h"
|
#include "passes/transparent.h"
|
||||||
#include "passes/tonemap.h"
|
#include "passes/tonemap.h"
|
||||||
@@ -36,6 +37,11 @@ void RenderPassManager::init(EngineContext *context)
|
|||||||
ssrPass->init(context);
|
ssrPass->init(context);
|
||||||
addPass(std::move(ssrPass));
|
addPass(std::move(ssrPass));
|
||||||
|
|
||||||
|
// GPU particle system (compute update + render)
|
||||||
|
auto particlePass = std::make_unique<ParticlePass>();
|
||||||
|
particlePass->init(context);
|
||||||
|
addPass(std::move(particlePass));
|
||||||
|
|
||||||
// Post-process AA (FXAA-like) after tonemapping.
|
// Post-process AA (FXAA-like) after tonemapping.
|
||||||
auto fxaaPass = std::make_unique<FxaaPass>();
|
auto fxaaPass = std::make_unique<FxaaPass>();
|
||||||
fxaaPass->init(context);
|
fxaaPass->init(context);
|
||||||
|
|||||||
Reference in New Issue
Block a user