Files
Noita-GPU-Vulkan/shaders/fx_state.glsl
T
Gaël 00a9c1c26c Lot of bugfixes
* Adding clipper to generate the terrain's negative powder-contour offset.
* Fixing RDB issues (fracture, active trigger, performances)
* Fixing blood emission
* Fixing the freezing formula on liquids
* Fixing Steve spawning due to a missed test.
* Reaction table properly build
2026-10-21 20:46:13 +02:00

130 lines
8.6 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) coherent 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];
};
// Integer alias of the same storage, solely for atomically claiming a particle's
// life word. A plain float test allowed concurrent searches to return the same slot.
layout(std430, set = 0, binding = 36) coherent buffer FxStateAtomic { uint fxwords[]; };
// 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.
#include "particle_alloc.glsl"
// 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; c.z = material + 1 supplies the stain colour. It never owns or deposits a cell.
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;
}