#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(); } } }