This commit is contained in:
Gaël committed 2026-10-19 20:59:15 +02:00
commit 87bff164f1
416 files changed
+279512

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
#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);
}
}