#version 450 #extension GL_GOOGLE_include_directive : require layout(local_size_x = 8, local_size_y = 8) in; #include "common.glsl" #include "fx_state.glsl" layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // 1024 noise words, then 4 per material: burning explosion (type + 1, power, probability, damage), then 2 per material: flame and smoke material + 1 (0 = the default) #include "biome_mods.glsl" // Fire, following docs/DISPLACEMENT_AND_FIRE.md (functions 0070f5e0, 0070f9e0, 0070fb10, 0070c6e0, 0070e060, 00709fc0), in a data-parallel form. // // * State of a cell: burning (bit 27) and the engine's signed fade byte (cell + 0x11), in the top byte of the birth word. A flame cell (fire material) carries its fade // (the "intensity" the emitter gave it), a burning cell the fade of the cell that ignited it. // * Ordinary ignition: a burnable, not burning cell ignites iff r % 101 < 100 - autoignition_temperature, and takes the source's fade. // * Spread: every source (burning cell, fire source material, flame) first passes the gate r % 101 <= its fade, then picks ONE of its four neighbours; seen from // the target each neighbour is therefore a source with probability gate x 1/4 (flames spread twice per update), then the ignition test applies. // * A burning cell emits flames (generates_flames percent per update, intensity fade x (0.75 .. 1.29), into (x-1..x+1, y-1) if that cell is empty) and smoke // (generates_smoke); a material that requires_oxygen restores its fade from temperature_of_fire when the flame was accepted and otherwise loses fade, burning // stops when the fade is below 1. // * Liquids (and powders, gases) burn out with a chance 1/(fire_hp+1) per update into a flame (fire_hp 0: at once); solids age (a counter in ext.x) and are // removed on every 32nd frame once fire_hp < age (fire_hp -1 never). // * Water does not put out a neighbour (0070c6e0 / 0070e060): it smothers a covered cell through requires_oxygen. // * Hot smoke (0070f800 + CGasCell update 0070a670): smoke made by a burning cell carries an intensity trunc(fade x (0.75 .. 1.29)) (birth top byte); on an update that // reaches it ((gas_speed + 1) / 101) the intensity drops by rand % 3 and, as the signed fade, spreads fire (rand % 101 < fade, then 0070f9e0): fire climbs through its smoke. // * A burnable gas reached by a spread turns into its flame material at once (0070aa80: whatever the ignition draw; cell type fire excluded). // The engine's random streams are shared and ordered; here every cell draws its own hash, the probabilities are the engine's. const ivec2 D4[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1)); uint cell_at(ivec2 p) { if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return NONE; return cells[p.y * pc.gw + p.x]; } float rnd(uint seed, uint k) { return float(hash3(seed, k, 0x51u) & 0xFFFFu) / 65536.0; } 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; } // 0070f8c0: the CellData flame material (+0xe0) of the burning cell 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; } // 0070f800: its smoke material (+0xfc) // TODO(to-do #30): the flames over burning oil look a little taller / denser than the game's low line void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); if (p.x >= pc.gw || p.y >= pc.gh) return; int i = p.y * pc.gw + p.x; uint c = cells[i]; uint m = cell_mat(c); if (m == 0u) return; uvec4 sp = mats[mats.length() - 1u]; // special ids: fire, smoke, steam, material count uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w; uint f = mflags(m); bool burnable = ((mz >> 16) & 1u) != 0u; bool burning = (c & BURN_BIT) != 0u; uint seed = uint(i) * 977u + uint(pc.frame) * 64u + uint(pc.pass_); int ai = int(mw & 255u); int tempFire = int((mw >> 8) & 255u); int smokeP = int((mw >> 16) & 255u), flameP = int((mw >> 24) & 255u); int fireHp = int(mz << 16) >> 16; // fire_hp as the signed 16 bits bool noBurnOut = fireHp == -1; // exactly -1 never burns out (0070e060); other negative values satisfy fire_hp < age { // the biome's reaction_freeze_chance / reaction_unfreeze_chance (0070e8c0; FREEZING, HOT): r % (cold_freezes_chance_rev + 1) < freeze turns the cell into its // cold_freezes_to_material when the random neighbour is free; r % (warmth_melts_chance_rev + 1) < unfreeze turns it into its warmth_melts_to_material when that // neighbour is free or of another material (the cell-type gate +0xa0 and the contact predicate 004ac0f0 of the freeze are not ported) const uint bb = bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y))); const uint fc = (bb >> 16) & 0xFFu, uc = bb >> 24; if ((fc | uc) != 0u && !burning) { const uvec2 cv = bm_conversions(m); const uint nb = cell_at(p + D4[hash3(seed, 41u, 9u) & 3u]); const uint nm = nb == NONE ? 0u : cell_mat(nb); if (fc != 0u && (cv.x & 0xFFFFu) != 0u && (hash3(seed, 42u, 9u) & 0x7FFFu) % ((cv.x >> 16) + 1u) < fc && nm == 0u) { cells[i] = ((cv.x & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; } if (uc != 0u && (cv.y & 0xFFFFu) != 0u && (hash3(seed, 43u, 9u) & 0x7FFFu) % ((cv.y >> 16) + 1u) < uc && nm != m) { cells[i] = ((cv.y & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; } } } if (!burning && (f & F_GAS) != 0u) { // CGasCell update 0070a670: with probability (gas_speed + 1) / 101 (else the update only drifts the gas), intensity -= rand % 3, // fade = (char) intensity; a positive one then spreads (rand % 101 < fade, then 0070f9e0) - the targets below count this smoke as a source const uint raw = (birth[i] >> 24) & 255u; if (raw != 0u && (mats[3u * m + 1u].w & 255u) >= uint(rnd(seed, 30u) * 101.0)) { const int inten = max(int(raw) - int(hash3(seed, 31u, 7u) % 3u), 0); birth[i] = with_fade(birth[i], inten); } if (!burnable) return; } if (!burning) { if (!burnable) return; // ignition from the four neighbours // 0070f9e0: every source attempts: r % 101 <= its fade, one of its four neighbours is chosen, then the ignition draw; a fire cell attempts twice per update (00709fc0). The target // sees each neighbour as a source with that probability, and the first one that succeeds gives the fade it carries (not the strongest neighbour's) int srcFade = 0; bool ignited = false; float pIgnite = max(0.0, float(100 - ai)) / 101.0; for (int d = 0; d < 4 && !ignited; ++d) { ivec2 np = p + D4[d]; uint n = cell_at(np); if (n == NONE) continue; uint nm = cell_mat(n); if (nm == 0u) continue; uint nz = mats[3u * nm + 2u].z, nw = mats[3u * nm + 2u].w; bool nBurning = (n & BURN_BIT) != 0u; const bool hotGas = !nBurning && (mflags(nm) & F_GAS) != 0u && ((nz >> 17) & 1u) == 0u; int nf = fade_of(birth[np.y * pc.gw + np.x]); bool nSource = nBurning || ((nz >> 17) & 1u) != 0u || (hotGas && nf > 0); if (!nSource) continue; if (!nBurning && !hotGas && nf == 0) nf = int((nw >> 8) & 255u); // a fire source material without a stored fade: its temperature_of_fire bool flame = (mflags(nm) & F_FIRE) != 0u; float gate = nf < 0 ? 0.0 : min(float(nf) + 1.0, 101.0) / 101.0; // (a hot gas: its update reaches the spread with (gas_speed + 1) / 101, then rand % 101 < fade, then 0070f9e0's own gate) if (hotGas) gate *= float(min(mats[3u * nm + 1u].w & 255u, 100u) + 1u) / 101.0 * float(nf) / 101.0; // 0070aa80: a gas target (not of cell type fire) turns into its flame material whenever a spread reaches it, whatever the ignition draw float pSrc = gate * 0.25 * ((f & F_GAS) != 0u ? 1.0 : pIgnite); // gate, then this neighbour is the one chosen, then the ignition draw if (flame) pSrc = 1.0 - (1.0 - pSrc) * (1.0 - pSrc); // two separate attempts if (rnd(seed, 1u + uint(d)) < pSrc) { ignited = true; srcFade = nf; } } if (ignited && (f & F_GAS) != 0u) { const uint fm = flame_material(m, sp.x); cells[i] = fm | (c & 0xFF0000u); birth[i] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); ext[i] = vec2(0.0); return; } if (ignited) { cells[i] = c | BURN_BIT; birth[i] = with_fade(birth[i], srcFade); } return; } // (no "water next to it puts it out": the cell updates 0070c6e0 / 0070e060 extinguish a burning cell only by the world's gate at +0x451 (r % 100 + 1 < byte); water // smothers a fire through requires_oxygen - a covered cell cannot put its flame into a free cell above, its fade runs out - while oil floating on water burns on) // the explosion of a burning cell (0070fb10, tail): p <= (r / 32767) * min(p + 0.5, 100) with p the create_cell_probability (100 by default: only r = 32767, one draw in 32768 per update); // the radius is min(sqrt(power * (0.75 + 0.5 u)), 150); the blast is a projectile of the material's type, spawned here and exploding on its first step { const uint bt = tbl[1024u + 4u * m]; if (bt != 0u) { const float pr = uintBitsToFloat(tbl[1024u + 4u * m + 2u]); const float r1 = float(hash3(seed, 12u, 5u) & 0x7FFFu) / 32767.0; if (pr <= r1 * min(pr + 0.5, 100.0)) { const float rad = min(sqrt(uintBitsToFloat(tbl[1024u + 4u * m + 1u]) * (0.75 + 0.5 * float(hash3(seed, 13u, 5u) & 0x7FFFu) / 32767.0)), 150.0); uint slot = atomicAdd(fcnt[0], 1u) % uint(FP_MAX); FProj bp; bp.a = vec4(vec2(p + ivec2(pc.view_x, pc.view_y)) + 0.5, 0.0, 0.0); bp.b = vec4(0.0, 1.0, 0.0, 0.0); bp.c = vec4(rad, uintBitsToFloat(tbl[1024u + 4u * m + 3u]), 0.0, 0.0); bp.d = ivec4(int(bt) - 1, -1, 1 << 8, 1); bp.e = vec4(0.0); fprojs[slot] = bp; cells[i] = 0u; ext[i] = vec2(0.0); // the cell is consumed return; } } } int fade = fade_of(birth[i]); bool liquidClass = (f & (F_LIQUID | F_GAS | F_LIQCELL)) != 0u; // (cell_type liquid, static ones too: wood_static, sand_static... are CLiquidCells) // flames and smoke into (x-1..x+1, y-1), the materials of the burning cell's CellData bool flameAccepted = false; if (flameP > 0 && int(rnd(seed, 2u) * 101.0) < flameP) { ivec2 t = p + ivec2(int(rnd(seed, 4u) * 3.0) - 1, -1); if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) { int ti = t.y * pc.gw + t.x; if (cell_mat(cells[ti]) == 0u) { const uint fm = flame_material(m, sp.x); // (0070f8c0: the intensity goes to the new gas cell's +0x2c; its fade byte +0x11, the gate of its spread, is what a new cell of the flame material has: its // temperature_of_fire, 0070f450 - fire: 100. A flame of burning oil ignites wood as readily as any other) if (atomicCompSwap(cells[ti], 0u, fm | (hash3(seed, 9u, 1u) & 0xFF0000u)) == 0u) { flameAccepted = true; birth[ti] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); } } } } if (((mz >> 19) & 1u) != 0u) { // requires_oxygen: an accepted flame restores the fade, a failure takes one off the signed byte (wrapping) fade = flameAccepted ? tempFire : ((fade - 1 + 128) & 255) - 128; birth[i] = with_fade(birth[i], fade); if (fade < 1) { cells[i] = c & ~BURN_BIT; return; } } if (smokeP > 0 && int(rnd(seed, 6u) * 101.0) < smokeP) { ivec2 t = p + ivec2(int(rnd(seed, 7u) * 3.0) - 1, -1); if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) { const int ti = t.y * pc.gw + t.x; // 0070f800: a new cell of type gas gets the intensity trunc(fade x (0.75 .. 1.29)) at +0x2c (here the birth word's top byte, unsigned); its gas update (0070a670) spreads fire with it const uint smm = smoke_material(m, sp.y); const int inten = (mflags(smm) & F_GAS) != 0u ? clamp(int(float(fade) * (0.75 + 0.54 * rnd(seed, 8u))), 0, 255) : 0; if (atomicCompSwap(cells[ti], 0u, smm | (hash3(seed, 9u, 2u) & 0xFF0000u)) == 0u) birth[ti] = with_fade(birth_frame(), inten); } } { // the biome's fire_extinguish_chance (0070c6e0 / 0070e060, after the burning tick 0070fb10): the cell goes out when rng % 100 + 1 < it (MOIST: 11) const uint fx = (bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y))) >> 8) & 0xFFu; if (fx != 0u && (hash3(seed, 31u, 7u) & 0x7FFFu) % 100u + 1u < fx) { cells[i] = c & ~BURN_BIT; return; } } if (liquidClass) { // liquids, powders, gases: burn out into a flame with chance 1/(fire_hp+1) (0: at once) bool burnOut = !noBurnOut && (fireHp <= 0 || int(hash3(seed, 8u, 3u) % uint(fireHp + 1)) == 0); if (!burnOut && !noBurnOut && fireHp > 0 && (f & F_STATIC) != 0u && int(hash3(seed, 8u, 4u) % uint(fireHp + 1)) <= 29) { // a passive (static) liquid (0070c6e0): a second draw <= 29 also burns it out when a random cardinal neighbour is empty (00743e80: one of four by two bits of a // draw; outside the window the opposite one is looked at) - the surface of burning wood is eaten away const uint k = hash3(seed, 8u, 5u) & 3u; ivec2 q = p + D4[k]; if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) q = p - D4[k]; if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cells[q.y * pc.gw + q.x] == 0u) burnOut = true; } if (burnOut) { const uint fm = flame_material(m, sp.x); cells[i] = fm | (c & 0xFF0000u); birth[i] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); ext[i] = vec2(0.0); return; } // (a new cell of the flame material: its own fade) } else if ((f & F_STATIC) != 0u) { // solids: a wrapping 16-bit burn age; removed every 32nd frame once fire_hp < age int age = (int(ext[i].x) + 1) & 0xFFFF; if ((pc.frame & 31) == 0 && !noBurnOut && fireHp < age) { cells[i] = 0u; ext[i] = vec2(0.0); return; } ext[i] = vec2(float(age), ext[i].y); } }