Files
Noita-GPU-Vulkan/shaders/rigid_burn.comp
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

196 lines
13 KiB
Plaintext

#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 256) in;
#include "rigid_common.glsl"
// Fire inside the rigid bodies of a burnable material (docs/FIRE_FRACTURE_AND_LIQUIDS.md, DISPLACEMENT_AND_FIRE.md, CEILING_ATTACHMENT_AND_CONTACT_GEOMETRY.md).
// One workgroup per body, a thread per group of pixels. A pixel has a state word in `burn`: bit 31 burning, bit 30 burnt away (reported to the host, which removes it from
// the body and splits it), bits 16-23 fade, bits 0-15 the burn age. A burning pixel does what a burning solid cell does:
// * common burning (0070fb10): flames into (x-1..x+1, y-1) with generates_flames percent, smoke, requires_oxygen fade (restored by an accepted flame, else -1 per update,
// burning stops below 1), and one spread attempt: r % 101 <= fade, then ONE of the four neighbours, then the ignition draw r % 101 < 100 - autoignition_temperature;
// * then the age grows, and on every 32nd frame a pixel with fire_hp != -1 and age > fire_hp is removed.
// A pixel that is not burning catches fire from the fire sources in its cell and the four around it with the probabilities of burn.comp. Water puts it out.
layout(std430, set = 0, binding = 41) buffer Burn { uint burn[]; };
layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; } ctrl;
const uint BURNING = 0x80000000u, BURNT = 0x40000000u;
float rnd(uint a, uint b, uint c) { return float(hash3(a, b, c) & 0xFFFFu) / 65536.0; }
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // 1024 noise words, 4 per material (burning explosion), then 2 per material: flame and smoke material + 1
uint flame_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m]; return v != 0u ? v - 1u : dflt; }
uint smoke_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m + 1u]; return v != 0u ? v - 1u : dflt; }
#include "biome_mods.glsl"
layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; };
layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; };
const ivec2 TD4[4] = ivec2[4](ivec2(0,-1), ivec2(-1,0), ivec2(1,0), ivec2(0,1));
uint native_material(uint m) { const uint n = mats[mats.length()-1u].w; return rhead[2u*n*(n+1u)+m]; }
uint body_neighbor(ivec2 q, Body B, int s, vec2 com, float cs, float sn, uint m, uint base) {
const ivec2 grid = q - ivec2(pc.view_x, pc.view_y);
if (grid.x < 0 || grid.y < 0 || grid.x >= pc.gw || grid.y >= pc.gh) return NONE;
const ivec2 local = ivec2(floor(rot2(cs, -sn, vec2(q) + 0.5 - B.p.xy) + com));
if (shape_solid(s, local.x, local.y) && (burn[base + uint(local.y*shi(s,0)+local.x)] & BURNT) == 0u) return m;
return cell_mat(cells[grid.y*pc.gw+grid.x]);
}
// Solid-cell 0070e060 calls the combined/fast paths when body record +0x2e
// permits them. Ice glass must keep that path after leaving the terrain grid.
int solid_conversion(ivec2 wc, Body B, int s, vec2 com, float cs, float sn, uint m, uint base, uint seed) {
if ((B.i.z & 65536) != 0) return -1; // native collapse record disables reactions
const uint r = hash3(seed, uint(pc.frame), 40u) & 32767u;
const uint R = r % 10001u;
const uint dir = hash3(seed, uint(pc.frame), 41u) & 3u;
const ivec2 q = wc + TD4[dir];
const uint nm = body_neighbor(q, B, s, com, cs, sn, m, base);
if (nm == NONE) return -1;
const bool full = (pc.frame % 11) == 0;
if (full) {
const uint bb = bm_bytes(vec2(wc));
const uint fc = (bb >> 16) & 255u, uc = bb >> 24;
const uvec2 cv = bm_conversions(m);
const bool cold = fc != 0u && (cv.x & 65535u) != 0u && (hash3(seed,uint(pc.frame),42u)&32767u) % ((cv.x>>16)+1u) < fc;
const bool warm = uc != 0u && (cv.y & 65535u) != 0u && (hash3(seed,uint(pc.frame),43u)&32767u) % ((cv.y>>16)+1u) < uc;
if (cold) return nm == 0u || (mflags(nm)&F_LIQCELL) == 0u || (mflags(nm)&F_POWDER) != 0u ? int((cv.x&65535u)-1u) : -1;
if (warm) return native_material(nm) != native_material(m) ? int((cv.y&65535u)-1u) : -1;
}
const uint n = mats[mats.length()-1u].w, table = full ? 0u : n*(n+1u);
const uint h = rhead[table + m*n+nm], count = h & 255u;
if (count == 0u || rhead[table+n*n+m] == 0u || R > rhead[table+n*n+m]) return -1;
const uint e = ((h>>8) + R%count)*5u, pw = rrec[e+1u];
const uint requiredDir = (pw>>16)&7u, actualDir = dir == 0u ? 1u : dir == 1u ? 3u : dir == 2u ? 4u : 2u;
if (R > (pw&65535u) || (requiredDir != 0u && requiredDir != actualDir)) return -1;
// Complex sprite reactions need per-pixel material storage; leave those to
// the existing fracture path rather than applying only half their outputs.
if ((rrec[e+2u]&65535u) != 65535u || (rrec[e+3u]&65535u) != 0u || (rrec[e+4u]&0x7FFFFFFFu) != 0u) return -1;
const uint target = rrec[e]&65535u, other = rrec[e]>>16;
if (target == 65535u || native_material(target) == native_material(m)) return -1;
if (other != 65535u && native_material(other) != native_material(nm)) return -1;
return int(target);
}
void main() {
int bi = int(gl_WorkGroupID.x);
if (bi >= bhdr.x) return;
Body B = bodies[bi];
if ((B.i.z & B_REMOVED) != 0 || B.i.y < 0) return;
const uint matp1 = uint(ents[B.i.y].ex5.z + 0.5);
if (matp1 == 0u) return;
const uint m = matp1 - 1u;
const uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w;
// Most rigid materials have no fast reactions. Avoid visiting their pixels
// on the ten ordinary updates between combined updates.
if (((mz >> 16) & 1u) == 0u) {
if ((B.i.z & 65536) != 0) return;
const uint n = mats[mats.length()-1u].w;
if ((pc.frame % 11) != 0 && rhead[n*(n+1u)+n*n+m] == 0u) return;
}
const int s = B.i.x;
const int w = shi(s, 0), h = shi(s, 1);
if (w * h > 4096) return;
const vec2 com = vec2(shf(s, 5), shf(s, 6));
const float cs = cos(B.p.z), sn = sin(B.p.z);
const uvec4 sp = mats[mats.length() - 1u]; // fire, smoke, steam, count
const int ai = int(mw & 255u), tempFire = int((mw >> 8) & 255u), smokeP = int((mw >> 16) & 255u), flameP = int((mw >> 24) & 255u);
const int fireHp = int(mz << 16) >> 16; // signed 16 bits
const bool noBurnOut = fireHp == -1;
const bool needO2 = ((mz >> 19) & 1u) != 0u;
const float pIgnite = max(0.0, float(100 - ai)) / 101.0;
const uint base = uint(bi) * 4096u;
for (int px = int(gl_LocalInvocationID.x); px < w * h; px += 256) {
const ivec2 pp = ivec2(px % w, px / w);
if (!shape_solid(s, pp.x, pp.y)) continue;
uint st = burn[base + uint(px)];
if ((st & BURNT) != 0u) continue;
const vec2 wp = B.p.xy + rot2(cs, sn, vec2(pp) + 0.5 - com);
const ivec2 wc = ivec2(floor(wp));
const uint seed = uint(bi) * 4099u + uint(px);
if ((st & BURNING) == 0u) {
const int target = solid_conversion(wc, B, s, com, cs, sn, m, base, seed);
if (target >= 0 && (mflags(uint(target)) & (F_LIQCELL | F_GAS | F_FIRE | F_AIR)) != 0u) {
const ivec2 grid = wc - ivec2(pc.view_x, pc.view_y);
if (grid.x >= 0 && grid.y >= 0 && grid.x < pc.gw && grid.y < pc.gh) {
const int index = grid.y*pc.gw+grid.x;
const int ei = atomicAdd(ctrl.burnN.x, 1);
if (ei < 256) {
if (atomicCompSwap(cells[index], 0u, uint(target)) == 0u) {
material_changed(index, 0u, uint(target));
ctrl.burnEv[ei] = ivec4(bi, px, target+1, 0);
burn[base+uint(px)] = BURNT;
continue;
}
ctrl.burnEv[ei] = ivec4(-1);
} else atomicAdd(ctrl.burnN.x, -1);
}
}
if (((mz >> 16) & 1u) == 0u) continue;
// fire sources in the pixel's cell and the four around it (cells are free under a body)
float keep = 1.0, bestFade = -1.0;
bool wet = false;
for (int d = 0; d < 5; ++d) {
const ivec2 q = wc + ivec2(d == 1 ? 1 : d == 2 ? -1 : 0, d == 3 ? 1 : d == 4 ? -1 : 0) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
const uint cw = cells[q.y * pc.gw + q.x], cm = cell_mat(cw);
if (cm == 0u) continue;
const uint nz = mats[3u * cm + 2u].z, nw = mats[3u * cm + 2u].w;
if (((nz >> 18) & 1u) != 0u) wet = true;
const bool nBurning = (cw & BURN_BIT) != 0u;
if (!(nBurning || ((nz >> 17) & 1u) != 0u)) continue;
int nfi = fade_of(birth[q.y * pc.gw + q.x]);
if (!nBurning && nfi == 0) nfi = int((nw >> 8) & 255u);
const float nf = float(nfi);
const float gate = nfi < 0 ? 0.0 : min(nf + 1.0, 101.0) / 101.0;
float pSrc = gate * 0.25 * pIgnite;
if ((mflags(cm) & F_FIRE) != 0u) pSrc = 1.0 - (1.0 - pSrc) * (1.0 - pSrc); // a fire cell attempts twice
keep *= 1.0 - min(pSrc, 1.0);
if (rnd(seed, uint(pc.frame), 20u + uint(d)) < pSrc && bestFade < 0.0) bestFade = nf; // the fade of the source that ignited it
}
if (!wet && bestFade >= 0.0 && rnd(seed, uint(pc.frame), 1u) > keep) burn[base + uint(px)] = BURNING | (uint(min(bestFade, 255.0)) << 16) | (st & 0xFFFFu);
continue;
}
// burning
uint age = st & 0xFFFFu;
int fade = int((st >> 16) & 255u);
bool wet = false;
for (int d = 0; d < 5 && !wet; ++d) {
const ivec2 q = wc + ivec2(d == 1 ? 1 : d == 2 ? -1 : 0, d == 3 ? 1 : d == 4 ? -1 : 0) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
const uint cm = cell_mat(cells[q.y * pc.gw + q.x]);
wet = cm != 0u && ((mats[3u * cm + 2u].z >> 18) & 1u) != 0u;
}
if (wet) { burn[base + uint(px)] = age; continue; }
bool flameAccepted = false;
if (flameP > 0 && int(rnd(seed, uint(pc.frame), 2u) * 101.0) < flameP) {
const ivec2 t = wc + ivec2(int(rnd(seed, uint(pc.frame), 3u) * 3.0) - 1, -1) - ivec2(pc.view_x, pc.view_y);
if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) {
const int ti = t.y * pc.gw + t.x;
const uint cur = cells[ti];
if (cell_mat(cur) == 0u) { // an empty place (the word may hold a leftover colour variation): the flame takes it
flameAccepted = atomicCompSwap(cells[ti], cur, flame_material(m, sp.x) | (hash3(seed, uint(pc.frame), 4u) & 0xFF0000u)) == cur;
if (flameAccepted) birth[ti] = with_fade(birth_frame(), int((mats[3u * flame_material(m, sp.x) + 2u].w >> 8) & 255u)); // (0070f8c0: a new flame has its material's own fade, temperature_of_fire; the intensity is a separate field)
}
}
}
if (needO2) fade = flameAccepted ? tempFire : fade - 1;
if (smokeP > 0 && int(rnd(seed, uint(pc.frame), 6u) * 101.0) < smokeP) {
const ivec2 t = wc + ivec2(int(rnd(seed, uint(pc.frame), 7u) * 3.0) - 1, -1) - ivec2(pc.view_x, pc.view_y);
if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) atomicCompSwap(cells[t.y * pc.gw + t.x], 0u, smoke_material(m, sp.y) | (hash3(seed, uint(pc.frame), 8u) & 0xFF0000u));
}
if (fade < 1) { burn[base + uint(px)] = age; continue; } // out of oxygen: the pixel stops burning
// one spread attempt towards one of the four neighbours inside the body
if (int(hash3(seed, uint(pc.frame), 9u) % 101u) <= fade) {
const uint dsel = hash3(seed, uint(pc.frame), 10u) & 3u;
const ivec2 np = pp + (dsel == 0u ? ivec2(0, -1) : dsel == 1u ? ivec2(-1, 0) : dsel == 2u ? ivec2(1, 0) : ivec2(0, 1));
if (shape_solid(s, np.x, np.y) && int(hash3(seed, uint(pc.frame), 11u) % 101u) < 100 - ai) {
const uint ni = base + uint(np.y * w + np.x), old = burn[ni];
if ((old & (BURNING | BURNT)) == 0u) atomicCompSwap(burn[ni], old, BURNING | (uint(fade) << 16) | (old & 0xFFFFu));
}
}
age = (age + 1u) & 0xFFFFu; // 0070e060: a wrapping 16-bit age
if ((pc.frame & 31) == 0 && !noBurnOut && fireHp < int(age)) {
const int ei = atomicAdd(ctrl.burnN.x, 1);
if (ei < 256) { ctrl.burnEv[ei] = ivec4(bi, px, 0, 0); burn[base + uint(px)] = BURNT | age; continue; }
atomicAdd(ctrl.burnN.x, -1);
}
burn[base + uint(px)] = BURNING | (uint(fade) << 16) | age;
}
}