ADD: Particle system

This commit is contained in:
2025-12-17 22:17:32 +09:00
parent fa0298e4c1
commit 4fea967bfc
11 changed files with 1060 additions and 10 deletions

26
shaders/particles.frag Normal file
View 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
View 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);
}

View 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;
}