Files
Noita-GPU-Vulkan/shaders/explode_rays.comp
T
2026-10-19 20:59:15 +02:00

125 lines
8.6 KiB
Plaintext

#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 256) in;
#include "common.glsl"
// Terrain part of an explosion with angular attenuation (docs/DAMAGE_CONTACTS_AND_EXPLOSIONS.md, 00657960 / 00656b10): 360 bins; each bin is a ray from the centre that
// walks the cells, spends min(remaining energy, cell hp left) on every solid / powder cell, and ends at a cell whose durability is above the maximum or when the energy
// is gone. A cell within the reach of its bin loses the bin's damage from its hp and goes below 1 (the damage stays on static cells: two weak blasts break what
// one cannot, as the engine's per-cell hp). One workgroup per edit
// of mode 2 (e[2k] = (x, y, radius <= 64, max durability), e[2k+1] = (2, ray energy, 0, 0)); the rays read the grid before the workgroup destroys anything.
layout(std430, set = 0, binding = 27) readonly buffer Edits { ivec4 head; vec4 e[512]; } ed;
shared float reach2[360]; // the squared reach of each bin
shared float binDmg[360]; // the largest hp a ray spent on one cell (00656b10 *param_5)
float rnd01(uint h) { return float(h & 0xFFFFu) / 65536.0; }
uint cell_hp(uint m) { return 50u * max(1u, (mats[3u * m + 2u].z >> 20) & 0xFFFu); }
// the rays and the hole spend the hp of solid and powder cells only (cell vtable slot 23: solid, slot 25: CellData liquid_sand +0x160); liquids and gases are free
bool ray_cell(uint m) { const uint f = mflags(m); return (f & F_POWDER) != 0u || ((f & F_STATIC) != 0u && (f & F_LIQCELL) == 0u); }
// the hp a cell has left: the material hp (007048c0: cell+4 = CellData hp +0x158) minus the explosion damage it took. A static cell keeps that damage in bits 24-26 of its word
// (free there: the movement passes skip static cells), in eighths of its hp, so it is saved with the cell when the window moves
float cell_hp_left(int i, uint m) { return float(cell_hp(m)) * ((mflags(m) & F_STATIC) != 0u ? 1.0 - float((cells[i] >> 24) & 3u) / 4.0 : 1.0); }
void main() {
int k = int(gl_WorkGroupID.x);
if (k >= ed.head.x || k >= 256) return;
vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1];
if (r.x < 1.5 || r.x > 2.5) return;
const vec2 centre = q.xy;
const float R = min(q.z, 64.0);
if (!(R > 0.0)) return; // (a radius that is not a positive number - NaN, -inf: no rays; the rays' stop tests never hold for them)
const float maxDur = mod(q.w, 4096.0);
const float stainCode = floor(q.w / 4096.0); // 0: stains_enabled off, else 1 + stains_radius (gpu_fx.cpp)
// the burnt ring (00657960 + 0070e760): out to the radius + stains_radius (0: + 0.4 x the radius)
const float Rs = stainCode > 0.0 ? R + (stainCode > 1.0 ? stainCode - 1.0 : 0.4 * R) : R;
const uint rot = hash3(uint(k), uint(pc.frame), 41u) % 181u; // the random rotation of the roughness table
// 00656b10: a ray from the centre to the edge, unit steps, a cell sampled when the integer coordinates change; on a solid / powder cell spend = min(energy, its hp
// left), damage = max(damage, spend); it stops at a cell whose durability is above the maximum or when the energy is out: the reach is then the previous sample's
// squared distance (+1 on an even LCG draw), else the full radius
for (int bin = int(gl_LocalInvocationID.x); bin < 360; bin += 256) {
const float a = float(bin) * 6.2831853 / 360.0;
const vec2 dir = vec2(cos(a), sin(a));
float energy = r.y >= 0.0 ? r.y : 20000.0; // ray_energy of the data, else the constructor default 20000 (004b2780)
ivec2 last = ivec2(floor(centre));
float rr = R * R, dmg = 0.0, prevD2 = 0.0;
for (int s = 1; s <= 2048; ++s) {
const float d = float(s);
if (d * d >= R * R) break;
const ivec2 w = ivec2(floor(centre + dir * d));
if (w == last) continue;
last = w;
const 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) { rr = prevD2; break; }
const int i = p.y * pc.gw + p.x;
const uint m = cell_mat(cells[i]);
if (m != 0u && ray_cell(m)) {
const float spend = min(energy, max(cell_hp_left(i, m), 0.0));
energy -= spend;
dmg = max(dmg, spend);
if (uintBitsToFloat(mats[3u * m + 2u].y) > maxDur || energy <= 0.0) {
rr = prevD2 + ((hash3(uint(bin), uint(k), uint(pc.frame) * 13u) & 1u) == 0u ? 1.0 : 0.0);
break;
}
}
prevD2 = d * d;
}
reach2[bin] = rr;
binDmg[bin] = dmg;
}
barrier();
// the hole (00657960, hole_enabled): every cell at integer offsets inside the radius and within the reach of its bin; a solid / powder cell with a durability up to
// the maximum loses the bin's damage and goes below 1 hp (static cells keep the damage they took); other cells (liquids) go as before
const int hr = int(ceil(Rs)), n = 2 * hr + 1;
const ivec2 c0 = ivec2(floor(centre));
for (int j = int(gl_LocalInvocationID.x); j < n * n; j += 256) {
const ivec2 o = ivec2(j % n, j / n) - ivec2(hr);
const ivec2 w = c0 + o;
const float dist2 = float(o.x * o.x + o.y * o.y);
if (dist2 > Rs * Rs) continue;
const int bin = (o.x == 0 && o.y == 0) ? 0 : int(mod(atan(float(o.y), float(o.x)) + 6.2831853, 6.2831853) * 360.0 / 6.2831853) % 360;
const 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) continue;
const int i = p.y * pc.gw + p.x;
if (dist2 >= R * R || dist2 > reach2[bin]) {
// not reached: a stainable static solid / powder cell is burnt by s = (uniform(0.5, 0.7)) x roughness[bin + rotation] x falloff^2, the falloff 1 - (d^2 - reach^2) /
// (Rs^2 - R^2) clamped to 0..1; the roughness of a bin: uniform(0, 0.5) + (sin(bin / 90) + 1) x 0.25. Each channel x (1 - s): kept as the product in ext.x (render.comp)
if (stainCode <= 0.0 || Rs <= R) continue;
const uint sm = cell_mat(cells[i]);
if (sm == 0u || !ray_cell(sm) || (mflags(sm) & (F_STATIC | F_NOSTAIN)) != F_STATIC || (cells[i] & BURN_BIT) != 0u) continue;
const float fall = clamp(1.0 - (dist2 - reach2[bin]) / (Rs * Rs - R * R), 0.0, 1.0);
const uint rb = (uint(bin) + rot) % 360u;
const float rough = rnd01(hash3(rb, 0u, 43u)) * 0.5 + (sin(float(rb) / 90.0) + 1.0) * 0.25; // (00657960 fills DAT_012059f0 once, at the first explosion of the run: fixed per bin)
const float sv = (0.5 + 0.2 * rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 44u))) * rough * fall * fall;
if (sv <= 0.0) continue;
// the burn level lives in bits 28-30 of the cell word (free on a static cell that does not burn; it is saved with the cell when the window moves): keep = 1 - L / 7,
// the new level rounded at random so that weak burns add up
const uint cw = cells[i];
const float keep = (1.0 - float((cw >> 28) & 7u) / 7.0) * (1.0 - sv);
const float lv = (1.0 - keep) * 7.0;
const uint L = min(7u, uint(floor(lv + rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 45u)))));
cells[i] = (cw & ~(7u << 28)) | (L << 28);
continue;
}
const uint m = cell_mat(cells[i]);
if (m != 0u && uintBitsToFloat(mats[3u * m + 2u].y) > maxDur) continue;
if (m != 0u && ray_cell(m)) {
const float left = cell_hp_left(i, m) - binDmg[bin];
if (left >= 1.0) { // damaged, still there: the level of the damage rounded at random (weak hits add up), at most 3 quarters (bits 24-25; bit 26 is the frozen stain)
if ((mflags(m) & F_STATIC) != 0u) {
const float lv = (1.0 - left / float(cell_hp(m))) * 4.0;
const uint L = min(3u, uint(floor(lv + rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 46u)))));
const uint cw = cells[i];
cells[i] = (cw & ~(3u << 24)) | (L << 24);
}
continue;
}
cells[i] = 0u; ext[i] = vec2(0.0);
} else if (m != 0u) { cells[i] = 0u; ext[i] = vec2(0.0); }
// create_cell_material / create_cell_probability (00657960): every cell the blast reaches, destroyed or empty, becomes the material with that probability in percent (draw % 101 < p)
if (r.z > 0.5 && int(hash3(uint(w.x), uint(w.y), uint(pc.frame) * 7u + uint(k)) % 101u) < int(r.w)) {
cells[i] = uint(r.z) | (((hash3(uint(w.x), uint(w.y), 97u) >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame();
}
}
}