#version 450 #extension GL_GOOGLE_include_directive : require layout(local_size_x = 8, local_size_y = 8) in; #include "common.glsl" // Movement intent. Every mobile cell decides at most one move per pass following the // original engine's rules (noita.exe CLiquidCell / CGasCell / CFireCell updates, see // docs/ENGINE_ANALYSIS.md). Conflicts are resolved afterwards by claim arbitration, then // apply.comp performs the winning swaps. Several passes per frame stand in for the // original's multi-cell steps. uint rnd(uint i, uint salt) { return hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), salt + uint(pc.seed)); } int rand100(uint i, uint salt) { return int(rnd(i, salt) % 101u); } // rand() % 101 float rand01(uint i, uint salt) { return float(rnd(i, salt) & 0x7FFFu) / 32767.0; } bool in_bounds(ivec2 p) { return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh; } uint idx(ivec2 p) { return uint(p.y * pc.gw + p.x); } bool is_air(uint m) { return (mflags(m) & F_AIR) != 0u; } // FUN_0070b200: may a liquid/powder `mm` enter a cell holding material `tm`? bool can_enter_liquid(uint mm, uint tm, bool roll25) { uint tf = mflags(tm); if ((tf & F_AIR) != 0u) return true; if ((tf & (F_GAS | F_FIRE)) != 0u) return true; // gas and fire are always displaced if ((tf & F_STATIC) != 0u) return false; // solids are never entered if (!roll25) return false; // dense-through-light swaps only 25% of the time if ((mflags(mm) & F_SAND) != 0u && (tf & F_SAND) != 0u) return false; // powder never through powder return mdensity(tm) < mdensity(mm); } // FUN_0070a5a0: may a gas `mm` enter a cell holding material `tm`? bool can_rise(uint mm, uint tm, bool roll25) { uint tf = mflags(tm); if ((tf & F_AIR) != 0u) return true; if ((tf & (F_STATIC | F_LIQUID)) != 0u) return false; if (tm == mm) return false; return roll25 && mdensity(tm) > mdensity(mm); } void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); if (p.x >= pc.gw || p.y >= pc.gh) return; uint i = idx(p); intent[i] = uvec4(NONE, 0u, 0u, 0u); uint c = cells[i]; uint m = cell_mat(c); uint f = mflags(m); if (pc.pass_ == 0 && (terrain_age[i] >> 16) != m) terrain_age[i] = (m << 16) | 0x300u; if ((f & (F_AIR | F_STATIC)) != 0u) return; vec2 e = ext[i]; float vy = e.x, vx = e.y; float nvy = vy, nvx = vx; uint target = NONE; uint flip = 0u; uint prio = ((hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), 777u + uint(pc.seed)) & 0xFFFu) << 20) | i; // unique per cell (12 random bits, the cell index in 20) bool roll25 = rand100(i, 11u) > 74; const int P = pc.pass_; if ((f & F_LIQUID) != 0u) { const bool sand = (f & F_SAND) != 0u; { // 0070c6e0: an active liquid / powder with a positive lifetime disappears when its age (frames since it got its material) reaches it const int lt = mlifetime(m); if (lt > 0 && int(birth_age(birth[i])) >= lt) { cells[i] = 0u; ext[i] = vec2(0.0); return; } } // Powders with liquid_sticks_to_ceiling (soil, mud, moss...): 0070b2d0 attaches them to the cell on one side / above before they would fall, 0070c6e0 releases them again. const uint ceilP = sand ? mceiling(m) : 0u; if (ceilP > 0u) { const ivec2 W = p + ivec2(pc.view_x, pc.view_y); if ((c & ATTACH_BIT) != 0u) { // passive: never moves by itself if (P == 0) { const uint r1 = rnd(i, 91u) % 101u; if (r1 < 20u) { // ceiling check const uint st = (c >> 24) & 3u; const ivec2 sp2 = p + (st == 1u ? ivec2(1, 0) : st == 3u ? ivec2(-1, 0) : ivec2(0, -1)); bool support = false; if (in_bounds(sp2)) { const uint nc = cells[idx(sp2)], nm = cell_mat(nc); support = nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u)); } bool release = !support; if (support) { const uint r2 = rnd(i, 92u) % 101u; const ivec2 bp = p + ivec2(0, 1); release = r2 < ceilP && in_bounds(bp) && cell_mat(cells[idx(bp)]) == 0u; } if (release) { cells[i] = c & ~(ATTACH_BIT | (3u << 24)); terrain_age[i] = (m << 16) | 0x300u; } } } return; } // attachment: one location chosen by a hash of the coordinates (0070acc0): even rows give 7 + u % 3 (7 = left, else above), odd rows 5 - u % 3 (5 = right, else above) { int u = W.x + 0x67577458 + 91 * W.y; u = int((uint((u >> 13) ^ u)) * 214013u + 2531011u); u = ((u >> ((W.x & 15) + 1)) ^ u) & 32767; const int code = (W.y & 1) == 0 ? 7 + u % 3 : 5 - u % 3; const ivec2 ap = p + (code == 5 ? ivec2(1, 0) : code == 7 ? ivec2(-1, 0) : ivec2(0, -1)); if (in_bounds(ap)) { const uint nc = cells[idx(ap)], nm = cell_mat(nc); if (nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u))) { cells[i] = (c & ~(3u << 24)) | ATTACH_BIT | ((code == 5 ? 1u : code == 7 ? 3u : 2u) << 24); ext[i] = vec2(0.0); return; } } } } const float g = mgravity(m); const bool falling = vy > 0.0; // powders that are already falling skip a frame with probability (1 - gravity) * 0.5 if (sand && falling && rand01(i, 3u) < (1.0 - g) * 0.5) return; const int extra = vy > 1.0 ? int(ceil((vy - 1.0) / 2.0)) : 0; // extra cells per frame from accumulated velocity const bool downAllowed = (P == 0) || (P <= extra); // drift of extra fall steps: 1/8 left, 1/8 right, else straight; liquids follow vx when it is large int dx = 0; if (P > 0) { if (!sand && abs(vx) >= 1.0) dx = vx >= 0.0 ? 1 : -1; else if (!sand) { // 0070b2d0 / 0070b7c0: the drift is drawn afresh only when (r & 127) < 64 (slime: < 12), otherwise the previous one of this update stays; the extra passes of one frame // replay that chain const uint gateLimit = (f & F_SLIME) != 0u ? 12u : 64u; for (int k = 1; k <= P; ++k) { const uint rg = hash3(i, uint(pc.frame) * 16u + uint(k), 5u + uint(pc.seed)); if (((rg >> 16) & 127u) < gateLimit) { const uint r = hash3(i, uint(pc.frame) * 16u + uint(k), 6u + uint(pc.seed)) & 7u; dx = r == 1u ? -1 : (r == 2u ? 1 : 0); } } } else if ((rnd(i, 5u) & 1u) == 0u) { uint r = rnd(i, 6u) & 7u; dx = r == 1u ? -1 : (r == 2u ? 1 : 0); } } bool belowFree = false; bool belowFalling = false; ivec2 below = p + ivec2(0, 1); if (in_bounds(below)) { const uint bm = cell_mat(cells[idx(below)]); belowFree = can_enter_liquid(m, bm, roll25); // The original updates bottom-up, so a falling cell below has already moved by the time this cell // runs. We decide from a snapshot, so wait for it instead of sliding sideways past it. if ((mflags(bm) & F_LIQUID) != 0u) { // already falling, or about to: it has free space of its own below it belowFalling = ext[idx(below)].x > 0.0; ivec2 below2 = below + ivec2(0, 1); if (!belowFalling && in_bounds(below2)) belowFalling = can_enter_liquid(bm, cell_mat(cells[idx(below2)]), false); } } if (downAllowed) { ivec2 t = p + ivec2(dx, 1); if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) { target = idx(t); if (P == 0) nvy = falling ? vy + g * 0.25 : 1.0e-4; // 0070b2d0: the falling state adds gravity / 4, a newly falling cell resets vy (the tiny value marks the falling state) // 0070bdc0: after the fall the lateral loop still runs (while |vx| >= 1: a cell sideways, vx *= liquid_damping), so the sideways speed of a falling liquid // dies within a frame or two; the GPU makes one move per pass, so the damping is applied here (a drop kept vx = +-1 from a diagonal slide and drifted // diagonally on every extra fall step: the diagonal chains of falling water / oil) nvx = (!sand && (f & F_SLIME) == 0u && P == 0 && abs(vx) >= 1.0) ? vx * mdamping(m) : vx; } } if (target == NONE && !belowFree && belowFalling) return; // wait for the falling cell below to move on if (target == NONE && P == 0) { if (belowFree == false) nvy = 0.0; // blocked below: stop falling if (sand) { // diagonal slide with probability liquid_viscosity %, left first then right int visc = mviscosity(m); if (visc < 100 && rand100(i, 7u) < visc) { ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1); if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l); else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), rand100(i, 12u) > 74)) target = idx(r); } } else if ((f & F_SLIME) != 0u && !belowFree) { // 0070b7c0 (slime): the viscosity roll ends the move (no diagonal fallback), then left-down, right-down in this order, then a lateral step only when exactly one side is // free: vx += 0.9, one move when |vx| >= 1 (vx -= 1); two free sides do nothing int visc = mviscosity(m); if (!(visc < 100 && rand100(i, 7u) < visc)) { ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1); if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l); else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), roll25)) target = idx(r); else { ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0); bool lv = in_bounds(lp), rv = in_bounds(rp); bool lo = lv && cell_mat(cells[idx(lp)]) != 0u, ro = rv && cell_mat(cells[idx(rp)]) != 0u; if (rv && !ro && (!lv || lo)) { nvx = vx + 0.9; if (nvx >= 1.0) { target = idx(rp); nvx -= 1.0; } } else if (lv && !lo && (!rv || ro)) { nvx = vx - 0.9; if (nvx <= -1.0) { target = idx(lp); nvx += 1.0; } } } } } else if (!belowFree) { // lateral flow: accelerate toward the single free side, then move when |vx| reaches 1 ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0); bool lf = in_bounds(lp) && can_enter_liquid(m, cell_mat(cells[idx(lp)]), roll25); bool rf = in_bounds(rp) && can_enter_liquid(m, cell_mat(cells[idx(rp)]), rand100(i, 13u) > 74); if (lf != rf) { nvx = vx + (rf ? mflow(m) : -mflow(m)); if (rf && nvx >= 1.0) { target = idx(rp); nvx *= mdamping(m); } else if (lf && nvx <= -1.0) { target = idx(lp); nvx *= mdamping(m); } } if (target == NONE) { // random diagonal-down side, then the other int dir = (rnd(i, 8u) & 1u) != 0u ? 1 : -1; ivec2 a = p + ivec2(dir, 1), b = p + ivec2(-dir, 1); if (in_bounds(a) && can_enter_liquid(m, cell_mat(cells[idx(a)]), roll25)) { target = idx(a); nvx = float(dir); } else if (in_bounds(b) && can_enter_liquid(m, cell_mat(cells[idx(b)]), rand100(i, 14u) > 74)) { target = idx(b); nvx = float(-dir); } } } } // damped ballistic lateral travel while |vx| >= 1 (one cell per pass, vx *= damping per cell) if (target == NONE && !sand && (f & F_SLIME) == 0u && !belowFree && abs(nvx) >= 1.0) { // (slime has no inertia loop: 0070b7c0 returns) int s = nvx >= 0.0 ? 1 : -1; ivec2 t = p + ivec2(s, 0); if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) { target = idx(t); nvx *= mdamping(m); } // blocked: the velocity stays (the original keeps it until its state / activity logic changes it) } } else if ((f & F_GAS) != 0u) { if (P != 0) return; // gases act once per frame const int lifetime = mlifetime(m); if (lifetime > 0 && vy >= float(lifetime)) { cells[i] = 0u; ext[i] = vec2(0.0); return; } // 0070a670: the move counter (0070aad0 counts the successful moves) against the lifetime uint sp = mgasbits(m); if (rand100(i, 21u) <= int(sp & 255u)) { if (rand100(i, 22u) < int((sp >> 8) & 255u)) { ivec2 t = p + ivec2(0, -1); if (in_bounds(t) && can_rise(m, cell_mat(cells[idx(t)]), roll25)) target = idx(t); } if (target == NONE && rand100(i, 23u) < int((sp >> 16) & 255u)) { int dir = (c & DIR_BIT) != 0u ? 1 : -1; ivec2 a = p + ivec2(dir, 0), b = p + ivec2(-dir, 0); if (in_bounds(a) && can_rise(m, cell_mat(cells[idx(a)]), roll25)) target = idx(a); else if (in_bounds(b) && can_rise(m, cell_mat(cells[idx(b)]), rand100(i, 25u) > 74)) { target = idx(b); flip = 1u; } } if (target == NONE && rand100(i, 24u) < int((sp >> 24) & 255u)) { ivec2 t = p + ivec2(0, 1); if (in_bounds(t) && is_air(cell_mat(cells[idx(t)]))) target = idx(t); // downward only into empty cells } } } else if ((f & F_FIRE) != 0u) { if (P != 0) return; if (rand100(i, 31u) < 35) { ivec2 up = p + ivec2(0, -1); if (in_bounds(up) && is_air(cell_mat(cells[idx(up)]))) target = idx(up); else if (rand100(i, 32u) >= 35) { int s = rand100(i, 33u) < 50 ? -1 : 1; ivec2 a = p + ivec2(s, 0), b = p + ivec2(-s, 0); if (in_bounds(a) && is_air(cell_mat(cells[idx(a)]))) target = idx(a); else if (in_bounds(b) && is_air(cell_mat(cells[idx(b)]))) target = idx(b); } } } if ((f & F_FIRE) != 0u && target == NONE && (rnd(i, 30u) & 15u) == 0u) { cells[i] = 0u; return; } // 00709fc0: the extinction test (1/16) sits at the tail: a successful move and the early returns bypass it if ((f & F_GAS) != 0u && target != NONE) nvy = vy + 1.0; // 0070aad0: the gas age counts its successful moves // 0070b160: only a resting powder whose state age reaches 20 becomes terrain. // Successful moves reset both swapped cells below in apply.comp. if ((f & F_LIQCELL) != 0u) { const bool resting = target == NONE && nvy == 0.0; const uint age = resting ? min((terrain_age[i] & 255u) + (P == 0 ? 1u : 0u), 255u) : 0u; terrain_age[i] = (m << 16) | (resting ? 0x300u : 0x100u) | age; } if (target != NONE) { intent[i] = uvec4(target | (flip << 31), prio, floatBitsToUint(nvy), floatBitsToUint(nvx)); atomicMax(claims[i], prio); atomicMax(claims[target], prio); } else { ext[i] = vec2(nvy, nvx); // resting updates (velocity bookkeeping) } }