134 lines
8.5 KiB
GLSL
134 lines
8.5 KiB
GLSL
// State of the GPU projectile / effect system (device local, all simulated by proj_*.comp and fx_*.comp).
|
|
const int FP_MAX = 32768, FPART_MAX = 16384, FANIM_MAX = 256, FANIM_ALL = 512; // (the GPU's ring is 0..255, the host writes 256..511)
|
|
// FProj.d.z bit 30: a CPU query materialized this instance; automatic Lua yields to its twin.
|
|
struct FProj { vec4 a; vec4 b; vec4 c; ivec4 d; vec4 e; vec4 f; }; // d.w: the cast serial (0 = dead; the host's twins match it), d.z bit 10 = friendly fire of the shot; f: the wand's damage_electricity_add, damage_ice_add, knockback_force add, trail material (material + 1 | amount << 12) // e: rotation angle, angular velocity (thrown bodies) // pos, vel / damage, life left, gravity, friction / explosion radius, damage, age, bounces / type, spec, fired mask + team, alive
|
|
struct FPart { vec4 a; vec4 b; uvec4 c; }; // pos, vel / life, max life, gravity, colour bits (rgb, bit 24 = fades)
|
|
struct FAnim { vec4 a; ivec4 b; vec4 c; }; // x, y, light radius, light strength / boom, age, max age, colour bits / angle
|
|
layout(std430, set = 0, binding = 36) buffer FxState {
|
|
uint fcnt[4]; // projectile ring, particle ring, animation ring
|
|
FProj fprojs[FP_MAX]; // (src/gun/gpu_fx.h kMaxProj)
|
|
FPart fparts[16384];
|
|
FAnim fanims[512];
|
|
};
|
|
|
|
// A free record (00717400 / 007174e0: the pools return null when they are full, a live particle is never replaced): the search starts at the ring position and takes the first dead
|
|
// record of the next 64; -1 when there is none.
|
|
int alloc_part() {
|
|
const uint s0 = atomicAdd(fcnt[1], 1u);
|
|
for (uint k = 0u; k < 64u; ++k) {
|
|
const uint s = (s0 + k) % uint(FPART_MAX);
|
|
if (fparts[s].b.x <= 0.0) return int(s);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// colour bits: rgb, bit 24 = fades with its life, bit 25 = additive. c.w of a cosmetic: bit 1 = no grid collision (collide_with_grid 0), bit 2 = the bounce is spent
|
|
// (00712010 sets the bounce flag 6a:20 on every record; fx_particles.comp)
|
|
int emit_particle_g(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive, bool collide) { // the record, -1 when the pool is full
|
|
const int si = alloc_part();
|
|
if (si < 0) return -1;
|
|
const uint s = uint(si);
|
|
fparts[s].a = vec4(pos, vel);
|
|
fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | (fade ? 0x01000000u : 0u) | (additive ? 0x02000000u : 0u)));
|
|
fparts[s].c = uvec4(0u, 0u, 0u, collide ? 0u : 2u); // c.z: 255 - the material's alpha (drawn opacity of a non-additive cosmetic)
|
|
return si;
|
|
}
|
|
// the cosmetic constructor 00716ce0 without force-create: the particle is made only where the grid slot is empty (and loaded)
|
|
bool cell_free_at(vec2 w) {
|
|
const ivec2 q = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y);
|
|
return q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cell_mat(cells[q.y * pc.gw + q.x]) == 0u;
|
|
}
|
|
void emit_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive) { emit_particle_g(pos, vel, life, grav, rgb, fade, additive, true); }
|
|
// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; with a material (c.z = material + 1) it leaves a cell of it there, so the spray feeds the pools.
|
|
uint emit_particle_hit(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // returns 0xFFFFFFFF when the pool is full
|
|
const int si = alloc_part();
|
|
if (si < 0) return 0xFFFFFFFFu;
|
|
const uint s = uint(si);
|
|
fparts[s].a = vec4(pos, vel);
|
|
fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x01000000u));
|
|
fparts[s].c = uvec4(1u, 0u, uint(material + 1), 0u);
|
|
return s;
|
|
}
|
|
// A particle that becomes a cell of the material when it lands (blood, acid... "real" particles): max life is -(material + 1).
|
|
// the acceleration of a particle record (00712010 copies 0115499c / 011549a0, which the particle manager resets to (0, pixels_per_physics_unit x 10) = (0, 100) px/s^2,
|
|
// 007128c0 / 00712bc0); the real particles (00716af0 -> 007168c0) keep it: 100 / 3600 cells per frame^2
|
|
const float PART_GRAV = 100.0 / 3600.0;
|
|
bool emit_material_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // false when the pool is full
|
|
const int si = alloc_part();
|
|
if (si < 0) return false;
|
|
const uint s = uint(si);
|
|
fparts[s].a = vec4(pos, vel);
|
|
fparts[s].b = vec4(life, -float(material + 1), grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x04000000u));
|
|
fparts[s].c = uvec4(0u); // a flag in the top byte keeps the bits a normal float (a zero top byte is a denormal and may be flushed)
|
|
return true;
|
|
}
|
|
|
|
// ---- liquid taken out of the grid by an entity, a projectile or a body (docs/DISPLACEMENT_AND_FIRE.md): the cell becomes a particle that keeps its identity
|
|
// (the whole cell word) and goes back into the grid when it has landed. c.y: 1 = mode 1 (moving up, passes liquids with a penetration budget c.z), 2 = mode 0
|
|
// (falls, stops at liquid or solid), 3 = waiting for a free place (retries every tick).
|
|
void emit_displaced(vec2 pos, vec2 vel, uint cellword, uint kind, uint birthv) { // birthv: the age (birth frame) of the cell travels with it (00717670 / 007168c0 hand over the same cell object)
|
|
const int si = alloc_part();
|
|
if (si < 0) return; // the pool is full: the cell is gone, as in the original (the caller cleared the grid slot first)
|
|
const uint s = uint(si);
|
|
fparts[s].a = vec4(pos, vel);
|
|
fparts[s].b = vec4(1.0e6, 1.0, uintBitsToFloat(birthv), 0.0);
|
|
fparts[s].c = uvec4(cellword, kind, kind == 1u ? 20u : 0u, (hash3(s, cellword, 77u) % 101u) < 50u ? 0x100000u : 0u); // bit 20: effective density zero (an inclusive draw below 50)
|
|
}
|
|
|
|
// Takes a liquid cell (not a powder) out of the grid and makes it a particle with the velocity v (cells per frame). Returns whether it did.
|
|
bool displace_cell(ivec2 w, vec2 v) {
|
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
|
int i = p.y * pc.gw + p.x;
|
|
uint c = cells[i];
|
|
uint bv = birth[i];
|
|
uint m = cell_mat(c);
|
|
if (m == 0u) return false;
|
|
uint f = mflags(m);
|
|
if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) return false;
|
|
if (atomicCompSwap(cells[i], c, 0u) != c) return false; // the grid slot is cleared before the particle exists
|
|
ext[i] = vec2(0.0);
|
|
emit_displaced(vec2(w) + 0.5, v, c, 1u, bv); // mode 1; it turns into mode 0 when it moves down (fx_particles.comp)
|
|
return true;
|
|
}
|
|
|
|
// Is the cell at the world position a liquid that counts for the drag (liquid, not a powder)?
|
|
bool liquid_nonsand(ivec2 w) {
|
|
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
|
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
|
|
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
|
if (m == 0u) return false;
|
|
uint f = mflags(m);
|
|
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
|
|
}
|
|
|
|
// 0071fba0: puts a cell back into the grid. Searches from (x, y) upward to y - 128: an empty cell accepts it, a non-solid cell that is lighter is exchanged (the
|
|
// lighter cell then looks for a place of its own: it becomes a waiting particle); equal density is rejected, a solid cell ends the search; after more than
|
|
// five steps a rejection may shift the column (20 %, then 50 % right / left).
|
|
bool reinsert(uint cw, ivec2 w, uint seed, uint birthv) {
|
|
float dens = mdensity(cell_mat(cw));
|
|
for (int step = 0; step <= 128; ++step) {
|
|
ivec2 t = ivec2(w.x, w.y - step);
|
|
ivec2 q = t - ivec2(pc.view_x, pc.view_y);
|
|
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; // not loaded: no random draw either
|
|
int ti = q.y * pc.gw + q.x;
|
|
uint occ = cells[ti];
|
|
uint om = cell_mat(occ);
|
|
if (om == 0u) {
|
|
if (atomicCompSwap(cells[ti], 0u, cw) == 0u) { ext[ti] = vec2(0.0); birth[ti] = birthv; return true; }
|
|
continue;
|
|
}
|
|
uint of = mflags(om);
|
|
if ((of & F_STATIC) != 0u) return false; // a solid cell stops the search
|
|
if (mdensity(om) < dens && atomicCompSwap(cells[ti], occ, cw) == occ) { // the lighter cell is queued, not destroyed
|
|
ext[ti] = vec2(0.0);
|
|
const uint obirth = birth[ti];
|
|
birth[ti] = birthv;
|
|
emit_displaced(vec2(t) + 0.5, vec2(0.0), occ, 3u, obirth);
|
|
return true;
|
|
}
|
|
if (step > 5 && (hash3(seed, uint(step), 61u) % 101u) < 20u) w.x += (hash3(seed, uint(step), 62u) % 101u) < 50u ? 1 : -1;
|
|
}
|
|
return false;
|
|
}
|