#[compute] #version 450 #VERSION_DEFINES layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; #define SDF_MAX_LENGTH 16384.0 /* SET 0: GLOBAL DATA */ #include "samplers_inc.glsl" layout(set = 0, binding = 2, std430) restrict readonly buffer GlobalShaderUniformData { vec4 data[]; } global_shader_uniforms; /* Set 1: FRAME AND PARTICLE DATA */ // a frame history is kept for trail deterministic behavior #define MAX_ATTRACTORS 32 #define ATTRACTOR_TYPE_SPHERE 0 #define ATTRACTOR_TYPE_BOX 1 #define ATTRACTOR_TYPE_VECTOR_FIELD 2 struct Attractor { mat4 transform; vec3 extents; //exents or radius uint type; uint texture_index; //texture index for vector field float strength; float attenuation; float directionality; }; #define MAX_COLLIDERS 32 #define COLLIDER_TYPE_SPHERE 0 #define COLLIDER_TYPE_BOX 1 #define COLLIDER_TYPE_SDF 2 #define COLLIDER_TYPE_HEIGHT_FIELD 3 #define COLLIDER_TYPE_2D_SDF 4 struct Collider { mat4 transform; vec3 extents; //exents or radius uint type; uint texture_index; //texture index for vector field float scale; uint pad[2]; }; struct FrameParams { bool emitting; float system_phase; float prev_system_phase; uint cycle; float explosiveness; float randomness; float time; float delta; uint frame; float amount_ratio; uint pad1; uint pad2; uint random_seed; uint attractor_count; uint collider_count; float particle_size; mat4 emission_transform; vec3 emitter_velocity; float interp_to_end; Attractor attractors[MAX_ATTRACTORS]; Collider colliders[MAX_COLLIDERS]; }; layout(set = 1, binding = 0, std430) restrict buffer FrameHistory { FrameParams data[]; } frame_history; #define PARTICLE_FLAG_ACTIVE uint(1) #define PARTICLE_FLAG_STARTED uint(2) #define PARTICLE_FLAG_TRAILED uint(4) #define PARTICLE_FRAME_MASK uint(0xFFFF) #define PARTICLE_FRAME_SHIFT uint(16) struct ParticleData { mat4 xform; vec3 velocity; uint flags; vec4 color; vec4 custom; #ifdef USERDATA1_USED vec4 userdata1; #endif #ifdef USERDATA2_USED vec4 userdata2; #endif #ifdef USERDATA3_USED vec4 userdata3; #endif #ifdef USERDATA4_USED vec4 userdata4; #endif #ifdef USERDATA5_USED vec4 userdata5; #endif #ifdef USERDATA6_USED vec4 userdata6; #endif }; layout(set = 1, binding = 1, std430) restrict buffer Particles { ParticleData data[]; } particles; #define EMISSION_FLAG_HAS_POSITION 1 #define EMISSION_FLAG_HAS_ROTATION_SCALE 2 #define EMISSION_FLAG_HAS_VELOCITY 4 #define EMISSION_FLAG_HAS_COLOR 8 #define EMISSION_FLAG_HAS_CUSTOM 16 struct ParticleEmission { mat4 xform; vec3 velocity; uint flags; vec4 color; vec4 custom; }; layout(set = 1, binding = 2, std430) restrict buffer SourceEmission { int particle_count; uint pad0; uint pad1; uint pad2; ParticleEmission data[]; } src_particles; layout(set = 1, binding = 3, std430) restrict buffer DestEmission { int particle_count; int particle_max; uint pad1; uint pad2; ParticleEmission data[]; } dst_particles; /* SET 2: COLLIDER/ATTRACTOR TEXTURES */ #define MAX_3D_TEXTURES 7 layout(set = 2, binding = 0) uniform texture3D sdf_vec_textures[MAX_3D_TEXTURES]; layout(set = 2, binding = 1) uniform texture2D height_field_texture; /* SET 3: MATERIAL */ #ifdef MATERIAL_UNIFORMS_USED /* clang-format off */ layout(set = 3, binding = 0, std140) uniform MaterialUniforms { #MATERIAL_UNIFORMS } material; /* clang-format on */ #endif layout(push_constant, std430) uniform Params { float lifetime; bool clear; uint total_particles; uint trail_size; bool use_fractional_delta; bool sub_emitter_mode; bool can_emit; bool trail_pass; } params; uint hash(uint x) { x = ((x >> uint(16)) ^ x) * uint(0x45d9f3b); x = ((x >> uint(16)) ^ x) * uint(0x45d9f3b); x = (x >> uint(16)) ^ x; return x; } bool emit_subparticle(mat4 p_xform, vec3 p_velocity, vec4 p_color, vec4 p_custom, uint p_flags) { if (!params.can_emit) { return false; } bool valid = false; int dst_index = atomicAdd(dst_particles.particle_count, 1); if (dst_index >= dst_particles.particle_max) { atomicAdd(dst_particles.particle_count, -1); return false; } dst_particles.data[dst_index].xform = p_xform; dst_particles.data[dst_index].velocity = p_velocity; dst_particles.data[dst_index].color = p_color; dst_particles.data[dst_index].custom = p_custom; dst_particles.data[dst_index].flags = p_flags; return true; } vec3 safe_normalize(vec3 direction) { const float EPSILON = 0.001; if (length(direction) < EPSILON) { return vec3(0.0); } return normalize(direction); } #GLOBALS void main() { uint particle = gl_GlobalInvocationID.x; if (params.trail_size > 1) { if (params.trail_pass) { if (particle >= params.total_particles * (params.trail_size - 1)) { return; } particle += (particle / (params.trail_size - 1)) + 1; } else { if (particle >= params.total_particles) { return; } particle *= params.trail_size; } } if (particle >= params.total_particles * params.trail_size) { return; //discard } uint index = particle / params.trail_size; uint frame = (particle % params.trail_size); #define FRAME frame_history.data[frame] #define PARTICLE particles.data[particle] bool apply_forces = true; bool apply_velocity = true; float local_delta = FRAME.delta; float mass = 1.0; bool restart = false; bool restart_position = false; bool restart_rotation_scale = false; bool restart_velocity = false; bool restart_color = false; bool restart_custom = false; if (params.clear) { PARTICLE.color = vec4(1.0); PARTICLE.custom = vec4(0.0); PARTICLE.velocity = vec3(0.0); PARTICLE.flags = 0; PARTICLE.xform = mat4( vec4(1.0, 0.0, 0.0, 0.0), vec4(0.0, 1.0, 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 1.0)); } //clear started flag if set if (params.trail_pass) { //trail started uint src_idx = index * params.trail_size; if (bool(particles.data[src_idx].flags & PARTICLE_FLAG_STARTED)) { //save start conditions for trails PARTICLE.color = particles.data[src_idx].color; PARTICLE.custom = particles.data[src_idx].custom; PARTICLE.velocity = particles.data[src_idx].velocity; PARTICLE.flags = PARTICLE_FLAG_TRAILED | ((frame_history.data[0].frame & PARTICLE_FRAME_MASK) << PARTICLE_FRAME_SHIFT); //mark it as trailed, save in which frame it will start PARTICLE.xform = particles.data[src_idx].xform; #ifdef USERDATA1_USED PARTICLE.userdata1 = particles.data[src_idx].userdata1; #endif #ifdef USERDATA2_USED PARTICLE.userdata2 = particles.data[src_idx].userdata2; #endif #ifdef USERDATA3_USED PARTICLE.userdata3 = particles.data[src_idx].userdata3; #endif #ifdef USERDATA4_USED PARTICLE.userdata4 = particles.data[src_idx].userdata4; #endif #ifdef USERDATA5_USED PARTICLE.userdata5 = particles.data[src_idx].userdata5; #endif #ifdef USERDATA6_USED PARTICLE.userdata6 = particles.data[src_idx].userdata6; #endif } if (!bool(particles.data[src_idx].flags & PARTICLE_FLAG_ACTIVE)) { // Disable the entire trail if the parent is no longer active. PARTICLE.flags = 0; return; } if (bool(PARTICLE.flags & PARTICLE_FLAG_TRAILED) && ((PARTICLE.flags >> PARTICLE_FRAME_SHIFT) == (FRAME.frame & PARTICLE_FRAME_MASK))) { //check this is trailed and see if it should start now // we just assume that this is the first frame of the particle, the rest is deterministic PARTICLE.flags = PARTICLE_FLAG_ACTIVE | (particles.data[src_idx].flags & (PARTICLE_FRAME_MASK << PARTICLE_FRAME_SHIFT)); return; //- this appears like it should be correct, but it seems not to be.. wonder why. } } else { PARTICLE.flags &= ~PARTICLE_FLAG_STARTED; } bool collided = false; vec3 collision_normal = vec3(0.0); float collision_depth = 0.0; vec3 attractor_force = vec3(0.0); #if !defined(DISABLE_VELOCITY) if (bool(PARTICLE.flags & PARTICLE_FLAG_ACTIVE)) { PARTICLE.xform[3].xyz += PARTICLE.velocity * local_delta; } #endif if (!params.trail_pass && params.sub_emitter_mode) { if (!bool(PARTICLE.flags & PARTICLE_FLAG_ACTIVE)) { int src_index = atomicAdd(src_particles.particle_count, -1) - 1; if (src_index >= 0) { PARTICLE.flags = (PARTICLE_FLAG_ACTIVE | PARTICLE_FLAG_STARTED | (FRAME.cycle << PARTICLE_FRAME_SHIFT)); restart = true; if (bool(src_particles.data[src_index].flags & EMISSION_FLAG_HAS_POSITION)) { PARTICLE.xform[3] = src_particles.data[src_index].xform[3]; } else { PARTICLE.xform[3] = vec4(0, 0, 0, 1); restart_position = true; } if (bool(src_particles.data[src_index].flags & EMISSION_FLAG_HAS_ROTATION_SCALE)) { PARTICLE.xform[0] = src_particles.data[src_index].xform[0]; PARTICLE.xform[1] = src_particles.data[src_index].xform[1]; PARTICLE.xform[2] = src_particles.data[src_index].xform[2]; } else { PARTICLE.xform[0] = vec4(1, 0, 0, 0); PARTICLE.xform[1] = vec4(0, 1, 0, 0); PARTICLE.xform[2] = vec4(0, 0, 1, 0); restart_rotation_scale = true; } if (bool(src_particles.data[src_index].flags & EMISSION_FLAG_HAS_VELOCITY)) { PARTICLE.velocity = src_particles.data[src_index].velocity; } else { PARTICLE.velocity = vec3(0); restart_velocity = true; } if (bool(src_particles.data[src_index].flags & EMISSION_FLAG_HAS_COLOR)) { PARTICLE.color = src_particles.data[src_index].color; } else { PARTICLE.color = vec4(1); restart_color = true; } if (bool(src_particles.data[src_index].flags & EMISSION_FLAG_HAS_CUSTOM)) { PARTICLE.custom = src_particles.data[src_index].custom; } else { PARTICLE.custom = vec4(0); restart_custom = true; } } } } else if (FRAME.emitting) { float restart_phase = float(index) / float(params.total_particles); if (FRAME.randomness > 0.0) { uint seed = FRAME.cycle; if (restart_phase >= FRAME.system_phase) { seed -= uint(1); } seed *= uint(params.total_particles); seed += uint(index); float random = float(hash(seed) % uint(65536)) / 65536.0; restart_phase += FRAME.randomness * random * 1.0 / float(params.total_particles); } restart_phase *= (1.0 - FRAME.explosiveness); if (FRAME.system_phase > FRAME.prev_system_phase) { // restart_phase >= prev_system_phase is used so particles emit in the first frame they are processed if (restart_phase >= FRAME.prev_system_phase && restart_phase < FRAME.system_phase) { restart = true; if (params.use_fractional_delta) { local_delta = (FRAME.system_phase - restart_phase) * params.lifetime; } } } else if (FRAME.delta > 0.0) { if (restart_phase >= FRAME.prev_system_phase) { restart = true; if (params.use_fractional_delta) { local_delta = (1.0 - restart_phase + FRAME.system_phase) * params.lifetime; } } else if (restart_phase < FRAME.system_phase) { restart = true; if (params.use_fractional_delta) { local_delta = (FRAME.system_phase - restart_phase) * params.lifetime; } } } if (params.trail_pass) { restart = false; } if (restart) { PARTICLE.flags = FRAME.emitting ? (PARTICLE_FLAG_ACTIVE | PARTICLE_FLAG_STARTED | (FRAME.cycle << PARTICLE_FRAME_SHIFT)) : 0; restart_position = true; restart_rotation_scale = true; restart_velocity = true; restart_color = true; restart_custom = true; } } bool particle_active = bool(PARTICLE.flags & PARTICLE_FLAG_ACTIVE); uint particle_number = (PARTICLE.flags >> PARTICLE_FRAME_SHIFT) * uint(params.total_particles) + index; if (restart && particle_active) { #CODE : START } if (particle_active) { for (uint i = 0; i < FRAME.attractor_count; i++) { vec3 dir; float amount; vec3 rel_vec = PARTICLE.xform[3].xyz - FRAME.attractors[i].transform[3].xyz; vec3 local_pos = rel_vec * mat3(FRAME.attractors[i].transform); switch (FRAME.attractors[i].type) { case ATTRACTOR_TYPE_SPHERE: { dir = safe_normalize(rel_vec); float d = length(local_pos) / FRAME.attractors[i].extents.x; if (d > 1.0) { continue; } amount = max(0.0, 1.0 - d); } break; case ATTRACTOR_TYPE_BOX: { dir = safe_normalize(rel_vec); vec3 abs_pos = abs(local_pos / FRAME.attractors[i].extents); float d = max(abs_pos.x, max(abs_pos.y, abs_pos.z)); if (d > 1.0) { continue; } amount = max(0.0, 1.0 - d); } break; case ATTRACTOR_TYPE_VECTOR_FIELD: { vec3 uvw_pos = (local_pos / FRAME.attractors[i].extents + 1.0) * 0.5; if (any(lessThan(uvw_pos, vec3(0.0))) || any(greaterThan(uvw_pos, vec3(1.0)))) { continue; } vec3 s = texture(sampler3D(sdf_vec_textures[FRAME.attractors[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos).xyz * -2.0 + 1.0; dir = mat3(FRAME.attractors[i].transform) * safe_normalize(s); //revert direction amount = length(s); } break; } amount = pow(amount, FRAME.attractors[i].attenuation); dir = safe_normalize(mix(dir, FRAME.attractors[i].transform[2].xyz, FRAME.attractors[i].directionality)); attractor_force -= amount * dir * FRAME.attractors[i].strength; } float particle_size = FRAME.particle_size; #ifdef USE_COLLISION_SCALE particle_size *= dot(vec3(length(PARTICLE.xform[0].xyz), length(PARTICLE.xform[1].xyz), length(PARTICLE.xform[2].xyz)), vec3(0.33333333333)); #endif if (FRAME.collider_count == 1 && FRAME.colliders[0].type == COLLIDER_TYPE_2D_SDF) { //2D collision vec2 pos = PARTICLE.xform[3].xy; vec4 to_sdf_x = FRAME.colliders[0].transform[0]; vec4 to_sdf_y = FRAME.colliders[0].transform[1]; vec2 sdf_pos = vec2(dot(vec4(pos, 0, 1), to_sdf_x), dot(vec4(pos, 0, 1), to_sdf_y)); vec4 sdf_to_screen = vec4(FRAME.colliders[0].extents, FRAME.colliders[0].scale); vec2 uv_pos = sdf_pos * sdf_to_screen.xy + sdf_to_screen.zw; if (all(greaterThan(uv_pos, vec2(0.0))) && all(lessThan(uv_pos, vec2(1.0)))) { vec2 pos2 = pos + vec2(0, particle_size); vec2 sdf_pos2 = vec2(dot(vec4(pos2, 0, 1), to_sdf_x), dot(vec4(pos2, 0, 1), to_sdf_y)); float sdf_particle_size = distance(sdf_pos, sdf_pos2); float d = texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uv_pos).r * SDF_MAX_LENGTH; d -= sdf_particle_size; if (d < 0.0) { const float EPSILON = 0.001; vec2 n = normalize(vec2( texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uv_pos + vec2(EPSILON, 0.0)).r - texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uv_pos - vec2(EPSILON, 0.0)).r, texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uv_pos + vec2(0.0, EPSILON)).r - texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uv_pos - vec2(0.0, EPSILON)).r)); collided = true; sdf_pos2 = sdf_pos + n * d; pos2 = vec2(dot(vec4(sdf_pos2, 0, 1), FRAME.colliders[0].transform[2]), dot(vec4(sdf_pos2, 0, 1), FRAME.colliders[0].transform[3])); n = pos - pos2; collision_normal = normalize(vec3(n, 0.0)); collision_depth = length(n); } } } else { for (uint i = 0; i < FRAME.collider_count; i++) { vec3 normal; float depth; bool col = false; vec3 rel_vec = PARTICLE.xform[3].xyz - FRAME.colliders[i].transform[3].xyz; vec3 local_pos = rel_vec * mat3(FRAME.colliders[i].transform); // Allowing for a small epsilon to allow particle just touching colliders to count as collided const float EPSILON = 0.001; switch (FRAME.colliders[i].type) { case COLLIDER_TYPE_SPHERE: { float d = length(rel_vec) - (particle_size + FRAME.colliders[i].extents.x); if (d <= EPSILON) { col = true; depth = -d; normal = normalize(rel_vec); } } break; case COLLIDER_TYPE_BOX: { vec3 abs_pos = abs(local_pos); vec3 sgn_pos = sign(local_pos); if (any(greaterThan(abs_pos, FRAME.colliders[i].extents))) { //point outside box vec3 closest = min(abs_pos, FRAME.colliders[i].extents); vec3 rel = abs_pos - closest; depth = length(rel) - particle_size; if (depth <= EPSILON) { col = true; normal = mat3(FRAME.colliders[i].transform) * (normalize(rel) * sgn_pos); depth = -depth; } } else { //point inside box vec3 axis_len = FRAME.colliders[i].extents - abs_pos; // there has to be a faster way to do this? if (all(lessThan(axis_len.xx, axis_len.yz))) { normal = vec3(1, 0, 0); } else if (all(lessThan(axis_len.yy, axis_len.xz))) { normal = vec3(0, 1, 0); } else { normal = vec3(0, 0, 1); } col = true; depth = dot(normal * axis_len, vec3(1)) + particle_size; normal = mat3(FRAME.colliders[i].transform) * (normal * sgn_pos); } } break; case COLLIDER_TYPE_SDF: { vec3 apos = abs(local_pos); float extra_dist = 0.0; if (any(greaterThan(apos, FRAME.colliders[i].extents))) { //outside vec3 mpos = min(apos, FRAME.colliders[i].extents); extra_dist = distance(mpos, apos); } if (extra_dist > particle_size) { continue; } vec3 uvw_pos = (local_pos / FRAME.colliders[i].extents) * 0.5 + 0.5; float s = texture(sampler3D(sdf_vec_textures[FRAME.colliders[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos).r; s *= FRAME.colliders[i].scale; s += extra_dist; if (s <= particle_size + EPSILON) { col = true; depth = particle_size - s; normal = mat3(FRAME.colliders[i].transform) * normalize( vec3( texture(sampler3D(sdf_vec_textures[FRAME.colliders[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos + vec3(EPSILON, 0.0, 0.0)).r - texture(sampler3D(sdf_vec_textures[FRAME.colliders[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos - vec3(EPSILON, 0.0, 0.0)).r, texture(sampler3D(sdf_vec_textures[FRAME.colliders[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos + vec3(0.0, EPSILON, 0.0)).r - texture(sampler3D(sdf_vec_textures[FRAME.colliders[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos - vec3(0.0, EPSILON, 0.0)).r, texture(sampler3D(sdf_vec_textures[FRAME.colliders[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos + vec3(0.0, 0.0, EPSILON)).r - texture(sampler3D(sdf_vec_textures[FRAME.colliders[i].texture_index], SAMPLER_LINEAR_CLAMP), uvw_pos - vec3(0.0, 0.0, EPSILON)).r)); } } break; case COLLIDER_TYPE_HEIGHT_FIELD: { vec3 local_pos_bottom = local_pos; local_pos_bottom.y -= particle_size; if (any(greaterThan(abs(local_pos_bottom), FRAME.colliders[i].extents))) { continue; } const float DELTA = 1.0 / 8192.0; vec3 uvw_pos = vec3(local_pos_bottom / FRAME.colliders[i].extents) * 0.5 + 0.5; float y = texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz).r; if (y + EPSILON >= uvw_pos.y) { //inside heightfield vec3 pos1 = (vec3(uvw_pos.x, y, uvw_pos.z) * 2.0 - 1.0) * FRAME.colliders[i].extents; vec3 pos2 = (vec3(uvw_pos.x + DELTA, texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz + vec2(DELTA, 0)).r, uvw_pos.z) * 2.0 - 1.0) * FRAME.colliders[i].extents; vec3 pos3 = (vec3(uvw_pos.x, texture(sampler2D(height_field_texture, SAMPLER_LINEAR_CLAMP), uvw_pos.xz + vec2(0, DELTA)).r, uvw_pos.z + DELTA) * 2.0 - 1.0) * FRAME.colliders[i].extents; normal = normalize(cross(pos1 - pos2, pos1 - pos3)); float local_y = (vec3(local_pos / FRAME.colliders[i].extents) * 0.5 + 0.5).y; col = true; depth = dot(normal, pos1) - dot(normal, local_pos_bottom); } } break; } if (col) { if (!collided) { collided = true; collision_normal = normal; collision_depth = depth; } else { vec3 c = collision_normal * collision_depth; c += normal * max(0.0, depth - dot(normal, c)); collision_normal = normalize(c); collision_depth = length(c); } } } } } if (particle_active) { #CODE : PROCESS } PARTICLE.flags &= ~PARTICLE_FLAG_ACTIVE; if (particle_active) { PARTICLE.flags |= PARTICLE_FLAG_ACTIVE; } }