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

+27
View File
@@ -0,0 +1,27 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
// Performs the moves that won claim arbitration. A mover wins only if it holds the highest
// priority on both its source and its target cell, so the winners' cell pairs are disjoint and
// no atomics or ordering are needed here. The displaced occupant takes the mover's old slot
// (the original's swap-based move primitive, FUN_0070cc00).
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= pc.gw || p.y >= pc.gh) return;
uint i = uint(p.y * pc.gw + p.x);
uvec4 it = intent[i];
if (it.x == NONE) return;
uint t = it.x & 0x7FFFFFFFu;
if (claims[i] != it.y || claims[t] != it.y) return; // lost arbitration, retry next pass
uint cs = cells[i], ct = cells[t];
vec2 et = ext[t];
if ((it.x >> 31) != 0u) cs ^= DIR_BIT; // gas bounced: flip horizontal direction
cells[t] = cs;
ext[t] = vec2(uintBitsToFloat(it.z), uintBitsToFloat(it.w));
cells[i] = ct;
ext[i] = et;
const uint bt = birth[t]; birth[t] = birth[i]; birth[i] = bt; // the age moves with the cell
}
+26
View File
@@ -0,0 +1,26 @@
// The loaded biome areas (host: composeRegion in main.cpp). One record of 16 words per area after the 8 word header, then the tile map (biome-map tile -> area + 1).
// header (8 words): [0] area count, [1] tile map offset, [2] tile map width, [3] tile map height, [4] offset of the per-tile topology record offsets, [5] world seed, [6] x offset of the gradient noise (float)
// record: [0] origin x, [1] origin y (world cell at the centre of wang pixel (0,0)), [2] wang width, [3] wang height, [4] offset in the solidity plane, [5] offset in the material plane,
// [6] scene x0, [7] scene y0, [8] scene width, [9] scene height, [10] offset of the chunk table of the scene planes (materials, visuals, backgrounds; chunks of 64 x 64 cells, absolute index of the chunk or 0xFFFFFFFF), [11] offset of the background tile,
// [12] background width, [13] background height, [14] offset of the biome's <Materials> list, [15] its component count
layout(std430, set = 0, binding = 43) readonly buffer AreaTab { uint atab[]; };
int arec(int a, int k) { return int(atab[8 + 16 * a + k]); }
// The area of the biome-map tile that holds the world cell, -1 when none is loaded.
int area_of(ivec2 w) {
int tx = (w.x >= 0 ? w.x / 512 : -((-w.x + 511) / 512)) + 35;
int ty = (w.y >= 0 ? w.y / 512 : -((-w.y + 511) / 512)) + 14;
if (tx < 0 || ty < 0 || tx >= int(atab[2]) || ty >= int(atab[3])) return -1;
return int(atab[atab[1] + uint(ty) * atab[2] + uint(tx)]) - 1;
}
// Index of a world cell in the scene planes of area a, -1 when the cell is outside that area's planes.
int scene_index(int a, ivec2 w) {
ivec2 sc = w - ivec2(arec(a, 6), arec(a, 7));
if (sc.x < 0 || sc.y < 0 || sc.x >= arec(a, 8) || sc.y >= arec(a, 9)) return -1;
int cw = (arec(a, 8) + 63) >> 6;
uint ct = atab[uint(arec(a, 10) + (sc.y >> 6) * cw + (sc.x >> 6))]; // scene planes are stored in chunks of 64 x 64 cells
if (ct == 0xFFFFFFFFu) return -1;
return int(ct) + ((sc.y & 63) << 6) + (sc.x & 63);
}
+18
View File
@@ -0,0 +1,18 @@
// The biome modifiers (data/scripts/biome_modifiers.lua, BiomeModifiers at biome + 0x170; src/world/biome_modifiers.h) of the biome-map tile under a world point,
// and the cold / warm conversions of the materials. Needs the Tables buffer as `tbl` (main.cpp fills both after the fog of war hole images):
// per tile: projectile_drag_coeff, entity_gravity_y_multiplier (float bits), bytes (fog_of_war_delta, fire_extinguish_chance, reaction_freeze_chance, reaction_unfreeze_chance), dust_amount (float bits)
// per material: cold_freezes_to_material + 1 | cold_freezes_chance_rev << 16, warmth_melts_to_material + 1 | warmth_melts_chance_rev << 16
uint bm_fog_base() { const uint n = mats[mats.length() - 1u].w; return 1024u + 9u * n + 132u + n; } // the biome map's width, height, then the fog type per tile
uint bm_mods_base() { return bm_fog_base() + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u; }
int bm_tile(vec2 w) { // the tile index, -1 outside the map (the world default: drag 1, gravity 1, no chances)
const uint fb = bm_fog_base();
const int bw = int(tbl[fb]), bh = int(tbl[fb + 1u]);
const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14);
if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return -1;
return t.y * bw + t.x;
}
float bm_drag(vec2 w) { const int t = bm_tile(w); return t < 0 ? 1.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t)]); }
float bm_gravity(vec2 w) { const int t = bm_tile(w); return t < 0 ? 1.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t) + 1u]); }
uint bm_bytes(vec2 w) { const int t = bm_tile(w); return t < 0 ? 0u : tbl[bm_mods_base() + 4u * uint(t) + 2u]; } // fog delta | fire extinguish << 8 | freeze << 16 | unfreeze << 24
float bm_dust(vec2 w) { const int t = bm_tile(w); return t < 0 ? 0.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t) + 3u]); } // the biome modifier dust_amount (post_final.frag: tex_fog .g)
uvec2 bm_conversions(uint m) { const uint cb = bm_mods_base() + 8192u * 4u + 2u * m; return uvec2(tbl[cb], tbl[cb + 1u]); }
+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);
}
}
+23
View File
@@ -0,0 +1,23 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
// The frame for the replay recorder at half size (2 x 2 average) when the window is wider than 1600 px: the copy to the host is a quarter
// (main.cpp; a 2560 x 1360 frame is 14 MB, copied every simulation tick).
layout(std430, set = 0, binding = 58) buffer ClipHalf { uint clipHalf[]; };
layout(std430, set = 0, binding = 60) readonly buffer ClipRaw { uint clipraw[]; }; // the window picture without the HUD (render.comp)
void main() {
const ivec2 q = ivec2(gl_GlobalInvocationID.xy);
const int hw = pc.ow / 2, hh = pc.oh / 2;
if (q.x >= hw || q.y >= hh) return;
const int i = 2 * q.y * pc.ow + 2 * q.x;
const uvec4 a = uvec4(clipraw[i], clipraw[i + 1], clipraw[i + pc.ow], clipraw[i + pc.ow + 1]);
uint o = 0u;
for (int c = 0; c < 32; c += 8) {
const uvec4 v = (a >> uint(c)) & 0xFFu;
o |= ((v.x + v.y + v.z + v.w + 2u) >> 2) << uint(c);
}
clipHalf[q.y * hw + q.x] = o;
}
+84
View File
@@ -0,0 +1,84 @@
// Shared by intent/apply/paint/render compute shaders.
//
// Cell word (u32): bits 0-15 material index, 16-23 colour variation, 26 gas/fire horizontal direction.
// Per-cell extension (vec2): x = vertical velocity accumulator, y = horizontal velocity.
// Material table: 3 uvec4 per material
// [0] colour(ARGB), flags, density(float), liquid_gravity(float)
// [1] liquid_viscosity(int), liquid_damping(float), liquid_flow_speed(float),
// gas speeds packed: byte0 gas_speed, byte1 up, byte2 horizontal, byte3 down
// [2] lifetime(int), durability(float), burn word 1 (fire_hp | burnable << 16 | on_fire << 17 | extinguishes << 18), burn word 2 (autoignition | temperature_of_fire << 8 | smoke << 16)
// The last entry of the table is special: (fire id, smoke id, steam id, material count).
layout(std430, set = 0, binding = 0) buffer Cells { uint cells[]; };
layout(std430, set = 0, binding = 1) readonly buffer Mats { uvec4 mats[]; };
layout(std430, set = 0, binding = 2) buffer Pixels { uint pixels[]; };
layout(std430, set = 0, binding = 3) buffer Ext { vec2 ext[]; };
layout(std430, set = 0, binding = 42) buffer Birth { uint birth[]; }; // the frame a cell got its material (cell age = frame - birth, 0070ebf0 req_lifetime)
layout(std430, set = 0, binding = 4) buffer Claims { uint claims[]; };
// per cell: x = target index | (flip direction << 31), y = priority, z/w = new vy/vx bits
layout(std430, set = 0, binding = 5) buffer Intent { uvec4 intent[]; };
layout(push_constant) uniform PC {
int gw, gh; // grid size in cells
int ow, oh; // output size in pixels
int scale; // pixels per cell
int frame;
int pass_; // movement pass within the frame
int brush_x, brush_y, brush_r;
int brush_mat; // -1: none, 0: erase
int seed;
int view_x, view_y; // world cell at the top-left of the grid
int area_ox, area_oy; // world cell at the centre of wang pixel (0,0)
int pw, ph; // wang plane size
int m_rock, m_soil, m_sand, m_wet; // fill materials
int shift_x, shift_y, shift_mode;
int scene_x0, scene_y0, scene_w, scene_h; // pixel-scene plane placement (cells)
int bg_w, bg_h; // biome background tile size (0 = none)
int ent_count; // sprite instances for sprites.comp // terrain.comp: mode 1 = scroll by (shift_x, shift_y), keep the overlap
} pc;
const uint F_STATIC = 1u; // never moves (solid or liquid_static)
const uint F_LIQUID = 2u; // liquid or powder
const uint F_GAS = 4u;
const uint F_SAND = 8u; // powder (liquid_sand)
const uint F_AIR = 16u;
const uint F_FIRE = 32u;
const uint F_LIQCELL = 128u; // cell_type liquid, static or not (the native count / immersion predicate: liquid slot 22 true, powder slot 25 false)
const uint F_POWDER = 256u; // liquid_sand, static or not
const uint F_STAINS = 512u; // liquid_stains: a body in it takes its stain (vine points: the stored material that slows them)
const uint F_NOSTAIN = 2048u; // CellData stainable off (the box2d materials): no explosion burn
const uint F_INDESTRUCT = 1024u; // material tag [indestructible]: a CellEaterComponent with ignored_material_tag leaves it
const uint F_SLIME = 64u; // liquid_slime: moved by 0070b7c0 (docs/IMPLEMENTATION_BLOCKERS.md)
const uint NONE = 0xFFFFFFFFu;
const uint DIR_BIT = 1u << 26;
const uint BURN_BIT = 1u << 27; // the cell is burning
const uint FROZEN_BIT = 1u << 26; // a static cell stained frozen (MagicConvertMaterialComponent stain_frozen; bits 24-25 then hold its damage level, explode_rays.comp)
const uint ATTACH_BIT = 1u << 26; // a powder attached to a wall or ceiling (passive: byte 22 of the original cell); bits 24-25 then hold the direction: 1 right (state 5), 2 above (6), 3 left (7)
// Fire fade (the engine's signed fade byte, in steps of 4): bits 24-25 and 28-31 of the cell word. Burning cells and flames carry it.
uint cell_fade6(uint c) { return ((c >> 28) & 15u) | (((c >> 24) & 3u) << 4); }
uint with_fade6(uint c, uint f) { f = min(f, 63u); return (c & ~0xF3000000u) | ((f & 15u) << 28) | ((f >> 4) << 24); }
// The age of a cell and its fire fade share the birth word: bits 0-23 the frame it got its material (the age is the difference modulo 2^24), bits 24-31 the engine's signed fade byte (cell + 0x11).
const uint BIRTH_MASK = 0xFFFFFFu;
uint birth_frame() { return uint(pc.frame) & BIRTH_MASK; }
uint birth_age(uint b) { return (uint(pc.frame) - b) & BIRTH_MASK; }
int fade_of(uint b) { return int(b) >> 24; } // signed byte
uint with_fade(uint b, int f) { return (b & BIRTH_MASK) | (uint(f & 255) << 24); }
uint cell_mat(uint c) { return c & 0xFFFFu; }
uint mflags(uint m) { return mats[3u * m].y; }
float mdensity(uint m) { return uintBitsToFloat(mats[3u * m].z); }
float mgravity(uint m) { return uintBitsToFloat(mats[3u * m].w); }
int mviscosity(uint m) { return int(mats[3u * m + 1u].x & 0xFFFFu); }
uint mceiling(uint m) { return mats[3u * m + 1u].x >> 16; } // liquid_sticks_to_ceiling (powders)
float mdamping(uint m) { return uintBitsToFloat(mats[3u * m + 1u].y); }
float mflow(uint m) { return uintBitsToFloat(mats[3u * m + 1u].z); }
uint mgasbits(uint m) { return mats[3u * m + 1u].w; }
int mlifetime(uint m) { return int(mats[3u * m + 2u].x); }
uint hash(uint x) {
x ^= x >> 16; x *= 0x7feb352du;
x ^= x >> 15; x *= 0x846ca68bu;
x ^= x >> 16;
return x;
}
uint hash3(uint a, uint b, uint c) { return hash(a * 0x9E3779B1u ^ hash(b + 0x85EBCA6Bu ^ hash(c))); }
+90
View File
@@ -0,0 +1,90 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "rigid_common.glsl"
// Rigid bodies displace liquids: the liquid cells inside their volume are moved to the nearest free cell outside it, away from the centre (preferring upward).
// (One workgroup per entity, which return at once, then one per rigid body.)
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; } ctrl;
// Liquids, and for bodies that do not go through sand (CellData solid_go_through_sand, bit 9 of the body flags) the powders too: a body that sinks into a pile pushes the grains out.
bool liquid_cell(uint c, bool powders) {
uint m = cell_mat(c);
if (m == 0u) return false;
uint f = mflags(m);
if ((f & F_STATIC) != 0u) return false;
return (f & F_LIQUID) != 0u && ((f & F_SAND) == 0u || powders);
}
// Moves the cell at world position w out of the volume: the volume is a box (bodyIdx < 0) or a rigid body's mask.
void push_out(ivec2 w, vec2 centre, ivec4 box, int bodyIdx) {
const bool powders = bodyIdx >= 0 && (bodies[bodyIdx].i.z & 512) == 0;
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) return;
int i = p.y * pc.gw + p.x;
uint c = cells[i];
if (!liquid_cell(c, powders)) return;
vec2 d = vec2(w) + 0.5 - centre;
d.y -= 0.8; // up is preferred
d = length(d) < 0.01 ? vec2(0.0, -1.0) : normalize(d);
// the target is claimed first (only free cells are claimed), then the source is released; another invocation that already moved the source (bodies that overlap handle the
// same cell) makes this one give its target back. Nothing is lost or doubled: before, the source was emptied first and put back when no free cell was found, and a neighbour
// could fill the briefly empty source in between (a full pool with rubble in it lost its water)
const vec2 e = ext[i];
for (int r = 1; r <= 12; ++r) {
ivec2 t = w + ivec2(round(d * float(r)));
ivec2 q = t - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) break;
bool inside;
if (bodyIdx < 0) inside = t.x >= box.x && t.x < box.z && t.y >= box.y && t.y < box.w;
else {
Body b = bodies[bodyIdx];
int s = b.i.x;
vec2 dd = vec2(t) + 0.5 - b.p.xy;
float cs = cos(b.p.z), sn = sin(b.p.z);
vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6));
inside = shape_solid(s, int(floor(im.x)), int(floor(im.y)));
}
if (inside) continue;
int ti = q.y * pc.gw + q.x;
if (atomicCompSwap(cells[ti], 0u, c) == 0u) { // a free target, now holding the liquid
if (atomicCompSwap(cells[i], c, 0u) == c) { ext[ti] = e; ext[i] = vec2(0.0); }
else atomicCompSwap(cells[ti], c, 0u); // the source went elsewhere meanwhile: give the target back
return;
}
}
// nowhere to go: the liquid stays where it is
}
void main() {
int g = int(gl_WorkGroupID.x), t = int(gl_LocalInvocationID.x);
int nEnt = ctrl.inp.w;
if (g < nEnt) {
return; // creatures and the player displace through the particle path of entities.comp (docs/DISPLACEMENT_AND_FIRE.md); only rigid bodies are pushed out here (the engine's body rule is not recovered)
Ent e = ents[g];
int kind = e.st.x;
if ((kind != 1 && kind != 2 && kind != 3) || (e.st.w & 8) != 0) return;
ivec2 ip = ivec2(floor(e.a.xy));
ivec2 lo = ip + ivec2(int(e.box.x), int(e.box.z)), hi = ip + ivec2(int(e.box.y), int(e.box.w));
ivec2 sz = hi - lo;
if (sz.x <= 0 || sz.y <= 0 || sz.x * sz.y > 4096) return;
vec2 centre = vec2(lo + hi) * 0.5;
for (int k = t; k < sz.x * sz.y; k += 64) push_out(lo + ivec2(k % sz.x, k / sz.x), centre, ivec4(lo, hi), -1);
} else {
int bi = g - nEnt;
if (bi >= bhdr.x) return;
Body b = bodies[bi];
if ((b.i.z & 2) != 0) return;
int s = b.i.x;
float rad = shf(s, 9);
int n = int(2.0 * rad) + 2;
ivec2 lo = ivec2(floor(b.p.xy - vec2(rad)));
for (int k = t; k < n * n; k += 64) {
ivec2 w = lo + ivec2(k % n, k / n);
vec2 dd = vec2(w) + 0.5 - b.p.xy;
float cs = cos(b.p.z), sn = sin(b.p.z);
vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6));
if (shape_solid(s, int(floor(im.x)), int(floor(im.y)))) push_out(w, b.p.xy, ivec4(0), bi);
}
}
}
+191
View File
@@ -0,0 +1,191 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
#include "far.glsl"
#include "fx_state.glsl"
// Explosions and spills: circular edits of the cell grid, two vec4 per edit.
// e[2k] = (world x, world y, radius, max durability)
// e[2k+1] = (mode, material, probability, 0) mode 0: dig (cells whose durability is at most the limit become air)
// mode 1: fill air cells with the material, each with the given probability
// mode 4: a flask sucks the liquid: the cell becomes air when it still holds material e[2k+1].y
// mode 3: the same with a velocity (a poured flask): e[2k].w = horizontal velocity (cells per frame), e[2k+1].w = vertical velocity
// mode 5: CellEaterComponent (00a639e0): every cell inside the radius whose material is not e[2k+1].y (ignored_material) and not [indestructible]
// (with ignored_material_tag) is removed with probability e[2k+1].z (eat_probability); e[2k].w = 1: only_stain (nothing removed)
// mode 6: MagicConvertMaterialComponent (00bab8c0 / 00baac80): the rings e[2k].z .. e[2k].w (excluded) of the square (e[2k+1].w = 0) or circle (1)
// around the point: a cell of material e[2k+1].y (-1: any material) becomes e[2k+1].z
// mode 7: a line from (e[2k].x, e[2k].y) to (e[2k].z, e[2k].w): its empty cells become material e[2k+1].y (an ArcComponent of type MATERIAL; the host
// cut the line at the first cell of another material)
// mode 8: MaterialSeaSpawnerComponent (00bbc060), two records: (left x, top y, width, first row) (8, material, end row, 0), then
// (sine_wavelength, sine_amplitude, noise_scale, noise_threshold) (0, 0, 0, 0): the empty cells of the rows
// first .. end (excluded) at y = top + row + int(sin(x / wavelength) x amplitude) where 3D simplex noise (00872560:
// x scale, y scale, 0) x 0.5 + 0.5 >= threshold become the material
// mode 10: every non-air cell not e[2k+1].y / .z becomes e[2k+1].z when static, else .y (ConvertEverythingToGold)
// mode 11: the one cell at (x, y) is set on fire with the fade e[2k+1].y: ordinary ignition (0070f5e0: a burnable cell, not burning,
// r % 101 < 100 - autoignition_temperature) or, with e[2k+1].z = 1, forced (0070f750: no draw) - TorchComponent, hack_is_set_fire
// mode 9: AnimalAI eating (00a232e0): the cells of material e[2k+1].y inside the box (x, y) +- (e[2k].z, e[2k].w) become, each with
// probability e[2k+1].z, air (e[2k+1].w = 0) or material e[2k+1].w - 1 (food_particle_effect_material)
// Up to 256 edits per frame.
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after 1024 + 8 words per material: the material tag bits (one word per material, bits assigned by the host)
layout(std430, set = 0, binding = 27) readonly buffer Edits { ivec4 head; vec4 e[512]; } ed; // filled by the host and by the GPU projectiles
#include "simplex3.glsl"
// pass 1: the far blocks (far.glsl) - per edit (z) the cells of a 128 x 128 box around its point outside the window: dig (0), fill (1), the cell eater (5),
// magic convert (6: freezing charge's liquids to ice);
// the slot is marked dirty (main.cpp copies it back to the store)
void far_pass() {
const int k = int(gl_GlobalInvocationID.z);
if (k >= ed.head.x || k >= 256) return;
const vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1];
const int mode = int(r.x + 0.5);
if (mode != 0 && mode != 1 && mode != 5 && mode != 6 && mode != 12) return;
const ivec2 wc = ivec2(floor(q.xy)) - 64 + ivec2(gl_GlobalInvocationID.xy);
const ivec2 lp = wc - ivec2(pc.view_x, pc.view_y);
if (lp.x >= 0 && lp.y >= 0 && lp.x < pc.gw && lp.y < pc.gh) return; // (the window's own pass)
const vec2 w = vec2(wc) + 0.5;
if (mode != 6 && distance(w, q.xy) > q.z) return;
const int s = far_slot(wc);
if (s == 0) return;
const int fi = far_index(s, wc);
const uint c = farcells[fi], m = cell_mat(c);
const uint hi = uint(fi) * 2654435761u;
if (mode == 12) { // a frozen stain (as the window's)
if (m == 0u || (mflags(m) & F_STATIC) == 0u || (mflags(m) & F_FIRE) != 0u || (c & FROZEN_BIT) != 0u) return;
farcells[fi] = c | FROZEN_BIT;
} else if (mode == 6) { // (as the window's: the rings q.z .. q.w of the circle / square, from r.y (-1: any) to r.z)
if (m == 0u) return;
const vec2 d = w - q.xy;
const int ring = r.w > 0.5 ? int(length(d) + 0.5) : int(max(abs(d.x), abs(d.y)));
if (ring < int(q.z) || ring >= int(q.w) || (r.y >= 0.0 && m != uint(r.y)) || m == uint(r.z)) return;
farcells[fi] = uint(r.z) | (((hash3(hi, uint(pc.frame), uint(k) + 41u) >> 16) & 0xFFu) << 16);
} else if (mode == 5) {
if (m == 0u || (r.y >= 0.0 && m == uint(r.y)) || (q.w > 1.5 && (mflags(m) & F_INDESTRUCT) != 0u)) return;
if (r.w > 0.5 && ((tbl[1024u + 8u * mats[mats.length() - 1u].w + m] >> uint(r.w - 1.0)) & 1u) != 0u) return;
if (float(hash3(hi, uint(pc.frame), uint(k) + 37u) % 101u) >= r.z * 100.0 || q.w == 1.0 || q.w == 3.0) return;
farcells[fi] = 0u;
} else if (mode == 0) {
if (m == 0u || uintBitsToFloat(mats[3u * m + 2u].y) > q.w) return;
farcells[fi] = 0u;
} else {
if (m != 0u) return;
const uint h = hash3(hi, uint(pc.frame), uint(k) + 31u);
if (float(h & 0xFFFFu) / 65536.0 >= r.z) return;
farcells[fi] = uint(r.y) | (((h >> 16) & 0xFFu) << 16);
}
farmap[kFarDirty + s - 1] = 1;
}
void main() {
if (pc.pass_ == 1) { far_pass(); return; }
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 m = cell_mat(cells[i]);
vec2 w = vec2(p + ivec2(pc.view_x, pc.view_y)) + 0.5;
for (int k = 0; k < ed.head.x && k < 256; ++k) {
vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1];
if (r.x > 1.5 && r.x < 2.5) continue; // mode 2 (angular explosion) is explode_rays.comp
if (r.x > 10.5 && r.x < 11.5) { // ignite one cell
if (p + ivec2(pc.view_x, pc.view_y) != ivec2(floor(q.xy)) || m == 0u || ((mats[3u * m + 2u].z >> 16) & 1u) == 0u || (cells[i] & BURN_BIT) != 0u) continue;
if (r.z < 0.5 && int(hash3(uint(i), uint(pc.frame), uint(k) + 71u) % 101u) >= 100 - int(mats[3u * m + 2u].w & 255u)) continue;
cells[i] |= BURN_BIT; birth[i] = with_fade(birth[i], int(r.y));
continue;
}
if (r.x > 12.5 && r.x < 14.5) { // the player digs out of the ground (main.cpp, 00bfa850): q = the box; 13: its loose cells fly out as real particles (007175c0: not a
// liquid_static material - the powders and liquids), r.y = the speed s; 14: every cell of the box goes (stuck in the static ground for long)
if (m == 0u || w.x < q.x || w.x > q.z + 1.0 || w.y < q.y || w.y > q.w + 1.0) continue;
if (r.x > 13.5) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; continue; }
if ((mflags(m) & F_STATIC) != 0u || (mflags(m) & (F_LIQUID | F_SAND)) == 0u) continue;
const uint h1 = hash3(uint(i), uint(pc.frame), uint(k) + 81u), h2 = hash3(uint(i), uint(pc.frame), uint(k) + 83u);
const float s = r.y, c = mix(-1.0, -0.65, float(h1 & 0xFFFFu) / 65536.0) * s, b = mix(-0.5, 0.5, float(h1 >> 16) / 65536.0) * s;
const float a = float(h2 & 0xFFFFu) / 65536.0 - 0.5, mul = 0.65 + 0.4 * float(h2 >> 16) / 65536.0;
const vec2 v = vec2(cos(a) * b - sin(a) * c, sin(a) * b + cos(a) * c) * mul / 60.0;
if (emit_material_particle(w, v, 600.0, PART_GRAV, mats[3u * m].x, int(m))) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; }
continue;
}
if (r.x > 11.5 && r.x < 12.5) { // a frozen stain (MagicConvertMaterialComponent stain_frozen: freeze_charge.xml): the static cells inside the radius look frozen (FROZEN_BIT on a static cell)
if (m != 0u && distance(w, q.xy) <= q.z && (mflags(m) & F_STATIC) != 0u && (mflags(m) & F_FIRE) == 0u) cells[i] |= FROZEN_BIT;
continue;
}
if (r.x > 4.5 && r.x < 5.5) { // cell eater
if (m == 0u || distance(w, q.xy) > q.z) continue;
if ((r.y >= 0.0 && m == uint(r.y)) || (q.w > 1.5 && (mflags(m) & F_INDESTRUCT) != 0u)) continue;
if (r.w > 0.5 && ((tbl[1024u + 8u * mats[mats.length() - 1u].w + m] >> uint(r.w - 1.0)) & 1u) != 0u) continue; // ignored_material_tag
if (float(hash3(uint(i), uint(pc.frame), uint(k) + 37u) % 101u) >= r.z * 100.0) continue;
if (q.w == 1.0 || q.w == 3.0) continue; // only_stain: the colour darkens in the engine (not drawn here)
{ // 00a639e0: the eaten cell leaves a cosmetic particle of its material (00716ce0 force-created at the cell, velocity uniform(-30, 30) px/s on each axis,
// lifetime arguments 5 .. 10): the debris in the tunnel of a black hole. TODO(to-do #36): the unit of that lifetime - the real game's debris
// (spell_reference/BH_SPEED_PLAIN.gif) is gone after 0.5 .. 1 s, so 30 .. 60 frames here
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 39u);
const vec2 v = (vec2(float(h & 0xFFFFu), float(h >> 16)) / 65536.0 * 60.0 - 30.0) / 60.0;
emit_particle_g(w, v, 6.0 * (5.0 + 5.0 * float(hash3(uint(i), uint(pc.frame), 41u) & 0xFFFFu) / 65536.0), PART_GRAV, mats[3u * m].x, false, false, true);
}
cells[i] = 0u; ext[i] = vec2(0.0); m = 0u;
continue;
}
if (r.x > 8.5 && r.x < 9.5) { // a creature eats
if (m == 0u || m != uint(r.y) || abs(w.x - q.x) > q.z + 0.5 || abs(w.y - q.y) > q.w + 0.5) continue;
if (float(hash3(uint(i), uint(pc.frame), uint(k) + 53u) & 0xFFFFu) / 65536.0 >= r.z) continue;
if (r.w < 0.5) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; continue; }
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 59u);
m = uint(r.w - 1.0); cells[i] = m | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame();
continue;
}
if (r.x > 5.5 && r.x < 6.5) { // magic convert
if (m == 0u) continue;
const vec2 d = w - q.xy;
const int ring = r.w > 0.5 ? int(length(d) + 0.5) : int(max(abs(d.x), abs(d.y)));
if (ring < int(q.z) || ring >= int(q.w)) continue;
if ((r.y >= 0.0 && m != uint(r.y)) || m == uint(r.z)) continue;
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 41u);
cells[i] = uint(r.z) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.z);
continue;
}
if (r.x > 7.5 && r.x < 8.5) { // a material sea (its parameters are the next record)
const vec4 sp = ed.e[2 * k + 2];
k += 1;
if (m != 0u) continue;
const int xi = int(floor(w.x)), yi = int(floor(w.y));
if (xi < int(q.x) || xi >= int(q.x) + int(q.z)) continue;
const int row = yi - int(q.y) - int(sin(float(xi) / sp.x) * sp.y);
if (row < int(q.w) || row >= int(r.z)) continue;
if (simplex3(vec3(float(xi) * sp.z, float(yi) * sp.z, 0.0)) * 0.5 + 0.5 < sp.w) continue;
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 47u);
cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.y);
continue;
}
if (r.x > 9.5 && r.x < 10.5) { // everything to gold (00704f70: every material but air is redefined as gold, the static ones as gold_static; here the cells take it)
if (m == 0u || m == uint(r.y) || m == uint(r.z)) continue;
const uint g = (mflags(m) & F_STATIC) != 0u ? uint(r.z) : uint(r.y);
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 61u);
cells[i] = g | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = g;
continue;
}
if (r.x > 6.5 && r.x < 7.5) { // a material line
if (m != 0u) continue;
const vec2 ab = q.zw - q.xy;
const float l2 = dot(ab, ab);
const float t = l2 > 0.0 ? clamp(dot(w - q.xy, ab) / l2, 0.0, 1.0) : 0.0;
if (distance(w, q.xy + ab * t) > 0.5) continue;
const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 43u);
cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.y);
continue;
}
if (distance(w, q.xy) > q.z) continue;
const bool moving = r.x > 2.5 && r.x < 3.5;
if (r.x > 3.5) { if (m == uint(r.y)) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; } continue; }
if (r.x < 0.5) {
if (m != 0u && uintBitsToFloat(mats[3u * m + 2u].y) <= q.w) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; }
} else if (m == 0u) {
uint h = hash3(uint(i), uint(pc.frame), uint(k) + 31u);
if (float(h & 0xFFFFu) / 65536.0 < r.z) {
cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16);
ext[i] = moving ? vec2(r.w, q.w) : vec2(0.0); // x = vertical, y = horizontal velocity
birth[i] = birth_frame();
m = uint(r.y);
}
}
}
}
+722
View File
@@ -0,0 +1,722 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "common.glsl"
#include "fx_state.glsl"
#include "fx_host.glsl"
// Entity simulation: gravity, walking, jumping, flying and a basic AI, one invocation per entity, once per frame.
// Entities collide with the live cell grid (solid = static or powder cells). Entities that are not fully inside the
// simulated window are frozen. Kinds: 0 static, 1 walker, 2 flyer, 3 player (index 0, controlled by the host).
struct Ent {
vec4 a; // position xy, velocity xy (cells, cells per frame)
vec4 box; // min_x, max_x, min_y, max_y
vec4 mv; // walk speed, max speed x, jump velocity (negative = up), gravity
vec4 ai; // detection range x, y, melee distance, melee damage
ivec4 st; // kind, facing, timer, flags (1 grounded, 2 can jump, 4 can fly, 8 dead, 16 in liquid)
vec4 hp; // hp, attack cooldown, max fall speed, climb-over height
ivec4 x; // damage taken this frame (milli-hp), jetpack energy / idle flag, spawn x (player) / ranged cooldown, spawn y
ivec4 rg; // ranged attack: projectile type (-1 none), frames between shots, min distance, max distance
ivec4 rg2; // projectiles per shot (min, max), muzzle offset y, the attack of the last attacking tick (1 melee, 2 ranged | tick << 4)
ivec4 aux; // player: maximum hp (milli-hp); creatures: velocity of the last hit (milli-cells per frame)
vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0
vec4 ex2; // ranged attack: projectile speed, gravity
ivec4 ex3; // blood material + 1, blood colour, damage types taken (1 projectile, 2 explosion, 4 fire, 8 melee), 0
vec4 ex4; // air in the lungs (frames), air maximum (0 = no air needed), damage per frame without air, frames left on fire
vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag
vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y)
vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left
ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending
};
struct Proj { vec4 a; vec4 b; vec4 c; }; // pos, vel / damage, life, gravity, colour bits / owner (0 creature, 1 player)
struct PType { vec4 a; vec4 b; }; // speed min, max, damage, lifetime / gravity, colour bits, 0, 0
layout(std430, set = 0, binding = 24) readonly buffer PTypes { PType ptypes[]; };
layout(std430, set = 0, binding = 25) buffer Projs { Proj projs[]; };
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 23) buffer Ctrl {
ivec4 inp; // input x (-1/0/1), jump held, attack pressed this frame, entity count
ivec4 inp2; // frame, has player, attack hits (debug), projectile counter
ivec4 inp3; // aim x, aim y (window cells), fire, player projectile type
vec4 outp; // player: x, y, hp, facing (written by the shader, read by the host)
vec4 outp2; // player: levitation energy 0..1, fire cooldown, 0, 0
vec4 view; // host: camera shift of the last tick (cells), interpolation alpha, gravity scale
vec4 hand0; // host: held wand: aim point (world cells), enabled, mirror
ivec4 hand1; // host: held wand: texel offset, w, h
vec4 hand2; // host: held wand: pivot
ivec4 burnN; // rigid_burn.comp: x the burnt pixels; y: the player's respawns, z: saved by SAVING_GRACE, w: an extra life used (the host reads and clears them)
} ctrl;
const int ST_HOVER = 128; // an item that hovers (a wand in the world): no physics, a small bob (play_hover_animation)
const int ST_SWIMMER = 256; // the immersion probe at ex.w (the integer buoyancy_check_offset_y) is tested
const int ST_GROUNDED = 1, ST_CANJUMP = 2, ST_CANFLY = 4, ST_DEAD = 8, ST_LIQUID = 16, ST_RIGID = 32; // ST_RIGID: moved by the rigid-body engine (rigid_*.comp), only damage is handled here
// rigid bodies are solid for creatures and the player: rigid_raster.comp marks the cells their masks cover (read here with a one-tick delay)
layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; };
layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; }; // per material: gfx_glow in the low 16 bits, the stain statuses it gives in bits 16-21 (WET, OILED, BLOODY, ALCOHOLIC, SLIMY, RADIOACTIVE)
bool body_solid(ivec2 w) {
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) return false;
const uint b = bodyGrid[p.y * pc.gw + p.x];
return b != 0u && (b & 0x80000000u) == 0u; // (bit 31: a body that is no platform - the lanterns and their chains do not block, main.cpp isPlatform)
}
// 00bb6820: the leak emitter's offset is the local hit point, clamped to the HitboxComponent (a corpse part: +-12 here); without one (ex7.w, a prop) as it is, zero beyond 256 px
vec2 leak_offset(Ent e, vec2 rel) { return e.ex7.w > 0.5 ? (dot(rel, rel) > 65536.0 ? vec2(0.0) : rel) : clamp(rel, vec2(-12.0), vec2(12.0)); }
bool solid_cell_at(ivec2 w) {
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) return false; // outside the window there is no ground (such entities are frozen)
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) return true;
return body_solid(w);
}
// The character's cell test 0063ef70: a platform cell counts only when one of its four neighbours (below, left, right, above) is one too - a lone pixel (a crumb of
// rock, a single body pixel) does not stop the player. (Its other exception, the bodies held by telekinesis - 00c2dfc0 stamps their b2Body user data +0x48 with
// the frame - does not apply: TelekinesisComponent is not done.)
bool solid_at(ivec2 w) {
if (!solid_cell_at(w)) return false;
return solid_cell_at(w + ivec2(0, 1)) || solid_cell_at(w + ivec2(-1, 0)) || solid_cell_at(w + ivec2(1, 0)) || solid_cell_at(w + ivec2(0, -1));
}
// What a creature or the player takes by damage type (bit 1 projectile, 2 explosion, 4 fire, 8 melee): the multipliers of the game effects, ex9 = projectile, explosion, fire, melee
float taken_mult(vec4 m, int types) {
if ((types & 2) != 0) return m.y;
if ((types & 8) != 0) return m.w;
if ((types & 4) != 0) return m.z;
return m.x;
}
// The hit flash (00ab5b00, called by TakeDamage 00ab9a90 for damage > 0 that is not fire alone): every sprite of the entity is drawn with
// sprite_damage_highlight (shader 6: white, the sprite's alpha) or, for a critical hit, sprite_damage_critical_hit_highlight (7: red) in its third shader slot;
// at most once per 5 frames (frame - DamageModel +0x238 > 4), and the DamageModelSystem update (00ab6d00) clears the slot at +0x238 + 3: 3 frames of flash.
// ex19.w counts it down: 5 at the hit, drawn while >= 3 (sprites.comp), a new flash at 0; + 8 for a critical hit. (Worm segments keep their root slot there.)
void hit_flash(uint id, bool hit, bool crit) {
const int c = int(ents[id].ex19.w + 0.5);
int n = max((c & 7) - 1, 0);
bool red = (c & 8) != 0;
if (hit && n == 0) { n = 5; red = crit; }
ents[id].ex19.w = n == 0 ? 0.0 : float(n + (red ? 8 : 0));
}
// What an entity deals: the DAMAGE_MULTIPLIER effect (the perk glass cannon) etc.; the host stores the factor in ex7.y (0 = none)
float dealt_mult(Ent s) { return s.ex7.y > 0.0 ? s.ex7.y : 1.0; }
bool liquid_at(ivec2 w) {
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m == 0u) return false;
uint f = mflags(m);
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
}
bool line_clear(vec2 a, vec2 b) { // no solid cell between two points (3-cell steps)
vec2 d = b - a;
int n = int(length(d) / 3.0);
for (int i = 1; i < n; ++i) if (solid_cell_at(ivec2(floor(a + d * (float(i) / float(n)))))) return false;
return true;
}
bool box_hits(ivec2 o, vec4 box) {
ivec2 lo = o + ivec2(int(box.x), int(box.z)), hi = o + ivec2(int(box.y), int(box.w)); // hi exclusive
for (int y = lo.y; y < hi.y; ++y)
for (int x = lo.x; x < hi.x; ++x)
if (solid_at(ivec2(x, y))) return true;
return false;
}
// Blood: real particles of the creature's blood material that fly out along the hit and become cells where they land.
void blood_burst(uint seed, vec2 pos, vec2 dirv, int count, float speed, int matp1, uint colour) {
if (matp1 <= 0) return;
for (int k = 0; k < count && k < 64; ++k) {
float r1 = float(hash3(seed, uint(k), 1u) & 0xFFFFu) / 65536.0, r2 = float(hash3(seed, uint(k), 2u) & 0xFFFFu) / 65536.0;
vec2 v = dirv * (0.4 + r1 * 1.2) * speed + vec2((r2 - 0.5) * 1.4 * speed, -0.3 - r1 * 0.9 * speed);
emit_material_particle(pos + vec2(0.0, -3.0), v, 70.0 + r2 * 40.0, PART_GRAV, colour, matp1 - 1);
}
}
// Liquid of a living hit (00ab9a90): the count does not depend on the damage, N = trunc((u * (40 - 20) + 20) * blood_multiplier + 0.5) (the incoming multiplier and the world
// multiplier are 1); the first droplet leaves along the hit, each next one is turned by a random angle (-0.6..0.6 rad) from the previous one, a speed of 0.5..1.25 times
// the base, at the hit point +-1.5. The velocity is 20 x s x the packet impulse (px/s): a projectile's velocity x ragdoll_force_multiplier (00c091f0), an explosion's radial force (00657960).
void hit_blood(uint seed, vec2 hit, vec2 dirv, float bloodMult, int matp1, uint colour, float speedMul) {
if (matp1 <= 0 || bloodMult <= 0.0) return;
const float u0 = float(hash3(seed, 0u, 11u) & 0x7FFFu) / 32767.0;
int n = int((u0 * 20.0 + 20.0) * bloodMult + 0.5);
vec2 v = dirv;
for (int k = 0; k < n && k < 64; ++k) {
float u1 = float(hash3(seed, uint(k), 12u) & 0x7FFFu) / 32767.0, u2 = float(hash3(seed, uint(k), 13u) & 0x7FFFu) / 32767.0;
float u3 = float(hash3(seed, uint(k), 14u) & 0x7FFFu) / 32767.0, u4 = float(hash3(seed, uint(k), 15u) & 0x7FFFu) / 32767.0;
float sp = 0.5 + 0.75 * u1, a = -0.6 + 1.2 * u2;
v = vec2(v.x * cos(a) - v.y * sin(a), v.x * sin(a) + v.y * cos(a));
emit_material_particle(vec2(hit.x - 1.5 + 3.0 * u4, hit.y - 1.5 + 3.0 * u3), v * (20.0 * sp / 60.0) * speedMul, 70.0 + u2 * 40.0, PART_GRAV, colour, matp1 - 1); // (20 s v px/s, v the packet impulse)
}
}
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits (see edits.comp): flames falling from a burning creature
bool extinguishes_at(ivec2 w) { // water and the like put a burning creature out
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && ((mats[3u * m + 2u].z >> 18) & 1u) != 0u;
}
// script_damage_about_to_be_received (the host sets 65536 in ex8.x when the entity or a child has one): the hp a hit takes goes to a running total instead
// (ex14.w, milli-hp, written only by the entity's own thread); the host runs the scripts on what was added and changes the hp by their result
float take_damage(uint id, float v) {
if ((ents[id].ex8.x & 65536) != 0) { ents[id].ex14.w += v * 1000.0; return 0.0; }
return v;
}
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= uint(ctrl.inp.w)) return;
Ent e = ents[id];
int kind = e.st.x;
if (kind == 0 || (e.st.w & ST_DEAD) != 0) return;
const bool is_prop = kind == 4;
if (is_prop && e.ex7.y > 0.0) { // a corpse part with a liquid inventory: the particle emitter of the ragdoll piece (docs/BLOOD_EMITTER_LIFECYCLE.md)
// ex = (x_vel_min, x_vel_max, y_vel_min, y_vel_max) in px/s (local), ex2 = (next emit frame, mode 0 off / 1 death leakage (1-3 per emission) / 2 damage leakage (1), 0, mass),
// ex7 = (-, reserve, material + 1, -). A damage event (min_damage_to_leak 0.09, leak_on_damage_percent 1 compared with u in [0,1)) rewrites the velocity ranges from the clamped
// local hit offset (00bb6820) and switches to one droplet per emission; the interval is 1-2 frames either way, the schedule advances before the spawn, a request larger than the
// reserve emits nothing, the reserve only falls by the droplets that were created, and a blocked spawn cell ends the burst (emit_only_if_there_is_space).
float reserve = e.ex7.y;
vec4 vr = e.ex;
float nextf = e.ex2.x, mode = e.ex2.y;
const uint fr = uint(ctrl.inp2.x);
const int dmg = atomicExchange(ents[id].ex8.z, 0);
if (dmg > 90) { // 00bb6820: strictly more than min_damage_to_leak (0.09)
const vec2 rel = vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001;
const uint sd = hash3(id, fr, 53u);
float u[8];
for (int k = 0; k < 8; ++k) u[k] = float(hash3(sd, uint(k), 54u) & 0x7FFFu) / 32767.0;
if (u[7] < 1.0) { // u < leak_on_damage_percent (1): fails only for the draw 32767 / 32767
vec2 d = leak_offset(e, rel);
const float dl = length(d);
d = dl > 1e-4 ? d / dl : vec2(0.0);
const float dx = d.x * (0.85 + 0.29999995 * u[0]), dy = d.y * (0.85 + 0.29999995 * u[1]);
const float speed = 100.0 * (0.7 + (1.1 - 0.7) * u[2]);
const float vx = speed * dx, vy = speed * dy;
vr = vec4(vx * (0.6 + 0.39999998 * u[3]), vx * (1.0 + 0.39999998 * u[4]), vy * (0.6 + 0.39999998 * u[5]), vy * (1.0 + 0.39999998 * u[6]));
mode = 2.0;
}
}
if (mode > 0.5 && float(fr) >= nextf && e.ex7.z > 0.0) {
const uint seed = hash3(id, fr, 51u);
nextf = float(fr) + ((e.ex2.z > 0.5 && mode < 1.5) ? 3.0 + float(hash3(seed, 3u, 58u) % 3u) : 1.0 + float(seed & 1u)); // the next emission is scheduled before anything is spawned (3-5 frames when the cosmetic spray exists and no damage has rewritten the emitter)
const int cnt = mode > 1.5 ? 1 : 1 + int(hash3(seed, 7u, 56u) % 3u);
if (reserve >= float(cnt)) {
const float ca = cos(e.a.z), sa = sin(e.a.z);
int made = 0;
for (int k = 0; k < cnt; ++k) {
const uint r = hash3(seed, uint(k), 52u);
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 57u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 57u) & 0xFFFFu) / 65536.0;
const vec2 lo = leak_offset(e, vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001);
const vec2 off = mode > 1.5 ? vec2(ca * lo.x - sa * lo.y, sa * lo.x + ca * lo.y) : vec2(0.0); // the damage event's offset (the hit point), turned with the body
const vec2 at = e.a.xy + off + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py); // rect (-1,-1) .. (1,1)
if (solid_cell_at(ivec2(floor(at)))) break;
vec2 v = vec2(mix(vr.x, vr.y, ux), mix(vr.z, vr.w, uy)) * (1.0 / 60.0); // px/s -> cells/frame
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y); // the entity's rotation
if (emit_material_particle(at, v, 70.0 + float((r >> 24) & 0x3Fu), PART_GRAV, uint(e.ex3.y), int(e.ex7.z) - 1)) ++made; // only the particles that were created cost reserve (00bc75e0)
}
reserve -= float(made);
}
}
if (e.ex2.z > 0.5 && e.ex7.z > 0.0 && float(fr) >= e.ex6.z) { // blood_spray_create_some_cosmetic: a second emitter on the same reserve, 1-3 cosmetic droplets (no cells) every 1-2 frames
const uint seed2 = hash3(id, fr, 59u);
ents[id].ex6.z = float(fr) + 1.0 + float(seed2 & 1u);
const int cnt2 = 1 + int(hash3(seed2, 7u, 56u) % 3u);
if (reserve >= float(cnt2)) {
float u2[8];
for (int k = 0; k < 8; ++k) u2[k] = float(hash3(seed2, uint(k), 60u) & 0x7FFFu) / 32767.0;
const vec2 d = e.ex6.xy * vec2(0.85 + 0.29999995 * u2[0], 0.85 + 0.29999995 * u2[1]); // the same formula as the death emitter, other draws
const float base = (360.0 + 0.7 * 1200.0 * u2[2]) * e.ex2.w, speed = 0.7 * base + (1.1 * base - 0.7 * base) * u2[3];
const vec2 v0 = speed * d;
const vec4 cr = vec4(v0.x * (0.8 + 0.19999999 * u2[4]), v0.x * (1.0 + 0.20000005 * u2[5]), v0.y * (0.8 + 0.19999999 * u2[6]), v0.y * (1.0 + 0.20000005 * u2[7]));
const float ca = cos(e.a.z), sa = sin(e.a.z);
int made = 0;
for (int k = 0; k < cnt2; ++k) {
const uint r = hash3(seed2, uint(k), 61u);
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 62u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 62u) & 0xFFFFu) / 65536.0;
const vec2 at = e.a.xy + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py);
if (solid_cell_at(ivec2(floor(at)))) break;
vec2 v = vec2(mix(cr.x, cr.y, ux), mix(cr.z, cr.w, uy)) * (1.0 / 60.0);
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y);
if (emit_particle_hit(at, v, 25.0 + float((r >> 24) & 0x1Fu), 0.1, uint(e.ex3.y), int(e.ex7.z) - 1) != 0xFFFFFFFFu) ++made; // a droplet without a cell: it fades in the air and stains where it lands (it owns no cell: 00716ce0)
}
reserve -= float(made);
}
}
ents[id].ex = vr; ents[id].ex2.x = nextf; ents[id].ex2.y = mode; ents[id].ex7.y = max(reserve, 0.0);
}
vec2 pos = e.a.xy, vel = e.a.zw;
ivec2 ip = ivec2(floor(pos));
// frozen unless the collision box (plus margin) is inside the simulated window
ivec2 wp = ip - ivec2(pc.view_x, pc.view_y);
if (wp.x < 3 || wp.y < 3 || wp.x > pc.gw - 3 || wp.y > pc.gh - 3) return;
uint rnd = hash3(id, uint(ctrl.inp2.x), 911u);
int flags = e.st.w;
int types = 0; // damage types taken this tick (creatures)
vec2 hitv = vec2(0.0); // direction of the hit
float dmgTick = 0.0; // damage taken this tick
int dir = e.st.y;
int timer = e.st.z;
float hp = e.hp.x, cooldown = max(e.hp.y - 1.0, 0.0);
int energy = e.x.y;
// pushed out of solid cells (sand piles landing on it, an explosion refilling, a wall appearing): find the nearest free spot above or beside
const bool rigidBody = is_prop && (flags & ST_RIGID) != 0; // moved by the rigid-body engine: its own mask must not count as an obstacle
if (kind != 0 && !rigidBody && box_hits(ip, e.box)) {
bool freed = false;
for (int k = 1; k <= 12 && !freed; ++k) {
const ivec2 cand[5] = ivec2[5](ivec2(0, -k), ivec2(-k, -k), ivec2(k, -k), ivec2(-k, 0), ivec2(k, 0));
for (int c = 0; c < 5 && !freed; ++c)
if (!box_hits(ip + cand[c], e.box)) { ip += cand[c]; pos = vec2(float(ip.x) + fract(pos.x), float(ip.y)); vel.y = min(vel.y, 0.0); freed = true; }
}
}
bool grounded = !rigidBody && box_hits(ip + ivec2(0, 1), e.box);
const bool wasGrounded = grounded;
bool liquid = liquid_at(ip);
if (kind == 1 || kind == 2 || kind == 3) { // stains: standing in water, oil, blood... leaves its status on the creature for a while (ex8.z: bits | frame << 8)
uint sm = 0u;
for (int sy = 0; sy < 2; ++sy) {
const ivec2 sp = ip + ivec2(0, -4 * sy) - ivec2(pc.view_x, pc.view_y);
if (sp.x >= 0 && sp.y >= 0 && sp.x < pc.gw && sp.y < pc.gh) sm |= (gtab[cell_mat(cells[sp.y * pc.gw + sp.x])] >> 16) & 63u;
}
if (sm != 0u) {
const uint old = ((uint(ents[id].ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(ents[id].ex8.z) & 63u) : 0u;
ents[id].ex8.z = int((sm | old) | (uint(pc.frame & 0xFFFFFF) << 8));
}
}
// swimming (CharacterPlatformingComponent swim_*): with the buoyancy check point (CharacterDataComponent buoyancy_check_offset_y) under liquid a walker or
// the player is nearly weightless (swim_idle_buoyancy_coeff), every velocity is damped (swim_drag, swim_extra_horizontal_drag) and the up key swims up
const bool swimming = (kind == 1 || kind == 3) && (e.st.w & ST_SWIMMER) != 0 && liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))); // float coordinates truncate toward zero (00a7ea90)
float walk = e.mv.x, vmax = e.mv.y;
if (e.ex15.z > 0.5 && (int(e.ex15.w) & 32) != 0) walk *= 2.0; // the run button of a creature the host runs (00a25d50: it has a prey or a threat; 00a7ea90 doubles the speed)
float target = 0.0;
bool want_jump = false, thrust = false;
// a flyer's ControlsComponent: mButtonDownFly and mFlyingTargetY (ex18.z), set by the path steer (00a29b20 -> 00a8d340) and kept between them
bool flyOn = kind == 2;
float flyTY = e.ex18.w < -1.0e8 ? pos.y : e.ex18.z;
vec4 pl = ents[0].a; // the player (if there is one)
bool has_player = ctrl.inp2.y != 0;
bool player_alive = has_player && (ents[0].st.w & ST_DEAD) == 0;
const bool frozen = (e.ex8.x & 1) != 0; // a game effect with disable_movement (FROZEN, PARALYSIS...): no walking, jumping, attacking
if (kind == 3) { // ---- the player ----
target = frozen ? 0.0 : float(ctrl.inp.x) * walk;
vec2 aim_w = vec2(ctrl.inp3.xy) + vec2(float(pc.view_x), float(pc.view_y)); // aim point in world cells
if (ctrl.inp3.x >= 0) dir = aim_w.x >= pos.x ? 1 : -1; else if (ctrl.inp.x != 0) dir = ctrl.inp.x; // face the mouse
cooldown = max(cooldown - 0.0, 0.0);
if (ctrl.inp3.z != 0 && !frozen && cooldown <= 0.0 && ctrl.inp3.w >= 0) { // fire the spark bolt toward the mouse
PType t = ptypes[ctrl.inp3.w];
vec2 from = pos + vec2(0.0, -3.0);
vec2 dv = aim_w - from;
vec2 dirv = length(dv) > 0.5 ? normalize(dv) : vec2(float(dir), 0.0);
uint r2 = hash3(id, uint(ctrl.inp2.x), 77u);
float ang = (float(r2 & 255u) / 255.0 - 0.5) * 0.04;
dirv = vec2(dirv.x * cos(ang) - dirv.y * sin(ang), dirv.x * sin(ang) + dirv.y * cos(ang));
uint slot = uint(atomicAdd(ctrl.inp2.w, 1)) % 512u;
projs[slot].a = vec4(from, dirv * t.a.y);
projs[slot].b = vec4(t.a.z, t.a.w, t.b.x, t.b.y);
projs[slot].c = vec4(1.0, 0.0, 0.0, 0.0);
cooldown = 9.0;
}
want_jump = ctrl.inp.y != 0 && !frozen;
{ // the jetpack (CharacterDataComponent fly_time_max, fly_recharge_spd, fly_recharge_spd_ground): flying burns one frame of fuel per frame, on the ground it refills at 6 seconds per second,
// in the air the fuel trickles back at fly_recharge_spd as soon as the key is up (units of 1/64 frame)
const float emax = (e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0;
float en = float(energy);
thrust = want_jump && !grounded && en >= 64.0; // the jetpack works in liquids too (the swim correction comes after it)
if (grounded) en = min(emax, en + 6.0 * 64.0);
else if (thrust) en -= 64.0;
else en = min(emax, en + (e.ex2.y > 0.0 ? e.ex2.y : 0.4) * 64.0);
energy = int(en);
}
// damage taken from enemies this frame
int dmg = atomicExchange(ents[id].x.x, 0);
const float hpBeforeHit = hp;
{ const int pty = atomicExchange(ents[id].ex3.z, 0); hp -= take_damage(id, float(dmg) * 0.001); if (pty != 0) ents[id].ex3.w = (pty & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); hit_flash(id, dmg > 0, (pty & 0x10000) != 0); } // (the accumulator already holds the multiplied damage: every source applies the victim's multiplier of its type) (ex3.w: the types of this tick | tick << 8, for the damage sounds)
// TakeDamage (00ab9a90) on a lethal hit: SAVING_GRACE (effect 0x2a) leaves 1 / 25 hp (1 HP shown) when there was more, else an unspent RESPAWN (0x2d, extra life: ex8.x
// bit 131072, the host clears it) sets 100 / 25 hp, at most the maximum; the host is told (ctrl burnN.z / w) for the message and the PROTECTION_ALL of 60 / 30 frames
if (hp <= 0.0 && (e.ex8.x & 16) != 0 && hpBeforeHit > 0.04) { hp = 0.04; atomicAdd(ctrl.burnN.z, 1); }
else if (hp <= 0.0 && (ents[id].ex8.x & 131072) != 0) { hp = min(4.0, e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0); atomicAnd(ents[id].ex8.x, ~131072); atomicAdd(ctrl.burnN.w, 1); }
if (hp <= 0.0 && (e.ex8.x & 32768) == 0) { // respawn at the spawn point (a polymorphed body is not: the host ends the polymorph, 32768)
pos = vec2(float(e.x.z), float(e.x.w)); vel = vec2(0.0); ip = ivec2(floor(pos));
hp = e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0;
atomicAdd(ctrl.burnN.y, 1);
}
} else if (kind == 1 || kind == 2) { // ---- creatures ----
// the target of the creature: the player; a charmed creature (game effect CHARM) fights the nearest creature of another herd instead
vec4 tp = pl; bool tAlive = player_alive; int tgt = 0;
const bool berserk = (e.ex8.x & 4) != 0 && (e.ex8.x & 2) == 0; // BERSERK (00a28e10): it attacks every mortal, the player and its own herd included, unless berserk_dont_attack_friends (bit 3)
if ((e.ex8.x & 2) != 0 || berserk) {
float best = 1.0e9;
tgt = -1;
for (int k = berserk ? 0 : 1; k < ctrl.inp.w; ++k) {
if (k == int(id) || (ents[k].st.w & ST_DEAD) != 0 || (ents[k].ex8.x & 8192) != 0) continue; // (an invisible one is not seen)
if (k == 0 ? (!berserk || ents[0].st.x != 3) : (ents[k].st.x != 1 && ents[k].st.x != 2)) continue;
if (berserk ? ((e.ex8.x & 8) != 0 && ents[k].ex8.y == e.ex8.y) : ents[k].ex8.y == e.ex8.y) continue;
const float dk = distance(ents[k].a.xy, pos);
if (dk < best && abs(ents[k].a.x - pos.x) < e.ai.x && abs(ents[k].a.y - pos.y) < e.ai.y) { best = dk; tgt = k; }
}
tAlive = tgt >= 0;
if (tgt >= 0) tp = ents[tgt].a; else tgt = 0;
}
float dx = tp.x - pos.x, dy = tp.y - pos.y;
// sees the player: within the detection range and, for creatures with a limited field of view (creature_detection_angular_range_deg), inside the cone in front of them
vec2 toP = vec2(tp.x, tp.y - 4.0) - vec2(pos.x, pos.y + e.ex.y);
bool inCone = e.ex.x <= -0.999 || (length(toP) > 1.0 && dot(normalize(toP), vec2(float(dir), 0.0)) >= e.ex.x);
bool sees = tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && abs(dx) < e.ai.x && abs(dy) < e.ai.y && inCone;
types = atomicExchange(ents[id].ex3.z, 0); // damage types taken since the last tick
timer -= 1;
if (timer <= 0) { // pick a new idle/wander action
uint r = rnd;
if ((r % 3u) == 0u) timer = 20 + int((r >> 4) % 50u);
else { dir = ((r >> 8) & 1u) == 0u ? -1 : 1; timer = 60 + int((r >> 4) % 120u); }
e.x.y = (r % 3u) == 0u ? 1 : 0; // 1 = idling for this action
}
bool idle = e.x.y == 1;
if (sees) { dir = dx >= 0.0 ? 1 : -1; idle = false; }
if (e.ex15.z > 0.5) {
// the host's AnimalAI (src/ai, AnimalAISystem 00a22540 + PathFindingSystem 00bca030): mode 1 heads for the next node of its path (a flyer in both axes), mode 2 stands;
// the attacks below stay the GPU's
target = 0.0;
if ((int(e.ex15.w) & 2) != 0 || (int(e.ex15.w) & 8) != 0) target = vel.x; // (from the launch on, also on the ground: a lob down off a ledge starts into the floor and slides off the edge at its speed) // a jump of the path: no control in the air (a dash: for its whole flight time)
else if (e.ex15.z < 1.5) {
const float sdx = e.ex15.x - pos.x;
if (abs(sdx) > 1.0) { dir = sdx > 0.0 ? 1 : -1; target = float(dir) * walk; }
// a flyer (00a29b20): the fly key with the node's y as mFlyingTargetY; a can_walk flyer with ground within 10 px below and the node not above it lets go
// of the key and walks (the host's flag 16)
if (kind == 2) { flyOn = (int(e.ex15.w) & 16) == 0; if (flyOn) flyTY = e.ex15.y; }
// (a fish swims up and down by its buoyancy: the swim correction below)
}
} else {
if (!idle) target = float(dir) * walk;
if (kind == 1) {
// do not walk off ledges while wandering; turn around at walls (handled below), jump after the player
if (!sees && grounded && !idle) {
ivec2 ahead = ip + ivec2(dir * 5, 0);
if (!box_hits(ahead + ivec2(0, 4), e.box) && !box_hits(ahead + ivec2(0, 8), e.box)) { dir = -dir; target = float(dir) * walk; }
}
} else if (sees) flyTY = tp.y - 8.0; // flyer without the host's AI: the fly key, toward the player's height when it sees one (else it holds the last height)
}
{ // hits: projectiles and explosions (damage accumulator), the blood sprays out along the hit
int dmg = atomicExchange(ents[id].x.x, 0);
hp -= take_damage(id, float(dmg) * 0.001); // (already multiplied at the source by the victim's multiplier of the damage type)
dmgTick += float(dmg) * 0.001;
hit_flash(id, dmg > 0, (types & 0x10000) != 0);
if (dmg > 0) {
hitv = vec2(float(atomicExchange(ents[id].aux.x, 0)), float(atomicExchange(ents[id].aux.y, 0))) * 0.001;
hit_blood(hash3(id, uint(ctrl.inp2.x), 31u), pos + vec2(0.0, 0.5 * (e.box.z + e.box.w)), hitv, e.ex7.x, e.ex3.x, uint(e.ex3.y), 1.0); // (hitv: the packet impulse, px/s)
}
}
// melee attack
if (tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && e.ex.z > 0.5 && abs(dx) < e.ai.z && abs(dy) < e.ai.z + 4.0 && cooldown <= 0.0) {
atomicAdd(ents[tgt].x.x, int(e.ai.w * dealt_mult(e) * ents[tgt].ex9.w * 1000.0)); // the target's melee multiplier
if (e.ai.w > 0.0 && e.ai.w * dealt_mult(e) * ents[tgt].ex9.w <= 0.0 && ents[tgt].st.x >= 1 && ents[tgt].st.x <= 3) ents[tgt].ex11.w = float(pc.frame); // (a blocked hit: BLOCK on the host)
ents[id].rg2.w = 1 | ((ctrl.inp2.x & 0xFFFFFF) << 4);
atomicOr(ents[tgt].ex3.z, 8);
ents[tgt].ex8.w = int(id + 1) | ((pc.frame & 0xFFFFF) << 12); // the target was hit by this creature
cooldown = 50.0;
}
// ranged attack: cast the creature's projectile (a node of the GPU projectile system, team 2) at the player when in range with a clear line
int rcd = max(e.x.z - 1, 0);
if (e.rg.x >= 0 && tAlive && !frozen) {
vec2 from = pos + vec2(0.0, float(e.rg2.z)), to = tp.xy + vec2(0.0, -4.0);
float dist = length(to - from);
if (sees && rcd == 0 && dist >= float(e.rg.z) && dist <= float(e.rg.w) && line_clear(from, to)) {
int count = e.rg2.x + int(rnd % uint(e.rg2.y - e.rg2.x + 1));
float speed = max(e.ex2.x, 0.1), tt = dist / speed;
vec2 aimv = (to - from) / tt + vec2(0.0, -0.5 * e.ex2.y * tt); // launch velocity that compensates the gravity over the flight time
for (int k = 0; k < count; ++k) {
uint r2 = hash3(id, uint(ctrl.inp2.x), uint(k) + 5u);
float ang = (float((r2 >> 8) & 255u) / 255.0 - 0.5) * (0.08 + 0.05 * float(count - 1));
vec2 d0 = normalize(aimv);
vec2 dirv = vec2(d0.x * cos(ang) - d0.y * sin(ang), d0.x * sin(ang) + d0.y * cos(ang));
int list = pc.frame & 1;
int slot = list == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1);
if (slot < 256) {
hcast[(list * 256 + slot) * 2] = vec4(from, dirv);
hcast[(list * 256 + slot) * 2 + 1] = vec4(float(e.rg.x), float(2 + (e.ex8.y << 3)), float(r2 & 0xFFFFFFu), float(1 + (id + 1) * 2)); // r1.w: muzzle flash flag, the shooter's slot + 1 in the higher bits
}
}
rcd = e.rg.y;
ents[id].rg2.w = 2 | ((ctrl.inp2.x & 0xFFFFFF) << 4); // the attack of the tick (1 melee, 2 ranged | tick << 4): the host plays its sprite events (attack sounds)
}
}
e.x.z = rcd;
// (the player's kick on creatures is the host's: KickSystem 00bf2040, 18 frames after the press - its damage goes through the damage accumulator)
if (hp <= 0.0) flags |= ST_DEAD;
}
if (frozen) { target = 0.0; want_jump = false; thrust = false; }
// soft separation: creatures and the player do not stack on top of each other
// (characters do not push each other: the engine has no creature-creature collision - CharacterCollisionComponent only collides with the world)
// ---- damage: projectiles and explosions (the atomic accumulator), contact with harmful materials, fire ----
if (is_prop && e.hp.x < 1.0e8) {
int dmg = atomicExchange(ents[id].x.x, 0);
hp -= float(dmg) * 0.001; // (the material damage of 00ab6010 / 00ab7410 comes through this accumulator from the host)
hit_flash(id, dmg > 0, (atomicExchange(ents[id].ex3.z, 0) & 0x10000) != 0);
if (hp <= 0.0) flags |= ST_DEAD;
}
if (kind == 1 || kind == 3 || kind == 2) { // fire hurts: cells of fire sources or burning cells overlapping the box
int burn = 0;
ivec2 lo = ip + ivec2(int(e.box.x), int(e.box.z)), hi = ip + ivec2(int(e.box.y), int(e.box.w));
for (int y = lo.y; y < hi.y; ++y)
for (int x = lo.x; x < hi.x; ++x) {
ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
uint cw = cells[q.y * pc.gw + q.x];
uint cm = cell_mat(cw);
if (cm != 0u && (((mats[3u * cm + 2u].z >> 17) & 1u) != 0u || (cw & BURN_BIT) != 0u)) ++burn;
}
if (burn > 0) types |= 4; // (touching fire does no damage of its own: only the materials of materials_that_damage and the burning do)
// ---- burning (DamageModelSystem): ignition in the cell scan 00ab6010, the burning update 00ab7a70 ----
// ignition (fire_probability_of_ignition != 0): the cells under the body that burn or are of an on_fire material (on_fire counts 2, not the creature's own sparks)
// add up; above 4, rand(0, 100) < the strength (fade) of such a cell sets mFireFramesLeft to max(left, seconds x 60): 12 s, 0 with a status that protects from
// fire (WET, OILED, BLOODY, SLIMY, RADIOACTIVE), 20 s ALCOHOLIC, 30 s OILED (in that order, the last one wins)
const uint stains = ((uint(e.ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(e.ex8.z) & 63u) : 0u;
const bool fireProt = (stains & (1u | 2u | 4u | 16u | 32u)) != 0u;
float fireLeft = e.ex4.w, fireProb = e.ex16.z;
if (e.ex2.z > 0.0) {
int cnt = 0, fade = 0;
ivec2 lo2 = ip + ivec2(int(e.box.x), int(e.box.z)), hi2 = ip + ivec2(int(e.box.y), int(e.box.w));
for (int y = lo2.y; y < hi2.y; ++y)
for (int x = lo2.x; x < hi2.x; ++x) {
ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
uint cw = cells[q.y * pc.gw + q.x];
uint cm = cell_mat(cw);
if (cm == 0u) continue;
const bool onFireMat = ((mats[3u * cm + 2u].z >> 17) & 1u) != 0u;
if (!onFireMat && (cw & BURN_BIT) == 0u) continue;
cnt += onFireMat ? 2 : 1;
int f = int(birth[q.y * pc.gw + q.x]) >> 24; // (the cell's fade, its strength: a fire cell without one has its temperature_of_fire)
if (f == 0 && (cw & BURN_BIT) == 0u) f = int((mats[3u * cm + 2u].w >> 8) & 255u);
fade = f;
}
if (cnt > 4 && int(hash3(id, uint(ctrl.inp2.x), 61u) % 101u) < fade) {
float secs = 12.0;
if (fireProt) secs = 0.0;
if ((stains & 8u) != 0u) secs = 20.0;
if ((stains & 2u) != 0u) secs = 30.0;
fireLeft = max(fireLeft, floor(secs * 60.0 + 0.5));
if (e.ex4.w <= 0.0 && fireLeft > 0.0) fireProb = 50.0; // (mIsOnFire changes: mFireProbability 50)
}
}
if (fireLeft > 0.0) {
// 00ab7a70: fire_how_much_fire_generates times a random point of the hitbox: an empty cell gets a spark (the creature's own: it does not ignite it), a burning cell
// keeps the fire going (mFireProbability + 1, at most 100), each liquid cell met counts 3 against it when more than half of the tries met liquid; out when
// mFireProbability or mFireFramesLeft (-1 a frame, -2 with a fire-protecting status, +1 OILED) reaches 0. The damage while it burns: max(0.8, max hp) x 0.02
// per second, twice that for anything but the player (fire_damage_amount only switches it on; the engine delivers the sum every 9 - 12 frames, here each frame)
const int nTry = max(int(e.ex2.z) - 1, 0);
int lit = 0, wet = 0;
const uint spark = mats[mats.length() - 2u].x;
for (int k = 0; k < nTry && k < 10; ++k) {
const float rx = float(hash3(id, uint(ctrl.inp2.x), 70u + uint(k)) & 0xFFFFu) / 65536.0, ry = float(hash3(id, uint(ctrl.inp2.x), 90u + uint(k)) & 0xFFFFu) / 65536.0;
const ivec2 w = ivec2(floor(pos + vec2(e.box.x + (e.box.y - e.box.x) * rx, e.box.z + (e.box.w - e.box.z) * ry)));
const ivec2 q = w - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
const int qi = q.y * pc.gw + q.x;
uint cw = cells[qi];
if (cell_mat(cw) == 0u && spark != 0u && atomicCompSwap(cells[qi], 0u, spark | (hash3(id, uint(k), 65u) & 0xFF0000u)) == 0u) { birth[qi] = birth_frame(); lit = 1; continue; }
cw = cells[qi];
const uint cm = cell_mat(cw);
if (cm != 0u && ((cw & BURN_BIT) != 0u || (mflags(cm) & F_FIRE) != 0u)) lit = 1;
else if (cm != 0u && (mflags(cm) & F_LIQUID) != 0u) wet += 3;
}
fireProb = min(fireProb + float(lit), 100.0);
if (nTry / 2 < wet) fireProb -= float(wet);
fireLeft -= 1.0; // (the extra -1 of a fire-protecting status and the +1 of OILED: the host, from the status system's stain amounts)
if (lit == 1 && e.ex2.w != 0.0) {
const float maxhp = kind == 3 ? float(e.aux.x) * 0.001 : float(e.x.w) * 0.001;
hp -= take_damage(id, max(0.8, maxhp) * 0.02 / 60.0 * (kind == 3 ? 1.0 : 2.0) * e.ex9.z);
}
types |= 4;
if (fireProb < 1.0 || fireLeft < 1.0) { fireLeft = 0.0; fireProb = 0.0; }
}
ents[id].ex16.z = fireProb;
ents[id].ex4.w = fireLeft;
// air (00ab81b0, frames here): with the head in liquid air_in_lungs runs down 1 per frame; out of it (ex16.w counts the frames) it refills 1 per frame, 5 after
// 20 frames, up to air_in_lungs_max; at 0.2 s or less of air (also while it refills) air_lack_of_damage x dt is lost each frame
if (e.ex4.y > 0.0) {
float air = e.ex4.x, outF = e.ex16.w;
if (liquid_at(ip + ivec2(0, int(e.box.z)))) { outF = 0.0; air -= 1.0; }
else { outF = min(outF + 1.0, 1.0e6); if (air < e.ex4.y) air = min(e.ex4.y, air + (outF > 20.0 ? 5.0 : 1.0)); }
if (air <= 12.0) { air = max(air, 0.0); hp -= take_damage(id, e.ex4.z); types |= 32; }
ents[id].ex4.x = air; ents[id].ex16.w = outF;
}
if (kind != 3 && hp <= 0.0) flags |= ST_DEAD;
if ((types & (1 | 2 | 8)) != 0) ents[id].ex3.w = (types & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); // the damage types of this tick | tick << 8 (the host's damage sounds, 00ab9a90)
}
if ((kind == 1 || kind == 2) && (flags & ST_DEAD) != 0 && (e.st.w & ST_DEAD) == 0) { // it dies now: blood bursts, the host spawns the corpse and runs the death scripts
float maxhp = float(e.x.w) * 0.001;
// KillMe (00abb7d0): the RAGDOLL_FX of the killing hit (bits 12-15; the packet's default NORMAL), a NORMAL projectile or explosion kill turns into BLOOD_SPRAY when
// rand % 100 + 1 <= 20 (01153024); BLOOD_EXPLOSION (2) makes the parts without joints: the gib (ragdoll_fx_forced is the host's)
int rfx = (types >> 12) & 15;
if (rfx == 0 && (types & 3) != 0) rfx = 1;
if (rfx == 1 && (types & 3) != 0 && int(hash3(id, uint(ctrl.inp2.x), 77u) % 100u) + 1 <= 20) rfx = 3;
bool gib = rfx == 2;
float hl = length(hitv);
vec2 hd = hl > 0.01 ? hitv / hl : vec2(0.0, -1.0);
int n = 14 + min(int(maxhp * 30.0), 24);
// no burst of its own: the lethal hit already made its liquid, the corpse parts carry the rest of the blood (finite reserves, see the part branch above)
ents[id].ex3.w = (types & 0xEF) | (gib ? 16 : 0) | ((ctrl.inp2.x & 0xFFFFF) << 8) | (rfx << 28); // (bits 28-31: the RAGDOLL_FX of the death)
ents[id].aux.z = int(hitv.x * 1000.0); ents[id].aux.w = int(hitv.y * 1000.0);
}
if (is_prop && (flags & ST_HOVER) != 0) {
const vec2 home = e.ex6.zw;
ents[id].a = vec4(home.x, home.y + 2.0 * sin(3.0 * float(pc.frame) / 60.0), 0.0, 0.0); // 00b83980 (ItemSystem, play_hover_animation): home y + 2 sin(3 t), t in seconds; the sprite turned by -pi/2
return;
}
if (is_prop && (flags & ST_RIGID) != 0) {
ents[id].st = ivec4(kind, dir, timer, flags);
ents[id].hp = vec4(hp, cooldown, e.hp.z, e.hp.w);
return;
}
// ---- physics ----
bool fly = kind == 2 && flyOn;
if (kind == 2) ents[id].ex18.zw = vec2(flyTY, fly ? (e.ex18.w < -1.0e8 ? flyTY : (e.ex18.y > 0.5 ? e.ex18.w + (flyTY - e.ex18.w) * 0.05 : flyTY)) : -1.0e9);
if (fly && (int(e.ex15.w) & 8) == 0) { // (a flyer's dash: the host's velocity for its flight)
// 00a7ea90, fly_model_player 0: mSmoothedFlyingTargetY eases to mFlyingTargetY by 0.05 a frame (fly_smooth_y; set at once when the key goes down); the vertical speed
// goes toward clamp(fly_speed_mult x (smoothed - y) - g dt, -fly_speed_max_up, fly_speed_max_down) by fly_speed_change_spd (snapped at the target), then the gravity
const float sY = ents[id].ex18.w;
const float tv = clamp(e.ex17.x * (sY - pos.y) / 60.0 - e.mv.w, -e.ex17.z, e.ex17.w);
const float nv = vel.y + e.ex17.y * (tv - vel.y);
vel.y = (sign(tv - nv) != sign(tv - vel.y)) ? tv : nv;
vel.y += e.mv.w;
} else if (!fly) {
vel.y += e.mv.w * ctrl.view.w; // gravity; the swim correction (docs/WATER_PHYSICS.md) follows the input staging below
if (kind == 4) vel *= 1.0 - e.ex5.x; // 00c370c0: air friction of the item's VelocityComponent (per frame) after gravity, then the liquid drag, then the terminal speed (vector)
else vel.y = min(vel.y, e.hp.z);
if (grounded && vel.y > 0.0) vel.y = 0.0; // resting on the ground: no gravity build-up (it made the sub-cell height creep 0.97 and snap back every 5 ticks)
}
// The knockback state of the platforming update (00a7ea90 lines 384-433): after a hit with knockback the horizontal velocity stays the kick's x (+0x148) and the controls are off; it ends
// after at least 3 frames when the body is on the ground (is_on_ground), stood in liquid at the buoyancy probe (x, y + buoyancy_check_offset_y) or collided horizontally
// the frame before (mCollidedHorizontally), and for the player after 60 frames (+0x14c, set to 60 by the kick 00ab9910; precision_jumping_max_duration_frames is
// -1 = no limit in every file)
vec4 kst = e.ex10; // the knockback state: x = the kick's horizontal velocity, y = frames since, z = 1 in the state (2 when it also hit a wall the frame before)
if ((kind == 1 || kind == 3) && (e.ex11.y != 0.0 || e.ex11.z != 0.0)) { // a queued kick (00a7ea90 lines 384-395): added to the staged velocity, CONFUSION inverts its x, it becomes the state's horizontal velocity
vec2 kk = e.ex11.yz;
if ((e.ex8.x & 128) != 0) kk.x = -kk.x;
vel += kk;
kst = vec4(kk.x, 0.0, 1.0, e.ex10.w);
ents[id].ex11.yz = vec2(0.0);
}
bool knocked = (kind == 1 || kind == 3) && kst.z > 0.5;
if (knocked) {
const float cnt = kst.y + 1.0;
if ((cnt >= 3.0 && (grounded || kst.z > 1.5 || liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))))) || (kind == 3 && cnt >= 60.0)) { ents[id].ex10.xyz = vec3(0.0); knocked = false; }
else { ents[id].ex10.xyz = vec3(kst.x, cnt, 1.0); vel.x = kst.x; }
}
if (!knocked) {
if (kind == 3 && thrust) { const float lf = (e.ex8.x & 16384) != 0 ? 1.75 : 1.0; vel.y = max(vel.y - 0.3 * lf, -1.45 * lf); } // levitation (FASTER_LEVITATION 0x4b: the two flying speeds of CharacterPlatformingComponent x 1.75)
else if (want_jump && grounded && kind != 2 && (kind == 3 || (flags & ST_CANJUMP) != 0)) vel.y = e.mv.z; // jump
if (kind == 2 && fly) target = sign(target) * e.ex18.x; // (flying: fly_velocity_x instead of run_velocity, 00a7ea90)
if (kind == 1 || kind == 2 || kind == 3) { // 00a7ea90: v += accel_x x (target - v) each frame (accel_x_air while flying in the air), the target when it would pass it
const float f = ((kind == 3 && thrust) || fly) && !grounded ? e.ex16.y : e.ex16.x;
const float nv = vel.x + f * (target - vel.x);
vel.x = (sign(target - nv) != sign(target - vel.x)) ? target : nv;
} else vel.x += clamp(target - vel.x, -0.12, 0.12);
// (the lob of a path jump keeps its speed from the launch until the host ends the jump: on the launch tick the creature still stands on the ground - capped
// there, a hop flew at walking speed and fell back where it started, and a lob down off a ledge never left it: zombies jumping in place)
if (!(((kind == 1) && (int(e.ex15.w) & 2) != 0) || ((kind == 1 || kind == 2) && (int(e.ex15.w) & 8) != 0))) vel.x = clamp(vel.x, -vmax, vmax); // (a dash keeps its speed for its flight, a flyer's too) // (the lob of a path jump keeps its speed)
}
// player splash (00a7ea90 / 00a809f0), queued BEFORE the swim correction below (the speed it tests and throws is the one before buoyancy and drag): the first swimming frame after more than 15 frames out of the liquid, faster than 40 px/s, falling, not on the ground: the 11x11 square
// around the position throws its liquid forward, v = s Rot(a) v_entity, s = 0.1 + 0.7 u1, a = 0.4 u2 - 0.2 (the original draws two numbers for every one of the 121 places)
if (kind == 3) {
float fns = e.ex6.y;
if (swimming) {
if (fns > 15.0 && length(vel) * 60.0 > 40.0 && vel.y > 0.0 && !grounded)
for (int dy = -5; dy <= 5; ++dy) for (int dx = -5; dx <= 5; ++dx) {
const uint rs = hash3(uint(id) * 131u + uint(dx + 5 + 11 * (dy + 5)), uint(pc.frame), 73u);
const float s1 = 0.1 + 0.7 * float(rs & 0x7FFFu) / 32767.0, a = 0.4 * float((rs >> 15) & 0x7FFFu) / 32767.0 - 0.2;
displace_cell(ivec2(pos) + ivec2(dx, dy), s1 * (mat2(cos(a), sin(a), -sin(a), cos(a)) * vel));
}
fns = 0.0;
} else fns += 1.0;
ents[id].ex6.y = fns;
}
if (swimming && !fly) {
// 00a7ea90: after gravity, input, jetpack: b = down ? swim_down : fly key ? swim_up : swim_idle; vx = extra * (drag * vx); vy = drag * (vy - b * G), G = pixel_gravity * dt
const float G = e.mv.w * ctrl.view.w;
// Down has priority over Fly (the down key is ControlsComponent.mButtonDownDown, the up one mButtonDownFly): down gives swim_down_buoyancy_coeff, fly swim_up_, nothing swim_idle_
const bool downKey = kind == 3 && ctrl.inp3.z != 0 && !frozen;
const float fishDy = (int(e.ex15.w) & 4) != 0 && e.ex15.z > 0.5 && e.ex15.z < 1.5 ? e.ex15.y - pos.y : 0.0; // a fish's up / down buttons (AdvancedFishAI's ControlsComponent)
const float b = kind == 3 ? (downKey ? e.ex6.x : (want_jump ? e.ex5.y : e.ex5.x)) : (fishDy > 2.0 ? e.ex6.x : (fishDy < -2.0 ? e.ex5.y : (want_jump && !grounded ? e.ex5.y : e.ex5.x)));
vel.x = e.ex5.w * (e.ex5.z * vel.x);
vel.y = e.ex5.z * (vel.y - b * G);
}
// 00c370c0 counters: only when the integer position changed, 3x3 count of liquid that is not powder; displacement 75 % of the time when on the ground
{
ivec2 last = ivec2(e.ex6.zw);
const ivec2 tp = ivec2(pos); // truncation toward zero (00c370c0)
if (tp != last) {
int n = 0;
for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(tp + ivec2(dx, dy))) ++n;
ents[id].ex6.z = float(tp.x); ents[id].ex6.w = float(tp.y);
if (n > 0 && kind == 4) vel *= 1.0 - e.ex5.y * (1.0 / 60.0) * float(n); // 00c370c0: an item with a VelocityComponent loses liquid_drag * dt * N of its velocity (unclamped)
if (n > 0 && (kind == 1 || kind == 2) && !swimming && e.ex10.w > 0.0) vel *= 1.0 - e.ex10.w * (1.0 / 60.0) * float(n); // 00c370c0 liquid drag of creatures that do not swim (liquid_drag of VelocityComponent, default 1)
if (n > 0 && grounded) { // every qualifying cell: r % 101 < 75, then it leaves as a particle with -Rot(a) * 0.1 v, a = 0.6 u - 0.3 (the velocity of a cell per frame is the velocity per second / 60 for both)
for (int dy = 1; dy >= -1; --dy) for (int dx = -1; dx <= 1; ++dx) {
const uint rs = hash3(uint(id) * 9u + uint(dx + 1 + 3 * (dy + 1)), uint(pc.frame), 71u);
if (rs % 101u >= 75u) continue;
const float ang = 0.6 * float((rs >> 8) & 0x7FFFu) / 32767.0 - 0.3;
displace_cell(tp + ivec2(dx, dy), -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel)));
}
}
}
}
if (kind == 4) { const float sp = length(vel); if (sp > e.hp.z) vel *= e.hp.z / sp; } // 00c370c0: the terminal speed limits the vector length and rescales both axes
// horizontal move in one-cell steps, climbing small steps when standing
float nx = pos.x + vel.x;
int steps = int(floor(nx)) - ip.x;
int sx = steps > 0 ? 1 : -1;
bool blocked = false;
for (int s = 0; s < abs(steps); ++s) {
ivec2 np = ip + ivec2(sx, 0);
if (!box_hits(np, e.box)) { ip = np; continue; }
bool ok = false;
if (grounded) { // (a flyer skimming the floor climbs its small steps too: it stopped dead at a plank's edge)
for (int k = 1; k <= int(e.hp.w) && !ok; ++k) {
ivec2 cp = np - ivec2(0, k);
if (!box_hits(cp, e.box)) { ip = cp; ok = true; }
}
}
if (!ok) { blocked = true; break; }
}
pos.x = blocked ? float(ip.x) + fract(pos.x) : nx;
if (blocked && !(kind == 1 && (int(e.ex15.w) & 2) != 0)) vel.x = 0.0;
if (knocked) ents[id].ex10.z = blocked ? 2.0 : 1.0; // (mCollidedHorizontally for the knockback end) // (the lob of a path jump keeps its speed against a wall: it rises past a steep slope's edge and goes on, not straight up and back down where it started)
// vertical move
pos.y = float(ip.y) + fract(pos.y);
float ny = pos.y + vel.y;
int vsteps = int(floor(ny)) - ip.y;
int sy = vsteps > 0 ? 1 : -1;
bool hit = false;
for (int s = 0; s < abs(vsteps); ++s) {
ivec2 np = ip + ivec2(0, sy);
if (!box_hits(np, e.box)) { ip = np; continue; }
hit = true;
break;
}
if (hit) { pos.y = float(ip.y); vel.y = 0.0; } else pos.y = ny;
grounded = box_hits(ip + ivec2(0, 1), e.box);
if (wasGrounded && !grounded && (kind == 1 || kind == 3) && vel.y > -0.05 && vel.y < 0.6 && !swimming) { // walking down a slope: the body follows the ground (up to the climb height) instead of hopping from step to step
for (int k = 1; k <= int(max(e.hp.w, 4.0)); ++k)
if (!box_hits(ip + ivec2(0, k), e.box) && box_hits(ip + ivec2(0, k + 1), e.box)) { ip.y += k; pos.y = float(ip.y); vel.y = 0.0; grounded = true; break; }
}
// creatures: turn around at walls, hop over obstacles when they can jump - only the GPU's own fallback walkers: a creature the host runs (ex15.z) moves by its
// path alone (its jumps are the AnimalAI's prepared lobs; this random 3-in-4 hop at -jump_velocity_y was a second controller, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-04)
if (kind == 1 && blocked && e.ex15.z <= 0.5) {
if (grounded && (flags & ST_CANJUMP) != 0 && (e.x.y == 0) && (rnd & 3u) != 0u) vel.y = e.mv.z;
else dir = -dir;
}
flags = (flags & ~(ST_GROUNDED | ST_LIQUID)) | (grounded ? ST_GROUNDED : 0) | (liquid ? ST_LIQUID : 0);
if ((e.st.w & 1024) != 0) { pos = e.a.xy; vel = e.a.zw; } // moved by the host (a worm: WormSystem): only its damage and death are handled here
ents[id].a = vec4(pos, vel);
ents[id].st = ivec4(kind, dir, timer, flags);
ents[id].hp = vec4(min(hp, kind == 3 && e.aux.x > 0 ? float(e.aux.x) * 0.001 : 1e9), cooldown, e.hp.z, e.hp.w);
ents[id].x.y = kind == 3 ? energy : e.x.y;
if (kind != 3) ents[id].x.z = e.x.z;
if (kind == 3) { ctrl.outp = vec4(pos, hp, float(dir)); ctrl.outp2 = vec4(float(energy) / ((e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0), cooldown, e.ex4.y > 0.0 ? ents[id].ex4.x / e.ex4.y : 1.0, ents[id].ex4.w > 0.0 ? 1.0 : 0.0); }
}
+16
View File
@@ -0,0 +1,16 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "common.glsl"
// Remembers the entity positions before a tick: sprites, projectiles and particles are drawn between the last two ticks.
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; } ctrl;
layout(std430, set = 0, binding = 39) buffer EntPrevW { vec4 prevw[]; };
void main() {
uint i = gl_GlobalInvocationID.x;
if (i >= uint(ctrl.inp.w)) return;
prevw[i] = vec4(ents[i].a.xy, 0.0, 0.0);
}
+124
View File
@@ -0,0 +1,124 @@
#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();
}
}
}
+18
View File
@@ -0,0 +1,18 @@
// Far blocks: 64 x 64 cell blocks of the world outside the simulated window, kept on the GPU (not simulated) for the projectiles that leave it - a black hole
// with accelerating shots eats on through them instead of meeting a wall at the window's edge. main.cpp makes them resident ahead of such projectiles (from the
// persistent store, the rest generated by terrain.comp shift_mode 3), edits.comp pass 1 applies the edits there, the dirty ones go back to the store.
// FarMap: (origin block x, y, map width, height), then a slot + 1 per block of the map (0 = not resident), then a dirty flag per slot, then the generation
// jobs: a count, then (slot, block x, block y, store-backed) each.
const int kFarMapW = 48, kFarMapH = 48, kFarSlots = 512, kFarJobs = 64;
const int kFarDirty = 4 + kFarMapW * kFarMapH, kFarJobCount = kFarDirty + kFarSlots, kFarJobList = kFarJobCount + 4;
layout(std430, set = 0, binding = 61) buffer FarCells { uint farcells[]; };
layout(std430, set = 0, binding = 62) buffer FarMap { int farmap[]; };
// the slot + 1 of the block of world cell w (0: none)
int far_slot(ivec2 w) {
const ivec2 b = (w >> 6) - ivec2(farmap[0], farmap[1]);
if (b.x < 0 || b.y < 0 || b.x >= farmap[2] || b.y >= farmap[3]) return 0;
return farmap[4 + b.y * farmap[2] + b.x];
}
bool far_in_map(ivec2 w) { const ivec2 b = (w >> 6) - ivec2(farmap[0], farmap[1]); return b.x >= 0 && b.y >= 0 && b.x < farmap[2] && b.y < farmap[3]; }
int far_index(int slot1, ivec2 w) { return (slot1 - 1) * 4096 + (w.y & 63) * 64 + (w.x & 63); }
+112
View File
@@ -0,0 +1,112 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
// Fog of war, as FogOfWarRadiusSystem (lambda 00d8f000 -> 00aeef70): a persistent map of the world in 32 x 32 pixel texels (the engine's fog byte, 255 = fog, 0 = clear;
// kept here as its complement, the "clear" amount, so that the zeroed buffer is all fog). It only clears: what was seen stays seen. Every frame, for the player
// (FogOfWarRadiusComponent radius 200, data/entities/misc/fogofwar_radius.xml; the position lifted by 4 x uniform(0, 3) pixels):
// * the 3 x 3 texels around it: fog = min(fog, max(fog - 3 d, 255 d)), d = the distance from the texel centre in texels;
// * 36 rays (10 degrees apart) of the radius, stopped by the terrain: at 0, 1/4, 2/4 and 3/4 of the free part of each ray the texel loses 5 of fog
// (a texel crossed by several rays loses more; 51 frames clear one entirely).
// render.comp reads the map bilinearly (the texel centres at 32 n + 16) as post_final.frag reads tex_fog.
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 49) buffer Fog { uint fog[]; }; // per texel: 255 - the fog byte (the clear amount)
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after the fire palettes: the biome map's width, height and fog_of_war_type per tile (main.cpp)
#include "biome_mods.glsl"
uint fog_type_at(vec2 w) { // 0 default, 1 HEAVY_CLEAR_AT_PLAYER, 2 HEAVY_CLEAR_WITH_MAGIC, 3 HEAVY_NO_CLEAR; outside the map: the default
const uint n = mats[mats.length() - 1u].w;
const uint fb = 1024u + 9u * n + 132u + n;
const int bw = int(tbl[fb]), bh = int(tbl[fb + 1u]);
const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14);
if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return 0u;
return tbl[fb + 2u + uint(t.y * bw + t.x)];
}
const int FOG_TW = 1152, FOG_TH = 896, FOG_OX = 18432, FOG_OY = 14336; // texels of 32 pixels from the world origin (-18432, -14336)
const float RADIUS = 256.0; // the player's (component_documentation: "256 is the default player has"; the X-ray card's fogofwar_radius.xml has 200)
bool solid_at(ivec2 w) { // (world cell; outside the window: open)
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) return false;
const uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
}
int texel_index(vec2 w) {
const ivec2 t = ivec2(floor((w + vec2(FOG_OX, FOG_OY)) / 32.0));
if (t.x < 0 || t.y < 0 || t.x >= FOG_TW || t.y >= FOG_TH) return -1;
return t.y * FOG_TW + t.x;
}
// the solid share of the 64 x 64 pixel region of a point, 0..255 (the coarse map of the world the 3 x 3 rule reads; outside the window: 0)
float coarse_at(vec2 w) {
const ivec2 r0 = ivec2(floor(w / 64.0)) * 64;
int n = 0, tot = 0;
for (int y = 2; y < 64; y += 4) for (int x = 2; x < 64; x += 4) {
const ivec2 p = r0 + ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
++tot;
const uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) ++n;
}
return tot > 0 ? 255.0 * float(n) / float(tot) : 0.0;
}
void main() {
// one workgroup per source: 0 the player (radius 256), then the other FogOfWarRadiusComponents (main.cpp, after the holes in the tables)
const uint t = gl_LocalInvocationIndex;
if (pc.pass_ != 0) return;
if (t >= 45u) { // 00c42d30: the biome's fog_of_war_delta (FOG_OF_WAR_REAPPEARS: 2) - every frame the fog bytes of the 17 x 17 texels around the player (ceil(427 / 32 x 0.5) + 1 = 8 each way) gain it
if (gl_WorkGroupID.x != 0u || (ents[0].st.w & 8) != 0) return;
const vec2 P0 = ents[0].a.xy;
const int delta = int(bitfieldExtract(int(bm_bytes(P0)), 0, 8)); // (a signed byte)
if (delta == 0) return;
for (uint k = t - 45u; k < 289u; k += 19u) {
const int ti = texel_index(P0 + vec2(float(int(k % 17u) - 8), float(int(k / 17u) - 8)) * 32.0);
if (ti < 0) continue;
uint old = fog[ti];
for (int tries = 0; tries < 4; ++tries) { const uint nv = uint(clamp(int(min(old, 255u)) - delta, 0, 255)); const uint got = atomicCompSwap(fog[ti], old, nv); if (got == old) break; old = got; } // (fog[] holds 255 - the fog byte)
}
return;
}
const uint wg = gl_WorkGroupID.x;
vec2 P;
float R = RADIUS;
if (wg == 0u) {
if ((ents[0].st.w & 8) != 0) return; // (dead)
P = ents[0].a.xy;
} else {
const uint n = mats[mats.length() - 1u].w;
const uint sb = 1024u + 9u * n + 132u + n + 2u + 8192u + 1u + 64u * 6u;
if (int(wg) > int(uintBitsToFloat(tbl[sb]))) return;
const uint o = sb + 1u + (wg - 1u) * 3u;
P = vec2(uintBitsToFloat(tbl[o]), uintBitsToFloat(tbl[o + 1u]));
R = uintBitsToFloat(tbl[o + 2u]);
}
P -= vec2(0.0, 4.0 * float(hash3(uint(pc.frame) + wg * 131u, 61u, 7u) & 3u));
if (t < 36u) { // a ray
const float a = float(t) * 0.17453294;
const vec2 dir = vec2(cos(a), sin(a));
float len = R;
for (int s = 1; s < int(R); ++s) if (solid_at(ivec2(floor(P + dir * float(s))))) { len = float(s); break; }
for (int k = 0; k < 4; ++k) {
const vec2 sw = P + dir * (len * 0.25 * float(k));
if (fog_type_at(sw) != 0u) continue; // only the default fog of war clears along the rays
const int i = texel_index(sw);
if (i >= 0 && fog[i] < 255u) atomicAdd(fog[i], 5u);
}
return;
}
// the 3 x 3 texels around the player
const int k = int(t) - 36;
const vec2 tc = (floor((P + vec2(FOG_OX, FOG_OY)) / 32.0) + vec2(float(k % 3 - 1), float(k / 3 - 1))) * 32.0 + 16.0 - vec2(FOG_OX, FOG_OY);
const int i = texel_index(tc);
if (i < 0 || fog_type_at(tc) > 1u) return; // the default and HEAVY_CLEAR_AT_PLAYER clear around the player
const float d = distance(P, tc) / 32.0;
const float old = 255.0 - float(min(fog[i], 255u));
float target = 255.0 * d;
if (k % 3 != 1 && k / 3 != 1) target = max(target, (coarse_at(tc) * 16.0 / 1024.0 - d) * 255.0); // the four diagonal texels: a solid region stays fogged (the coarse byte x 16 / 1024)
const float nf = min(old, max(old - 3.0 * d, clamp(target, 0.0, 255.0)));
atomicMax(fog[i], uint(255.0 - nf));
}
+69
View File
@@ -0,0 +1,69 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "fx_common.glsl"
// Explosion sprites and flashes: advance, draw the animated sprite (centred) and add a fading light.
// One workgroup per animation record (the dispatch is FANIM_ALL groups): its 64 threads share the sprite's pixels. (One thread per record drew a big
// explosion sprite alone - 100 000 pixels and more - and three bombs took 15 ms of GPU time.)
void main() {
const uint id = gl_WorkGroupID.x, lane = gl_LocalInvocationID.x;
const bool drawOnly = pc.pass_ == 1; // between ticks nothing advances
if (gl_GlobalInvocationID.x == 0u && !drawOnly) lpad[0] = lpad[0] * 5u / 6u; // camera shake decays
if (id >= uint(FANIM_ALL)) return;
FAnim a = fanims[id];
barrier(); // (every lane has read the record before lane 0 advances it)
if (a.b.z <= 0) return;
int age = drawOnly ? a.b.y : a.b.y + 1;
if (age >= a.b.z) { if (!drawOnly && lane == 0u) fanims[id].b.z = 0; return; }
float k = 1.0 - float(age) / float(a.b.z);
if (!drawOnly && lane == 0u) {
fanims[id].b.y = age;
const float lk = a.c.y > 0.0 ? max(0.0, 1.0 - float(age) / a.c.y) : k; // (c.y: the light's own fade frames)
if (a.a.w > 0.0 && lk > 0.0) add_light(a.a.xy, a.a.z, a.a.w * lk, rgb_of(uint(a.b.w)));
}
if (a.b.x < 0) return;
int b = BOOM_BASE + a.b.x * BOOM_STRIDE;
ivec4 b0 = fti(b), b1 = fti(b + 1);
vec4 b3 = ftf(b + 3);
if (b0.y <= 0 || b0.z <= 0) return;
int frame = age / max(b1.x, 1);
if (frame >= b0.w) return;
// the sprite is placed with its pivot at the effect position and rotated by the angle (muzzle flashes point along the shot)
// c.z: a scale (0: 1; negative: drawn behind - only where no cell and no sprite is, the render_back sprite particles of the host, main.cpp); c.w: a tint (rgb) and alpha
// (bits 24-31) as bits (0: none); a host record (tinted) has its own y scale in c.y (the GPU's records keep their light's fade there)
const uint tw = floatBitsToUint(a.c.w);
const bool tinted = tw != 0u;
const float sx0 = a.c.z == 0.0 ? 1.0 : abs(a.c.z);
const vec2 sclv = max(vec2(sx0, tinted ? a.c.y : sx0), vec2(0.01));
const float scl = max(sclv.x, sclv.y);
const bool behind = a.c.z < 0.0;
const vec3 tint = vec3(float((tw >> 16) & 255u), float((tw >> 8) & 255u), float(tw & 255u)) / 255.0;
const float alpha = tinted ? float(tw >> 24) / 255.0 : 1.0;
vec2 pivot = b3.zw, size = vec2(b0.yz);
float ca = cos(a.c.x), sa = sin(a.c.x);
float r = (length(max(pivot, size - pivot)) + 1.5) * scl;
int n = int(2.0 * r) + 2;
ivec2 lo = ivec2(floor(a.a.xy - vec2(r)));
bool additive = (b1.w & 8) != 0;
for (int j = int(lane); j < n * n; j += 64) {
ivec2 w = lo + ivec2(j % n, j / n);
vec2 d = vec2(w) + 0.5 - a.a.xy;
ivec2 sp = ivec2(floor(pivot + vec2(ca * d.x + sa * d.y, -sa * d.x + ca * d.y) / sclv));
if (sp.x < 0 || sp.y < 0 || sp.x >= b0.y || sp.y >= b0.z) continue;
ivec2 q = w - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
uint t = fxtex[b0.x + frame * b0.y * b0.z + sp.y * b0.y + sp.x];
if ((t >> 24) == 0u) continue;
const int qi = q.y * pc.gw + q.x;
if (behind && (cell_mat(cells[qi]) != 0u || entlayer[qi] != 0u)) continue; // (behind the terrain and the sprites already drawn)
if (tinted) {
const vec3 c = vec3(float((t >> 16) & 255u), float((t >> 8) & 255u), float(t & 255u)) / 255.0 * tint;
const float al = float(t >> 24) / 255.0 * alpha;
if (al < 0.02) continue;
if (additive) t = 0x01000000u | packUnorm4x8(vec4(c.b * al, c.g * al, c.r * al, 0.0));
else t = (uint(al * 255.0) << 24) | (packUnorm4x8(vec4(c.b, c.g, c.r, 0.0)) & 0xFFFFFFu);
entlayer[qi] = t;
} else entlayer[qi] = additive ? (0x01000000u | (t & 0xFFFFFFu)) : t;
}
}
+208
View File
@@ -0,0 +1,208 @@
// Shared declarations of the GPU wand/projectile/effect system (proj_spawn, proj_step, fx_particles, fx_anims, rigid_events).
// The CPU only runs the spell script (gun.lua) and uploads its result: tables of projectile types, effect definitions, and the shot tree
// (nodes and specs) of every cast. Flight, collisions, triggers, explosions, damage, emitters, particles, lights and animations run here.
#include "common.glsl"
#include "far.glsl"
struct Ent {
vec4 a, box, mv, ai;
ivec4 st;
vec4 hp;
ivec4 x, rg, rg2, aux;
vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0
vec4 ex2; // ranged attack: projectile speed, gravity
ivec4 ex3; // blood material + 1, blood colour, damage types taken since the last check (bit 1 projectile, 2 explosion, 4 fire, 8 melee), 0
vec4 ex4; // air in the lungs (frames), air maximum (0 = does not need air), damage per frame without air, frames left on fire
vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag
vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y)
vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left
ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending
};
layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; };
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // entity positions before the last tick (for interpolation)
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits, two vec4 each (see edits.comp)
layout(std430, set = 0, binding = 33) readonly buffer FxTables { uvec4 ft[]; }; // projectile types, explosion effects, emitters (words, see gun/gpu_fx.h)
layout(std430, set = 0, binding = 34) readonly buffer FxTex { uint fxtex[]; }; // sprite frames (ARGB) of projectiles and explosions
#include "fx_host.glsl"
#include "fx_state.glsl"
// rigid bodies (read only): projectiles stop at them
layout(std430, set = 0, binding = 30) readonly buffer RShapes { uint rsh[]; };
struct RBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0; vec4 ja1; vec4 ja2; vec4 ja3; };
layout(std430, set = 0, binding = 31) readonly buffer RBodies { ivec4 rhdr; RBody rbodies[]; };
layout(std430, set = 0, binding = 37) buffer Xev { ivec4 xhead; vec4 xe[512]; }; // explosion events for the rigid bodies: x, y, radius, power
struct FLight { vec4 a; vec4 b; }; // x, y, radius, strength / rgb
layout(std430, set = 0, binding = 38) buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; };
const int TYPE_STRIDE = 18, BOOM_BASE = 9216, BOOM_STRIDE = 4, EMIT_BASE = 9472, EMIT_STRIDE = 4, LOAD_BASE = 13568; // (512 types: the scripts' variants of a projectile are types of their own)
float uf(uint u) { return uintBitsToFloat(u); }
vec4 ftf(int i) { return uintBitsToFloat(ft[i]); }
ivec4 fti(int i) { return ivec4(ft[i]); }
float rnd01(uint h) { return float(h & 0xFFFFu) / 65536.0; }
bool solid_at(ivec2 w) {
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) { // outside the window: a resident far block (far.glsl), else a wall
const int s = far_slot(w);
if (s == 0) return true;
const uint fm = cell_mat(farcells[far_index(s, w)]);
return fm != 0u && (mflags(fm) & (F_STATIC | F_SAND)) != 0u;
}
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
}
void add_light(vec2 pos, float radius, float strength, vec3 rgb) {
uint i = atomicAdd(lcount, 1u);
if (i < 512u) { lights[i].a = vec4(pos, radius, strength); lights[i].b = vec4(rgb, 0.0); }
}
uint add_anim(vec2 pos, int boom, float radius, float strength, int maxage, uint rgb, float angle) { // the record
uint s = atomicAdd(fcnt[2], 1u) % uint(FANIM_MAX);
fanims[s].a = vec4(pos, radius, strength);
fanims[s].b = ivec4(boom, 0, maxage, int(rgb & 0xFFFFFFu));
fanims[s].c = vec4(angle, 0.0, 0.0, 0.0);
return s;
}
void add_edit(vec4 q, vec4 r) {
uint i = uint(atomicAdd(ehead.x, 1));
if (i < 256u) { ee[2u * i] = q; ee[2u * i + 1u] = r; }
}
// A world point inside a rigid body's pixel mask? (the grid is filled by rigid_raster.comp)
layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; };
bool body_hit(vec2 w) {
ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y);
return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh && bodyGrid[p.y * pc.gw + p.x] != 0u;
}
// The entity slot of the rigid body that covers a world point, -1 when none.
int body_slot(vec2 w) {
ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return -1;
return int(bodyGrid[p.y * pc.gw + p.x] & 0x7FFFFFFFu) - 1; // (bit 31: the characters-pass mark of rigid_raster.comp)
}
// Who damaged an entity last: the slot + 1 of the attacker (0 = unknown) in the low 12 bits of ex8.w, the frame in the rest. The host reads it when the hp drops (script_damage_received).
void note_attacker(int victim, int attacker_plus_1) {
if (attacker_plus_1 > 0 && attacker_plus_1 < 4096) ents[victim].ex8.w = attacker_plus_1 | ((pc.frame & 0xFFFFF) << 12);
}
// A hit that the victim's multipliers took to nothing (a protection): the frame in ex11.w of a creature or the player, for the host's BLOCK message (00b071f0)
void note_block(int victim, float raw, float dealt) {
const int kind = ents[victim].st.x;
if (raw > 0.0 && dealt <= 0.0 && (kind == 1 || kind == 2 || kind == 3)) ents[victim].ex11.w = float(pc.frame);
}
// A damage event on a body with a liquid inventory: the corpse part emits (entities.comp).
void body_damage(int slot, vec2 hit, float dmg) {
if (slot < 0 || ents[slot].ex7.y <= 0.0) return;
atomicAdd(ents[slot].ex8.z, max(1, int(dmg * 1000.0)));
const vec2 r = hit - ents[slot].a.xy; const float ca = cos(ents[slot].a.z), sa = sin(ents[slot].a.z);
const vec2 rel = vec2(ca * r.x + sa * r.y, -sa * r.x + ca * r.y); // in the body's frame (entities.comp turns the leak back with the rotation)
atomicExchange(ents[slot].ex8.x, int(rel.x * 1000.0)); atomicExchange(ents[slot].ex8.y, int(rel.y * 1000.0));
}
// The damage multiplier the perks give a shooter; applied ONCE when the shot is made (Message_Shot, 00b06870: projectile damage, and the explosion damage unless not_scaled_by_gamefx)
float dealt_of(int shooter) {
if (shooter <= 0) return 1.0;
const Ent s = ents[shooter - 1];
return (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) && s.ex7.y > 0.0 ? s.ex7.y : 1.0;
}
// The liquid material at a position (0 when there is none).
uint liquid_at_pos(vec2 pos) {
ivec2 p = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return 0u;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m == 0u) return 0u;
uint f = mflags(m);
return ((f & F_LIQUID) != 0u && (f & (F_SAND | F_STATIC)) == 0u) ? m : 0u;
}
// A projectile pushes the liquid cells around its position aside (to the nearest free cell away from it).
void displace_liquid(vec2 pos) {
{ ivec2 p0 = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); if (p0.x < 1 || p0.y < 1 || p0.x >= pc.gw - 1 || p0.y >= pc.gh - 1) return;
uint m0 = cell_mat(cells[p0.y * pc.gw + p0.x]); if (m0 == 0u || (mflags(m0) & F_LIQUID) == 0u) return; } // only when the projectile is inside a liquid
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx) {
ivec2 w = ivec2(floor(pos)) + ivec2(dx, dy);
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;
int i = p.y * pc.gw + p.x;
uint c = cells[i];
uint m = cell_mat(c);
if (m == 0u) continue;
uint f = mflags(m);
if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) continue;
vec2 d = vec2(w) + 0.5 - pos;
d = length(d) < 0.01 ? vec2(0.0, -1.0) : normalize(d);
for (int r = 1; r <= 6; ++r) {
ivec2 t = w + ivec2(round(d * float(r)));
ivec2 q = t - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) break;
int ti = q.y * pc.gw + q.x;
if (atomicCompSwap(cells[ti], 0u, c) == 0u) { ext[ti] = ext[i]; cells[i] = 0u; ext[i] = vec2(0.0); break; }
}
}
}
vec3 rgb_of(uint c) { return vec3(float((c >> 16) & 255u), float((c >> 8) & 255u), float(c & 255u)) / 255.0; }
// Explosion visuals: animated sprite, light flash, sparks and material sparks.
// radius >= 0: the explosion's own radius (a shot with Explosive Projectile: the file's 2 + 15 per modifier); the light is that x light_radius_coeff (00657960), the table
// holds it for the file's radius
void boom_fx(int boom, vec2 pos, uint seed, float radius) {
int b = BOOM_BASE + boom * BOOM_STRIDE;
ivec4 b0 = fti(b), b1 = fti(b + 1), b2 = fti(b + 2);
vec4 b3 = ftf(b + 3);
float rad = b3.x;
atomicMax(lpad[0], uint(b3.y * 1500.0)); // camera shake (milli-cells), decays in fx_anims
int frames = max(b0.w, 1), wait = max(b1.x, 1);
int maxage = b0.z > 0 ? frames * wait + 2 : 9;
// the light (ConfigExplosion light_*): radius explosion_radius x light_radius_coeff, colour light_r/g/b, fading over light_fade_time (b1.w bits 4-15 / 16-27)
const bool lightOn = (b1.w & 4) != 0;
float lrad = float((uint(b1.w) >> 4) & 0xFFFu);
if (radius >= 0.0) lrad = rad > 0.0 ? lrad * radius / rad : radius * 8.0; // (the coefficient default 8: 004b2780)
lrad = min(lrad, 4095.0);
const int lframes = max(1, int((uint(b1.w) >> 16) & 0xFFFu));
const uint an = add_anim(pos, boom, lightOn ? max(lrad, 9.0) : 0.0, lightOn ? 0.6 : 0.0, max(max(maxage, 9), lightOn ? lframes : 0), uint(b1.y), 0.0);
if (lightOn) fanims[an].c.y = float(lframes); // (the light fades over its own time, the sprite plays over its own)
const int sp0 = int(uint(b2.x) & 0xFFu);
const uint sparkRgb = uint(b2.x) >> 8;
int ns = (b1.w & 1) != 0 ? sp0 + int(rnd01(hash3(seed, 1u, 7u)) * float(b2.y - sp0 + 1)) : 0;
for (int i = 0; i < ns && i < 64; ++i) {
float a = rnd01(hash3(seed, uint(i), 11u)) * 6.2832, sp = 0.8 + rnd01(hash3(seed, uint(i), 12u)) * 2.4;
emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.6), 18.0 + rnd01(hash3(seed, uint(i), 13u)) * 27.0, 0.1, sparkRgb, true, true);
}
int nm = (b1.w & 2) != 0 ? b2.z + int(rnd01(hash3(seed, 2u, 8u)) * float(b2.w - b2.z + 1)) : 0;
if ((b1.w & 10) == 10 && b0.z == 0) { // an ambient emitter (a portal): material sparks spread over a disc of the radius, drifting away from its centre, no gravity, long-lived, additive
for (int i = 0; i < nm && i < 160; ++i) {
float a = rnd01(hash3(seed, uint(i), 31u)) * 6.2832, rr = sqrt(rnd01(hash3(seed, uint(i), 32u))) * rad, sp = 0.05 + rnd01(hash3(seed, uint(i), 33u)) * 0.25;
vec2 d = vec2(cos(a), sin(a));
emit_particle(pos + d * rr, d * sp, 150.0 + rnd01(hash3(seed, uint(i), 34u)) * 60.0, 0.0, uint(b1.z), true, true);
}
nm = 0;
}
for (int i = 0; i < nm && i < 64; ++i) {
float a = rnd01(hash3(seed, uint(i), 21u)) * 6.2832, sp = 0.5 + rnd01(hash3(seed, uint(i), 22u)) * 1.5;
emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.4), 20.0 + rnd01(hash3(seed, uint(i), 23u)) * 30.0, 0.12, uint(b1.z), true, true);
}
}
// The damage of a projectile hit on entity k (TakeDamage_Impl 00ab9a90: every damage type goes through the victim's multiplier of that type): the projectile channel, the
// damage_by_type channels of the type (table words 12-14: melee, electricity, fire, ice / slice, drill, radioactive, poison / healing, curse, holy, explosion) and the wand's adds (f.x electricity, f.y ice)
float hit_damage(int k, float proj, int tb, vec4 f) {
const Ent v = ents[k];
const vec4 c0 = ftf(tb + 12), c1 = ftf(tb + 13), c2 = ftf(tb + 14);
return proj * v.ex9.x
+ c0.x * v.ex9.w + (c0.y + f.x) * v.ex12.x + c0.z * v.ex9.z + (c0.w + f.y) * v.ex12.w
+ c1.x * v.ex12.z + c1.y * v.ex12.y + c1.z * v.ex13.z + c1.w * v.ex13.w
+ c2.x * v.ex13.x + c2.y * v.ex14.y + c2.z * v.ex14.z + c2.w * v.ex9.y;
}
+102
View File
@@ -0,0 +1,102 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "fx_common.glsl"
// The player's right arm and the item in its hand, drawn into the entity layer every frame. The arm (player_arm.xml) hangs at the body's
// right_arm_root hotspot (the host follows the stand / walk frames, so it breathes with the body), turns toward the aim point (the cursor, or
// ahead of the player) and is mirrored when aiming left; the item is held at the arm's "hand" hotspot, turned the same way about its grip.
// The arm header is in the first words of the hand texture region (hand1.x - its offset is absolute): [0] arm on, [1] arm pixels (absolute),
// [2..3] arm size, [4..5] arm root relative to the body (floats), [6..7] the arm sprite's pivot (offset_x, offset_y), [8..9] its hand hotspot;
// [48] cape on, [49..50] the corner of its 32 x 32 image (words 64..1087, rasterized by the host) relative to the body: drawn behind the body (empty pixels only).
const uint HAND_BASE = (1u << 20) - 32768u;
uint hw(uint i) { return fxtex[HAND_BASE + i]; }
// draws a w x h image whose pixel `pivot` sits at `org`, turned by (c, s), mirrored vertically when `mirror`
void draw_turned(vec2 org, vec2 pivot, float c, float s, bool mirror, int w, int h, uint off) {
float r = length(vec2(float(w), float(h))) + 2.0;
int n = int(2.0 * r) + 2;
ivec2 lo = ivec2(floor(org - vec2(r)));
for (int i = int(gl_LocalInvocationID.x); i < n * n; i += 64) {
ivec2 dst = lo + ivec2(i % n, i / n);
vec2 d = vec2(dst) + 0.5 - org;
vec2 l = vec2(c * d.x + s * d.y, -s * d.x + c * d.y); // the image frame, relative to the pivot
if (mirror) l.y = -l.y;
ivec2 sp = ivec2(floor(pivot + l));
if (sp.x < 0 || sp.y < 0 || sp.x >= w || sp.y >= h) continue;
ivec2 q = dst - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
uint t = fxtex[off + uint(sp.y * w + sp.x)];
const uint ta = t >> 24;
if (ta == 0u) continue;
const uint di = uint(q.y * pc.gw + q.x);
const uint under = entlayer[di];
if (ta < 255u && (under >> 24) != 0u) { // a translucent texel over the body: mixed with it (source over), not cut through it
uint mixd = max(ta, under >> 24) << 24;
for (uint sh = 0u; sh < 24u; sh += 8u) mixd |= (((t >> sh) & 0xFFu) * ta + ((under >> sh) & 0xFFu) * (255u - ta)) / 255u << sh;
entlayer[di] = mixd;
} else entlayer[di] = t;
}
}
// The items NPCs hold (ItemPickUpperComponent is_in_npc): at word 32640 of the hand region the count, then 12 words per entry: [0] the slot + 1, [1] the image
// (absolute), [2..3] its size, [4..5] the grip, [6..7] the hand relative to the body, [8..9] the aim point (floats); drawn like the player's item, turned toward the aim
void draw_npc_items() {
const uint n = min(hw(32640u), 10u);
for (uint k = 0u; k < n; ++k) {
const uint b = 32641u + k * 12u;
const int sl = int(hw(b)) - 1;
if (sl <= 0 || sl >= int(ents.length()) || (ents[sl].st.w & 8) != 0) continue;
vec2 cur = ents[sl].a.xy, prv = prevpos[sl].xy;
vec2 body = distance(prv, cur) < 10.0 ? mix(prv, cur, ctrl.view.z) : cur;
vec2 hand = body + vec2(uintBitsToFloat(hw(b + 6u)), uintBitsToFloat(hw(b + 7u)));
vec2 aim = vec2(uintBitsToFloat(hw(b + 8u)), uintBitsToFloat(hw(b + 9u)));
float ang = length(aim - hand) > 0.5 ? atan(aim.y - hand.y, aim.x - hand.x) : 0.0;
float c = cos(ang), s = sin(ang);
draw_turned(hand, vec2(uintBitsToFloat(hw(b + 4u)), uintBitsToFloat(hw(b + 5u))), c, s, c < 0.0, int(hw(b + 2u)), int(hw(b + 3u)), hw(b + 1u));
}
memoryBarrierBuffer();
barrier();
}
void main() {
draw_npc_items();
if (ctrl.inp.w < 1 || ents[0].st.x != 3 || (ents[0].st.w & 8) != 0 || (ents[0].ex8.x & 8192) != 0) return;
vec2 cur = ents[0].a.xy, prv = prevpos[0].xy;
vec2 body = distance(prv, cur) < 10.0 ? mix(prv, cur, ctrl.view.z) : cur;
if (hw(48u) == 1u) { // the cape, behind the body
ivec2 corner = ivec2(floor(body + vec2(uintBitsToFloat(hw(49u)), uintBitsToFloat(hw(50u))) + 0.5));
for (int i = int(gl_LocalInvocationID.x); i < 1024; i += 64) {
uint t = hw(64u + uint(i));
if ((t >> 24) == 0u) continue;
ivec2 q = corner + ivec2(i % 32, i / 32) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
if ((entlayer[q.y * pc.gw + q.x] >> 24) == 0u) entlayer[q.y * pc.gw + q.x] = t;
}
memoryBarrierBuffer();
barrier();
}
if (ctrl.hand0.z < 0.5) return;
const bool arm = hw(0u) == 1u;
vec2 root = arm ? body + vec2(uintBitsToFloat(hw(4u)), uintBitsToFloat(hw(5u))) : body + vec2(0.0, -5.0);
vec2 aim = ctrl.hand0.xy;
float ang = length(aim - root) > 0.5 ? atan(aim.y - root.y, aim.x - root.x) : 0.0;
float c = cos(ang), s = sin(ang);
bool mirror = c < 0.0;
vec2 hand = root;
if (arm) { // the hand: the arm's hand hotspot relative to its pivot, turned and mirrored with the arm
vec2 piv = vec2(uintBitsToFloat(hw(6u)), uintBitsToFloat(hw(7u)));
vec2 hl = vec2(uintBitsToFloat(hw(8u)), uintBitsToFloat(hw(9u))) + 0.5 - piv;
if (mirror) hl.y = -hl.y;
hand = root + vec2(c * hl.x - s * hl.y, s * hl.x + c * hl.y);
}
{ // the item's recoil (00b51670): hand2.z px back along the aim, turned muzzle-up by hand0.w radians (the arm keeps the aim)
const float ia = ang + (mirror ? ctrl.hand0.w : -ctrl.hand0.w);
const float ic = cos(ia), is = sin(ia);
draw_turned(hand - vec2(c, s) * ctrl.hand2.z, ctrl.hand2.xy, ic, is, mirror, ctrl.hand1.y, ctrl.hand1.z, uint(ctrl.hand1.x));
}
memoryBarrierBuffer();
barrier();
if (arm) draw_turned(root, vec2(uintBitsToFloat(hw(6u)), uintBitsToFloat(hw(7u))), c, s, mirror, int(hw(2u)), int(hw(3u)), hw(1u)); // the hand over the grip
}
+7
View File
@@ -0,0 +1,7 @@
// Data the host uploads for the GPU wand / projectile system: cast requests, shot nodes and specs (also appended to by the GPU).
layout(std430, set = 0, binding = 35) buffer FxHost { // written by the host: cast requests, shot nodes and specs
ivec4 hhead; // x, y: number of requests in list 0 / 1
vec4 hcast[1024]; // 2 lists x 256 requests x 2 vec4: (x, y, dir x, dir y), (node or -boom-1, team, seed, flags)
vec4 hnodes[8192]; // 2048 nodes x 4 vec4
ivec4 hspecs[16384]; // 4096 specs x 4 ivec4
};
+263
View File
@@ -0,0 +1,263 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "fx_common.glsl"
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
layout(std430, set = 0, binding = 56) buffer PartGlow { uint partglow[]; }; // the glowing particles added up per glow texel (r, g, b x 256; glow.comp reads it)
#include "simplex3.glsl"
// Particles: cosmetic ones (emitter trails, sparks) fade and vanish; "real" ones (blood, acid, slime...: max life < 0 encodes the material)
// fly like projectiles and turn into a cell of the material where they land. Both draw into the entity layer.
// Blood on the ground: a red droplet (cosmetic or real) that hits a static cell stains it (bit 31 of the cell word, which static cells do not use); render.comp tints it.
void stain_at(ivec2 q, int material, uint seed) {
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
const vec3 mc = rgb_of(mats[3u * uint(material)].x);
if (!(mc.r > 1.5 * mc.g && mc.r > 1.5 * mc.b)) return; // only reddish liquids (blood) stain
for (int k = 0; k < 5; ++k) {
const ivec2 o = k == 0 ? ivec2(0) : (k == 1 ? ivec2(1, 0) : (k == 2 ? ivec2(-1, 0) : (k == 3 ? ivec2(0, 1) : ivec2(0, -1))));
const ivec2 t = q + o;
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) continue;
if (k > 0 && (hash3(seed, uint(k), 93u) & 1u) != 0u) continue;
const uint m = cell_mat(cells[t.y * pc.gw + t.x]);
if (m != 0u && (mflags(m) & F_STATIC) != 0u && (cells[t.y * pc.gw + t.x] & BURN_BIT) == 0u) atomicOr(cells[t.y * pc.gw + t.x], 0x80000000u);
}
}
vec3 cell_colour(uint cw) { return rgb_of(mats[3u * cell_mat(cw)].x); }
// A particle of displaced liquid (docs/DISPLACEMENT_AND_FIRE.md, 00713e20 / 00712390 / 0071fba0): it moves by its OLD velocity, accelerates (a quarter of gravity in
// mode 1), marches cell by cell (at most 60 steps) and ends where it hits liquid or solid; it then goes back into the grid, keeping its identity.
void displaced_particle(uint id, FPart p) {
vec2 pos = p.a.xy, vel = p.a.zw;
uint kind = p.c.y;
uint cw = p.c.x;
if (pc.pass_ == 1) { // between ticks: only draw
pos -= vel * (1.0 - ctrl.view.z);
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
uvec3 d8 = uvec3(cell_colour(cw) * 255.0 + 0.5);
entlayer[q.y * pc.gw + q.x] = 0xFF000000u | (d8.r << 16) | (d8.g << 8) | d8.b;
}
return;
}
uint seed = id * 7u + uint(pc.frame);
ivec2 here = ivec2(floor(pos));
if (kind == 3u) { // waiting for a free place
if (reinsert(cw, here, seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
// No place: out of the window it is gone (00712390: a particle out of the loaded area dies; it waited there forever and filled the pool - 1000+ after 25 s).
// In the window the search of its column ended on a solid (a pocket under an overhang, a pool under ice, a column shifted into the rock): it drifts a cell,
// down into a free cell (air, gas), else sideways, and searches again from there; after 10 s it is dropped. (It used to wait where it was: blood and water
// pixels frozen in the air near pools.)
const ivec2 hq = here - ivec2(pc.view_x, pc.view_y);
const uint tries = p.c.w & 0xFFFFFu;
if (hq.x < 0 || hq.y < 0 || hq.x >= pc.gw || hq.y >= pc.gh || tries >= 600u) { fparts[id].b.x = 0.0; return; }
ivec2 step = ivec2(0, 1);
if (hq.y + 1 >= pc.gh || (mflags(cell_mat(cells[(hq.y + 1) * pc.gw + hq.x])) & (F_STATIC | F_LIQUID)) != 0u) {
const int sx = (hash3(seed, tries, 67u) & 1u) != 0u ? 1 : -1;
step = ivec2(sx, 0);
if (hq.x + sx < 0 || hq.x + sx >= pc.gw || (mflags(cell_mat(cells[hq.y * pc.gw + hq.x + sx])) & F_STATIC) != 0u) step = ivec2(0);
}
fparts[id].a.xy = pos + vec2(step);
fparts[id].c.w = p.c.w + 1u;
return;
}
const float g = 0.1667;
vec2 target = pos + vel;
vel.y += g * (kind == 1u ? 0.25 : 1.0);
if (kind == 1u && vel.y > 0.0) kind = 2u;
// 00712390: probes at unit-length increments along the move, at most 60, then the final target is committed
const float mlen = length(target - pos);
int steps = min(60, int(mlen));
vec2 dir = mlen > 1e-6 ? (target - pos) / mlen : vec2(0.0);
vec2 cur = pos;
uint budget = p.c.z;
bool dead = false;
float dens = mdensity(cell_mat(cw));
for (int s = 0; s < steps && !dead; ++s) {
vec2 nx = cur + dir;
ivec2 q = ivec2(floor(nx)) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { dead = true; break; }
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
if (m != 0u) {
uint f = mflags(m);
if ((f & F_STATIC) != 0u) { dead = true; break; }
if ((f & F_LIQUID) != 0u) {
if (kind == 2u) { dead = true; break; } // mode 0 stops at liquid (and powder)
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens; // mode 1: effective density 0 (an upward speed below 50 px/s, or a draw at creation) unless moving up fast; it must be lighter than the target
if (!(eff < mdensity(m)) || budget == 0u) { dead = true; break; }
budget -= 1u;
}
}
cur = nx;
}
if (!dead && int(mlen) <= 60) { // the target cell itself
ivec2 q = ivec2(floor(target)) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) dead = true;
else {
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
if (m != 0u) {
uint f = mflags(m);
if ((f & F_STATIC) != 0u) dead = true;
else if ((f & F_LIQUID) != 0u) {
if (kind == 2u) dead = true;
else {
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens;
if (!(eff < mdensity(m)) || budget == 0u) dead = true; else budget -= 1u;
}
}
}
if (!dead) cur = target;
}
} else if (!dead) cur = target; // beyond the 60 probes the end point is taken as it is
pos = cur;
if (dead) {
if (reinsert(cw, ivec2(floor(pos)), seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
kind = 3u; // no place yet: it keeps trying
}
fparts[id].a = vec4(pos, vel);
fparts[id].c = uvec4(cw, kind, budget, (p.c.w & 0x100000u) | ((p.c.w & 0xFFFFFu) + 1u));
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
vec3 col = cell_colour(cw);
uvec3 c8 = uvec3(col * 255.0 + 0.5);
entlayer[q.y * pc.gw + q.x] = 0xFF000000u | (c8.r << 16) | (c8.g << 8) | c8.b;
}
}
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= uint(FPART_MAX)) return;
FPart p = fparts[id];
if (p.b.x <= 0.0) return;
if (p.c.y != 0u) { displaced_particle(id, p); return; }
const bool real = p.b.y < 0.0;
vec2 pos = p.a.xy, vel = p.a.zw;
if (pc.pass_ == 1) { // between ticks: only draw, between the last two positions
pos -= vel * (1.0 - ctrl.view.z);
} else {
p.b.x -= 1.0;
bool gravDone = false;
bool landed = false; vec2 landPos = pos; int landMat = 0;
if (real) {
const int material = int(-p.b.y) - 1;
int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y)))));
vec2 prevp = pos;
bool done = false; // landed (the cell is left)
for (int s = 0; s < steps && p.b.x > 0.0; ++s) {
vec2 np = pos + vel / float(steps);
ivec2 q = ivec2(floor(np)) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { p.b.x = 0.0; break; }
if (cell_mat(cells[q.y * pc.gw + q.x]) != 0u) { // landed: the material stays in the last free cell
stain_at(q, material, id + uint(pc.frame) * 7u);
landed = true; landPos = prevp; landMat = material;
p.b.x = 0.0; done = true;
break;
}
prevp = pos;
pos = np;
}
if (p.b.x <= 0.0 && !done) { landed = true; landPos = pos; landMat = material; } // the life ran out in the air: it becomes a cell that falls
} else if (p.c.x == 1u) { // a droplet that stops at the first cell it meets
const int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y)))));
for (int s = 0; s < steps; ++s) {
const vec2 np = pos + vel / float(steps);
const ivec2 q = ivec2(floor(np)) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh || cell_mat(cells[q.y * pc.gw + q.x]) != 0u) {
if (p.c.z > 0u && q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) stain_at(q, int(p.c.z) - 1, id + uint(pc.frame) * 7u);
p.b.x = 0.0; break;
}
pos = np;
}
} else if ((p.c.w & 2u) != 0u || p.b.y < 0.0) { // collide_with_grid 0
pos += vel;
} else {
// 00712390, mode 0: probes at unit steps toward the target (at most 60, int(distance + 0.5)); a liquid / powder or solid cell (not gas, fire, air) ends the move
// at the last free point. With the bounce flag (6a:20, set at creation) the velocity bounces: vy flips when |dx| <= |dy| else vx; |vx| < 10 px/s gets
// + uniform(-10, 10); both are scaled by uniform(0.1, 0.3); a life above 1 s at a speed below 40 px/s becomes uniform(0.5, 1.5) s; below 10 px/s the flag
// clears. Without it the particle dies (6a:10). Out of the loaded area: dead. (Velocities here are per frame: px/s / 60, lives in frames.)
const vec2 target = pos + vel; // (the old velocity; the forces come before the collision, so the bounce acts on the new one)
const float dist = length(vel);
const int n = min(60, int(dist + 0.5));
const vec2 dir = dist > 0.0 ? vel / dist : vec2(0.0);
vel.y += p.b.z; gravDone = true;
vec2 cur = pos;
bool hit = false;
for (int s = 0; s < n; ++s) {
const vec2 nx = cur + dir;
const ivec2 q = ivec2(floor(nx)) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { p.b.x = 0.0; break; }
const uint m = cell_mat(cells[q.y * pc.gw + q.x]);
if (m != 0u && (mflags(m) & (F_GAS | F_FIRE | F_AIR)) == 0u) { hit = true; break; }
cur = nx;
}
if (hit) {
if ((p.c.w & 4u) != 0u) p.b.x = 0.0;
else {
const uint sd = id * 13u + uint(pc.frame) * 7u;
if (abs(dir.x) <= abs(dir.y)) vel.y = -vel.y; else vel.x = -vel.x;
if (abs(vel.x) < 10.0 / 60.0) vel.x += (-10.0 + 20.0 * rnd01(hash3(sd, 1u, 71u))) / 60.0;
const float f = 0.1 + 0.2 * rnd01(hash3(sd, 2u, 72u));
vel *= f;
const float sp2 = dot(vel, vel) * 3600.0;
if (p.b.x > 60.0 && sp2 < 1600.0) p.b.x = (0.5 + rnd01(hash3(sd, 3u, 73u))) * 60.0;
if (sp2 < 100.0) fparts[id].c.w = p.c.w | 4u;
}
pos = cur;
} else if (p.b.x > 0.0) pos = target;
}
if (!gravDone) vel += vec2(0.0, p.b.z);
if (!real && p.c.x == 0u && ((p.c.z >> 19) & 0xFFu) != 0u) { // attractor_force (00713e20): v += (attractor - pos) x dt x strength, the attractor the emitting projectile while it lives
const FProj ap = fprojs[(p.c.z >> 8) & 0x7FFu];
if (ap.d.w != 0) vel += (ap.a.xy - pos) * float((p.c.z >> 19) & 0xFFu) / 3600.0;
}
if (!real && p.c.x == 0u && (p.c.w >> 20) != 0u) { // airflow (00713e20): angle = 3D simplex (00872560) of (target x scale, target y scale, time) x pi; v += (-sin, cos) x airflow_force (px/s, per update, no dt)
const float force = float(p.c.w >> 20) / 1000.0, scale = float((p.c.w >> 8) & 0xFFFu) / 4096.0;
const float ang = simplex3(vec3(pos * scale, float(pc.frame) / 60.0)) * 3.1415927; // (the time: the particle system's clock, here the frame / 60)
vel += vec2(-sin(ang), cos(ang)) * force / 60.0;
}
fparts[id].a = vec4(pos, vel);
fparts[id].b.x = p.b.x;
if (landed) { // 00716600 / 0071fba0: the cell goes back into the grid where the particle ended, else the first free (or lighter) cell up to 128 above, drifting
// sideways; with no place yet it waits as a returning cell (not lost: a barrel's 750 units of spill all come out)
const uint cw = uint(landMat) | (((hash3(id + uint(pc.frame) * 7u, uint(landMat), 91u) >> 16) & 0xFFu) << 16);
const uint bv = birth_frame();
const ivec2 lq = ivec2(floor(landPos)) - ivec2(pc.view_x, pc.view_y);
if (lq.x >= 0 && lq.y >= 0 && lq.x < pc.gw && lq.y < pc.gh && !reinsert(cw, ivec2(floor(landPos)), id * 7u + uint(pc.frame), bv)) {
fparts[id].a = vec4(landPos, 0.0, 0.0);
fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0);
fparts[id].c = uvec4(cw, 3u, 0u, 0u);
}
return;
}
}
if (p.b.x <= 0.0) return;
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
uint bits = floatBitsToUint(p.b.w);
float a = (!real && (bits & 0x01000000u) != 0u) ? clamp(p.b.x / p.b.y, 0.0, 1.0) : 1.0;
if (!real && p.c.x == 0u && (bits & 0x02000000u) == 0u) a *= 1.0 - float(p.c.z & 0xFFu) / 255.0; // the material's alpha (c.z = 255 - alpha)
if ((bits & 0x02000000u) != 0u) { // additive: the colour is scaled by the remaining life, alpha 1 marks it for render.comp
vec3 c = rgb_of(bits) * a;
uvec3 q8 = uvec3(c * 255.0 + 0.5);
entlayer[q.y * pc.gw + q.x] = 0x02000000u | (q8.r << 16) | (q8.g << 8) | q8.b; // (alpha 2: an additive particle; its glow is summed below)
// tex_glow_source_particles: every glowing particle adds its light to the glow source (the overlapping particles of a dense trail add up to a white-hot core)
const uint gt = uint((q.y >> 1) * ((pc.gw + 1) / 2) + (q.x >> 1)) * 3u;
atomicAdd(partglow[gt], uint(c.r * 256.0 + 0.5));
atomicAdd(partglow[gt + 1u], uint(c.g * 256.0 + 0.5));
atomicAdd(partglow[gt + 2u], uint(c.b * 256.0 + 0.5));
} else {
entlayer[q.y * pc.gw + q.x] = (uint(a * 255.0) << 24) | (bits & 0xFFFFFFu);
}
if (!real && p.c.x == 0u && (p.c.z & 0x8000000u) != 0u && (bits & 0x02000000u) != 0u) { // draw_as_long: a streak back along the motion, as long as a frame's travel (at most 8 px), fading
const float sl = min(length(vel), 8.0);
const vec2 bd = length(vel) > 1e-4 ? -normalize(vel) : vec2(0.0);
for (int s = 1; s <= int(sl); ++s) {
const ivec2 qs = ivec2(floor(pos + bd * float(s))) - ivec2(pc.view_x, pc.view_y);
if (qs.x < 0 || qs.y < 0 || qs.x >= pc.gw || qs.y >= pc.gh) break;
const vec3 c = rgb_of(bits) * a * (1.0 - float(s) / (sl + 1.0));
const uvec3 q8 = uvec3(c * 255.0 + 0.5);
entlayer[qs.y * pc.gw + qs.x] = 0x02000000u | (q8.r << 16) | (q8.g << 8) | q8.b;
}
}
}
+133
View File
@@ -0,0 +1,133 @@
// State of the GPU projectile / effect system (device local, all simulated by proj_*.comp and fx_*.comp).
const int FP_MAX = 32768, FPART_MAX = 16384, FANIM_MAX = 256, FANIM_ALL = 512; // (the GPU's ring is 0..255, the host writes 256..511)
// FProj.d.z bit 30: a CPU query materialized this instance; automatic Lua yields to its twin.
struct FProj { vec4 a; vec4 b; vec4 c; ivec4 d; vec4 e; vec4 f; }; // d.w: the cast serial (0 = dead; the host's twins match it), d.z bit 10 = friendly fire of the shot; f: the wand's damage_electricity_add, damage_ice_add, knockback_force add, trail material (material + 1 | amount << 12) // e: rotation angle, angular velocity (thrown bodies) // pos, vel / damage, life left, gravity, friction / explosion radius, damage, age, bounces / type, spec, fired mask + team, alive
struct FPart { vec4 a; vec4 b; uvec4 c; }; // pos, vel / life, max life, gravity, colour bits (rgb, bit 24 = fades)
struct FAnim { vec4 a; ivec4 b; vec4 c; }; // x, y, light radius, light strength / boom, age, max age, colour bits / angle
layout(std430, set = 0, binding = 36) buffer FxState {
uint fcnt[4]; // projectile ring, particle ring, animation ring
FProj fprojs[FP_MAX]; // (src/gun/gpu_fx.h kMaxProj)
FPart fparts[16384];
FAnim fanims[512];
};
// A free record (00717400 / 007174e0: the pools return null when they are full, a live particle is never replaced): the search starts at the ring position and takes the first dead
// record of the next 64; -1 when there is none.
int alloc_part() {
const uint s0 = atomicAdd(fcnt[1], 1u);
for (uint k = 0u; k < 64u; ++k) {
const uint s = (s0 + k) % uint(FPART_MAX);
if (fparts[s].b.x <= 0.0) return int(s);
}
return -1;
}
// colour bits: rgb, bit 24 = fades with its life, bit 25 = additive. c.w of a cosmetic: bit 1 = no grid collision (collide_with_grid 0), bit 2 = the bounce is spent
// (00712010 sets the bounce flag 6a:20 on every record; fx_particles.comp)
int emit_particle_g(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive, bool collide) { // the record, -1 when the pool is full
const int si = alloc_part();
if (si < 0) return -1;
const uint s = uint(si);
fparts[s].a = vec4(pos, vel);
fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | (fade ? 0x01000000u : 0u) | (additive ? 0x02000000u : 0u)));
fparts[s].c = uvec4(0u, 0u, 0u, collide ? 0u : 2u); // c.z: 255 - the material's alpha (drawn opacity of a non-additive cosmetic)
return si;
}
// the cosmetic constructor 00716ce0 without force-create: the particle is made only where the grid slot is empty (and loaded)
bool cell_free_at(vec2 w) {
const ivec2 q = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y);
return q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cell_mat(cells[q.y * pc.gw + q.x]) == 0u;
}
void emit_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive) { emit_particle_g(pos, vel, life, grav, rgb, fade, additive, true); }
// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; with a material (c.z = material + 1) it leaves a cell of it there, so the spray feeds the pools.
uint emit_particle_hit(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // returns 0xFFFFFFFF when the pool is full
const int si = alloc_part();
if (si < 0) return 0xFFFFFFFFu;
const uint s = uint(si);
fparts[s].a = vec4(pos, vel);
fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x01000000u));
fparts[s].c = uvec4(1u, 0u, uint(material + 1), 0u);
return s;
}
// A particle that becomes a cell of the material when it lands (blood, acid... "real" particles): max life is -(material + 1).
// the acceleration of a particle record (00712010 copies 0115499c / 011549a0, which the particle manager resets to (0, pixels_per_physics_unit x 10) = (0, 100) px/s^2,
// 007128c0 / 00712bc0); the real particles (00716af0 -> 007168c0) keep it: 100 / 3600 cells per frame^2
const float PART_GRAV = 100.0 / 3600.0;
bool emit_material_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // false when the pool is full
const int si = alloc_part();
if (si < 0) return false;
const uint s = uint(si);
fparts[s].a = vec4(pos, vel);
fparts[s].b = vec4(life, -float(material + 1), grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x04000000u));
fparts[s].c = uvec4(0u); // a flag in the top byte keeps the bits a normal float (a zero top byte is a denormal and may be flushed)
return true;
}
// ---- liquid taken out of the grid by an entity, a projectile or a body (docs/DISPLACEMENT_AND_FIRE.md): the cell becomes a particle that keeps its identity
// (the whole cell word) and goes back into the grid when it has landed. c.y: 1 = mode 1 (moving up, passes liquids with a penetration budget c.z), 2 = mode 0
// (falls, stops at liquid or solid), 3 = waiting for a free place (retries every tick).
void emit_displaced(vec2 pos, vec2 vel, uint cellword, uint kind, uint birthv) { // birthv: the age (birth frame) of the cell travels with it (00717670 / 007168c0 hand over the same cell object)
const int si = alloc_part();
if (si < 0) return; // the pool is full: the cell is gone, as in the original (the caller cleared the grid slot first)
const uint s = uint(si);
fparts[s].a = vec4(pos, vel);
fparts[s].b = vec4(1.0e6, 1.0, uintBitsToFloat(birthv), 0.0);
fparts[s].c = uvec4(cellword, kind, kind == 1u ? 20u : 0u, (hash3(s, cellword, 77u) % 101u) < 50u ? 0x100000u : 0u); // bit 20: effective density zero (an inclusive draw below 50)
}
// Takes a liquid cell (not a powder) out of the grid and makes it a particle with the velocity v (cells per frame). Returns whether it did.
bool displace_cell(ivec2 w, vec2 v) {
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) return false;
int i = p.y * pc.gw + p.x;
uint c = cells[i];
uint bv = birth[i];
uint m = cell_mat(c);
if (m == 0u) return false;
uint f = mflags(m);
if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) return false;
if (atomicCompSwap(cells[i], c, 0u) != c) return false; // the grid slot is cleared before the particle exists
ext[i] = vec2(0.0);
emit_displaced(vec2(w) + 0.5, v, c, 1u, bv); // mode 1; it turns into mode 0 when it moves down (fx_particles.comp)
return true;
}
// Is the cell at the world position a liquid that counts for the drag (liquid, not a powder)?
bool liquid_nonsand(ivec2 w) {
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m == 0u) return false;
uint f = mflags(m);
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
}
// 0071fba0: puts a cell back into the grid. Searches from (x, y) upward to y - 128: an empty cell accepts it, a non-solid cell that is lighter is exchanged (the
// lighter cell then looks for a place of its own: it becomes a waiting particle); equal density is rejected, a solid cell ends the search; after more than
// five steps a rejection may shift the column (20 %, then 50 % right / left).
bool reinsert(uint cw, ivec2 w, uint seed, uint birthv) {
float dens = mdensity(cell_mat(cw));
for (int step = 0; step <= 128; ++step) {
ivec2 t = ivec2(w.x, w.y - step);
ivec2 q = t - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; // not loaded: no random draw either
int ti = q.y * pc.gw + q.x;
uint occ = cells[ti];
uint om = cell_mat(occ);
if (om == 0u) {
if (atomicCompSwap(cells[ti], 0u, cw) == 0u) { ext[ti] = vec2(0.0); birth[ti] = birthv; return true; }
continue;
}
uint of = mflags(om);
if ((of & F_STATIC) != 0u) return false; // a solid cell stops the search
if (mdensity(om) < dens && atomicCompSwap(cells[ti], occ, cw) == occ) { // the lighter cell is queued, not destroyed
ext[ti] = vec2(0.0);
const uint obirth = birth[ti];
birth[ti] = birthv;
emit_displaced(vec2(t) + 0.5, vec2(0.0), occ, 3u, obirth);
return true;
}
if (step > 5 && (hash3(seed, uint(step), 61u) % 101u) < 20u) w.x += (hash3(seed, uint(step), 62u) % 101u) < 50u ? 1 : -1;
}
return false;
}
+99
View File
@@ -0,0 +1,99 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
// The glow of the materials (gfx_glow of materials.xml), as the game's data/shaders/post_glow1.frag and post_glow2.frag do it: the glow builds up over frames.
// pass 1 (post_glow1): H = 0.1 x the sum of 11 taps of the previous frame's glow, 1.5 texels apart, horizontally
// pass 2 (post_glow2): D = the sum of 11 taps of H vertically / 12.2; N = the new source x 4 ("HDR": gfx_glow 255 is the old default brightness, up to 1000);
// glow = N x 0.125 + mix(D, N, 0.05)
// In a steady area the glow settles near 3 x the source and spreads into a wide soft halo (the white core of a dense fire). Half resolution (a texel = 2 x 2 cells);
// the previous frame's glow follows the camera (its view is kept after the texels). render.comp adds it to the lights and blends it like post_final.frag.
layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; };
layout(std430, set = 0, binding = 47) buffer GlowA { vec4 ga[]; }; // the glow (persistent), then one vec4: the view of the frame that wrote it
layout(std430, set = 0, binding = 48) buffer GlowB { vec4 gb[]; }; // the horizontal pass
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
layout(std430, set = 0, binding = 21) readonly buffer EntLayer { uint entlayer[]; };
layout(std430, set = 0, binding = 56) readonly buffer PartGlow { uint partglow[]; }; // the glowing particles of the frame added up per texel (fx_particles.comp) // the sprites and particles of the frame: the additive particles (alpha 1) are the particle glow
#define FPAL_TABLE tbl
// ---- the fire palettes of the cell blitting (main.cpp loads them after the tables; 00722490 the lookup, 00724580 the use) ----
uint fpal_word(uint k) { return FPAL_TABLE[1024u + 9u * mats[mats.length() - 1u].w + k]; }
vec3 fpal_rgb(uint w) { return vec3(float((w >> 16) & 255u), float((w >> 8) & 255u), float(w & 255u)) / 255.0; }
vec3 fire_lookup(uint at, uint row, uint w, float t) { // the row of the image at t x width: the first / last pixel outside, else the two neighbours mixed
const float x = float(w) * t;
const uint r0 = at + row * w;
if (x >= float(w)) return fpal_rgb(fpal_word(r0 + w - 1u));
if (x <= 0.0) return fpal_rgb(fpal_word(r0));
const uint i0 = uint(floor(x));
if (float(i0) == x || i0 + 1u >= w) return fpal_rgb(fpal_word(r0 + i0));
return mix(fpal_rgb(fpal_word(r0 + i0)), fpal_rgb(fpal_word(r0 + i0 + 1u)), x - float(i0));
}
uint fire_row(uint m) { return min(fpal_word(132u + m), 5u); } // CellData +0x68 fire_colors_index
float fire_age(uint b) { return float(birth_age(b)) + float(hash3(b & BIRTH_MASK, 77u, 5u) % 21u); } // 00709ef0: the flame's frame is the creation frame - uniform(0, 20)
vec3 fire_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(0u, fire_row(m), 8u, a * a); } // colors_fire.png at (age / 40)^2
vec3 fire_glow_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(48u, fire_row(m), 8u, fire_age(b) < 15.0 ? a : a * a); } // colors_fire_glow.png (young: age / 40)
vec3 burning_colour(uint m, uint h) { return fpal_rgb(fpal_word(96u + fire_row(m) * 6u + h % 6u)); } // colors_lava.bmp: a random pixel of the row
const float TAP = 1.5; // texels between the taps (post_glow*.frag: one_per_glow_texture_size x 1.5)
vec3 fetch(bool fromA, vec2 t, int hw, int hh) { // bilinear, zero outside
const vec2 q = t - 0.5;
const ivec2 q0 = ivec2(floor(q));
const vec2 f = q - vec2(q0);
vec3 r = vec3(0.0);
for (int k = 0; k < 4; ++k) {
const ivec2 s = q0 + ivec2(k & 1, k >> 1);
if (s.x < 0 || s.y < 0 || s.x >= hw || s.y >= hh) continue;
const float w = ((k & 1) != 0 ? f.x : 1.0 - f.x) * ((k >> 1) != 0 ? f.y : 1.0 - f.y);
r += (fromA ? ga[s.y * hw + s.x].rgb : gb[s.y * hw + s.x].rgb) * w;
}
return r;
}
void main() {
const int hw = (pc.gw + 1) / 2, hh = (pc.gh + 1) / 2;
const ivec2 q = ivec2(gl_GlobalInvocationID.xy);
if (pc.pass_ == 0) return; // (the source is read in pass 2)
if (q.x >= hw || q.y >= hh) return;
const vec2 t = vec2(q) + 0.5;
if (pc.pass_ == 1) {
// the previous frame's glow, moved with the camera (the view it was written in; half a texel per cell)
const vec4 pv = ga[hw * hh];
const vec2 sh = pv.w > 0.5 ? (vec2(float(pc.view_x), float(pc.view_y)) - pv.xy) * 0.5 : vec2(1.0e6);
vec3 acc = vec3(0.0);
for (int x = -5; x <= 5; ++x) acc += fetch(true, t + sh + vec2(float(x) * TAP, 0.0), hw, hh);
gb[q.y * hw + q.x] = vec4(acc * 0.1, 1.0);
return;
}
// pass 2
vec3 dec = vec3(0.0);
for (int y = -5; y <= 5; ++y) dec += fetch(false, t + vec2(0.0, float(y) * TAP), hw, hh);
dec *= 1.0 / 12.2;
vec3 src = vec3(0.0); // the cells of the texel: colour x gfx_glow / 1000, averaged (the cell blitting's HDR range), x 4
for (int k = 0; k < 4; ++k) {
const ivec2 c = min(q * 2 + ivec2(k & 1, k >> 1), ivec2(pc.gw - 1, pc.gh - 1));
const uint cw = cells[c.y * pc.gw + c.x];
const uint m = cell_mat(cw);
if (m == 0u) continue;
const uint g = gtab[m] & 0xFFFFu;
if ((mflags(m) & F_FIRE) != 0u) { src += fire_glow_colour(m, birth[c.y * pc.gw + c.x]); continue; } // a flame: colors_fire_glow.png by its age, as it is (the blitter writes the palette colour, not scaled by gfx_glow)
if ((cw & BURN_BIT) != 0u) { src += burning_colour(m, hash3(uint(c.x + pc.view_x), uint(c.y + pc.view_y), uint(pc.frame) + 3u)) * 0.25; continue; } // a burning cell: its colour / 4 (00722340 with 0x40)
if (g == 0u) continue;
const uint mc = mats[3u * m].x;
src += vec3(float((mc >> 16) & 0xFFu), float((mc >> 8) & 0xFFu), float(mc & 0xFFu)) / 255.0 * (float(g) / 1000.0);
}
vec3 part = vec3(0.0); // post_glow2.frag: + tex_glow_source_particles x 2.5 (the glowing particles: here the additive ones of the frame, fx_particles.comp)
for (int k = 0; k < 4; ++k) {
const ivec2 c = min(q * 2 + ivec2(k & 1, k >> 1), ivec2(pc.gw - 1, pc.gh - 1));
const uint e = entlayer[c.y * pc.gw + c.x];
if ((e >> 24) == 1u) part += vec3(float((e >> 16) & 255u), float((e >> 8) & 255u), float(e & 255u)) / 255.0; // (additive sprites and projectiles)
else if ((e >> 24) == 253u) src += vec3(float((e >> 16) & 255u), float((e >> 8) & 255u), float(e & 255u)) / 255.0 * 0.25; // a burning pixel of a body (sprites.comp): its colour / 4, as a burning cell
}
{ // the particles: all of them, added up (fx_particles.comp)
const uint gt = uint(q.y * hw + q.x) * 3u;
part += vec3(float(partglow[gt]), float(partglow[gt + 1u]), float(partglow[gt + 2u])) / 256.0;
}
const vec3 N = src * 0.25 * 4.0 + part * 0.25 * 2.5;
ga[q.y * hw + q.x] = vec4(N * 0.125 + mix(dec, N, 0.05), 1.0);
if (q.x == 0 && q.y == 0) ga[hw * hh] = vec4(float(pc.view_x), float(pc.view_y), 0.0, 1.0); // (the next frame's pass 1 reads all texels before this pass runs again)
}
+266
View File
@@ -0,0 +1,266 @@
#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 ((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));
}
}
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
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)
}
}
+23
View File
@@ -0,0 +1,23 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
// The light of the entities (LightComponent, up to 128 near the window) at every cell of the window, once per frame: render.comp's lighting() reads it.
// (render.comp summed every light for every output pixel - some 36 pixels per cell in a large window - and grew slow with the lanterns and fires of a
// long session.)
layout(std430, set = 0, binding = 45) readonly buffer Lit { uvec4 lhead; vec4 lent[]; }; // count, then (x, y, radius, strength), (r, g, b, 0) per light
layout(std430, set = 0, binding = 57) buffer LitCells { uvec2 litc[]; }; // per cell: the summed light, half floats (r g, b)
void main() {
const ivec2 cp = ivec2(gl_GlobalInvocationID.xy);
if (cp.x >= pc.gw || cp.y >= pc.gh) return;
const vec2 w = vec2(cp + ivec2(pc.view_x, pc.view_y)) + 0.5;
vec3 L = vec3(0.0);
for (uint i = 0u; i < min(lhead.x, 128u); ++i) {
const vec4 a = lent[2u * i], b = lent[2u * i + 1u];
const float d = distance(w, a.xy) / a.z;
if (d < 1.0) L += b.rgb * (1.0 - d) * (1.0 - d) * (1.0 - d) * 1.1 * a.w;
}
litc[cp.y * pc.gw + cp.x] = uvec2(packHalf2x16(L.rg), packHalf2x16(vec2(L.b, 0.0)));
}
+121
View File
@@ -0,0 +1,121 @@
// Native C++ Lua bytecode target. Only self/owner handles are visible.
struct NgpValue { uint tag; float n; uint s; };
struct NgpTuple { NgpValue v[8]; int count; };
FProj ngp_proj;
vec3 ngp_owner;
float ngp_owner_id;
bool ngp_failed, ngp_loaded;
uint ngp_line;
uvec4 ngp_error;
vec3 ngp_surface(vec2 start,vec2 end);
NgpValue ngp_nil() { return NgpValue(0u,0.0,0u); }
NgpValue ngp_num(float n) { return NgpValue(1u,n,0u); }
NgpValue ngp_bool(bool b) { return NgpValue(2u,b?1.0:0.0,0u); }
NgpValue ngp_string(uint s) { return NgpValue(3u,0.0,s); }
NgpTuple ngp_empty() { NgpTuple t;t.count=0;for(int i=0;i<8;i++)t.v[i]=ngp_nil();return t; }
NgpTuple ngp_one(NgpValue v) { NgpTuple t=ngp_empty();t.count=1;t.v[0]=v;return t; }
void ngp_fault(uint code) {if(!ngp_failed)ngp_error=uvec4(code,ngp_line,1u,0u);ngp_failed=true;}
bool ngp_truth(NgpValue v) {return v.tag!=0u&&!(v.tag==2u&&v.n==0.0);}
bool ngp_equal(NgpValue a,NgpValue b) {return a.tag==b.tag&&(a.tag==0u||(a.tag>=3u?a.s==b.s:a.n==b.n));}
float ngp_number(NgpValue v) {
float n;if(v.tag==1u)return v.n;
if(v.tag==3u&&ngp_parse_number(v.s,n))return n;
ngp_fault(1u);return 0.0;
}
bool ngp_less(NgpValue a,NgpValue b,bool equal) {
if(a.tag==3u&&b.tag==3u)return equal?ngp_rank(a.s)<=ngp_rank(b.s):ngp_rank(a.s)<ngp_rank(b.s);
if(a.tag!=1u||b.tag!=1u){ngp_fault(1u);return false;}
return equal?a.n<=b.n:a.n<b.n;
}
float ngp_pow(float a,float b) {
if(b==0.0)return 1.0;
if(a<0.0){if(floor(b)!=b)return uintBitsToFloat(0x7fc00000u);return (mod(abs(b),2.0)==0.0?1.0:-1.0)*pow(-a,b);}
return pow(a,b);
}
NgpValue ngp_arithmetic(int op,NgpValue a,NgpValue b) {
float x=ngp_number(a),y=ngp_number(b);
if(op==0)return ngp_num(x+y);if(op==1)return ngp_num(x-y);
if(op==2)return ngp_num(x*y);if(op==3)return ngp_num(x/y);
if(op==4)return ngp_num(x-floor(x/y)*y);return ngp_num(ngp_pow(x,y));
}
NgpTuple ngp_call(NgpValue f,NgpTuple a) {
if(f.tag!=4u){ngp_fault(1u);return ngp_empty();}
uint api=f.s;
if(api==1u){if(a.count!=0)ngp_fault(2u);return ngp_one(ngp_num(16777216.0));}
if(api==2u){
if(a.count!=1){ngp_fault(2u);return ngp_empty();}
float e=ngp_number(a.v[0]);
if(e!=16777216.0&&e!=ngp_owner_id){ngp_fault(3u);return ngp_empty();}
vec3 tr=e==16777216.0?vec3(ngp_proj.a.xy,ngp_proj.e.x):ngp_owner;
NgpTuple r=ngp_empty();r.count=5;
r.v[0]=ngp_num(tr.x);r.v[1]=ngp_num(tr.y);r.v[2]=ngp_num(tr.z);r.v[3]=ngp_num(1.0);r.v[4]=ngp_num(1.0);return r;
}
if(api==3u){
if(a.count!=2){ngp_fault(2u);return ngp_empty();}
if(ngp_number(a.v[0])!=16777216.0||a.v[1].tag!=3u){ngp_fault(3u);return ngp_empty();}
if(a.v[1].s==NGP_PROJECTILE)return ngp_one(ngp_num(1.0));
if(a.v[1].s==NGP_VELOCITY)return ngp_one(ngp_num(2.0));
ngp_fault(3u);return ngp_empty();
}
if(api==4u){
if(a.count!=2){ngp_fault(2u);return ngp_empty();}
if(ngp_number(a.v[0])==2.0&&a.v[1].tag==3u&&a.v[1].s==NGP_MOTION){
NgpTuple r=ngp_empty();r.count=2;
r.v[0]=ngp_num(ngp_proj.a.z*60.0);r.v[1]=ngp_num(ngp_proj.a.w*60.0);return r;
}
if(ngp_number(a.v[0])!=1.0||a.v[1].tag!=3u||a.v[1].s!=NGP_WHO){ngp_fault(3u);return ngp_empty();}
return ngp_one(ngp_num(ngp_owner_id));
}
if(api==5u){
if(a.count==4&&ngp_number(a.v[0])==2.0&&a.v[1].tag==3u&&a.v[1].s==NGP_MOTION){
vec2 velocity=vec2(ngp_number(a.v[2]),ngp_number(a.v[3]));
if(!ngp_failed)ngp_proj.a.zw=velocity/60.0;return ngp_empty();
}
if(a.count!=3){ngp_fault(2u);return ngp_empty();}
if(ngp_number(a.v[0])!=2.0||a.v[1].tag!=3u||a.v[1].s!=NGP_GRAVITY){ngp_fault(3u);return ngp_empty();}
float gravity=ngp_number(a.v[2]);if(!ngp_failed)ngp_proj.b.z=gravity/3600.0;
return ngp_empty();
}
if(api==6u||api==7u){
if(a.count!=1||a.v[0].tag!=3u||a.v[0].s!=NGP_UTIL){ngp_fault(3u);return ngp_empty();}
if(api==7u||!ngp_loaded){ngp_module_init();ngp_loaded=true;}return ngp_empty();
}
if(api==8u){
if(a.count!=4){ngp_fault(2u);return ngp_empty();}
vec2 start=vec2(ngp_number(a.v[0]),ngp_number(a.v[1]));
vec2 end=vec2(ngp_number(a.v[2]),ngp_number(a.v[3]));
if(ngp_failed)return ngp_empty();
vec3 hit=ngp_surface(start,end);
if(hit.z<0.0){ngp_fault(15u);return ngp_empty();}
NgpTuple r=ngp_empty();r.count=3;
r.v[0]=ngp_bool(hit.z!=0.0);r.v[1]=ngp_num(hit.x);r.v[2]=ngp_num(hit.y);return r;
}
if(api==9u){
// Reviewed cloud script sets only position, retaining angle and scale.
if(a.count!=3){ngp_fault(2u);return ngp_empty();}
if(ngp_number(a.v[0])!=16777216.0){ngp_fault(3u);return ngp_empty();}
vec2 position=vec2(ngp_number(a.v[1]),ngp_number(a.v[2]));
if(!ngp_failed)ngp_proj.a.xy=position;
return ngp_empty();
}
if(api>=10u&&api<=15u){
int count=(api==12u||api==15u)?2:1;
if(a.count!=count){ngp_fault(2u);return ngp_empty();}
float x=ngp_number(a.v[0]), y=count==2?ngp_number(a.v[1]):0.0, value=0.0;
if(api==10u)value=abs(x);
if(api==11u)value=sqrt(x);
// GLSL atan(0,0) is undefined; LuaJIT/libm returns signed zero or pi.
if(api==12u){
if(x==0.0)value=(floatBitsToUint(y)&0x80000000u)!=0u?3.141592653589793:0.0;
else if(y==0.0)value=1.5707963267948966;
else value=atan(abs(x),y);
if((floatBitsToUint(x)&0x80000000u)!=0u)value=-value;
}
if(api==13u)value=cos(x);
if(api==14u)value=sin(x);
if(api==15u)value=max(x,y);
return ngp_one(ngp_num(value));
}
ngp_fault(3u);return ngp_empty();
}
+15
View File
@@ -0,0 +1,15 @@
// Same half-cell samples and last-free result as the engine's CPU raytrace.
// ngp_solid_at supplies terrain only; rigid bodies are outside this API.
bool ngp_solid_at(vec2 position);
vec3 ngp_surface(vec2 start, vec2 end) {
float distance=length(end-start);
if(isnan(distance)||isinf(distance)||distance>512.0) return vec3(start, -1.0);
int steps=max(1,int(ceil(distance*2.0)));
vec2 previous=start;
for(int k=1;k<=steps;++k) {
vec2 point=start+(end-start)*float(k)/float(steps);
if(ngp_solid_at(point))return vec3(previous,1.0);
previous=point;
}
return vec3(end,0.0);
}
+21
View File
@@ -0,0 +1,21 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= pc.gw || p.y >= pc.gh || pc.brush_mat < 0) return;
ivec2 d = p - ivec2(pc.brush_x, pc.brush_y);
if (d.x * d.x + d.y * d.y > pc.brush_r * pc.brush_r) return;
uint h = hash3(uint(p.x), uint(p.y), uint(pc.frame + pc.seed) + uint(pc.pass_) * 131u);
int i = p.y * pc.gw + p.x;
if (pc.brush_mat == 0) { cells[i] = 0u; ext[i] = vec2(0.0); return; }
// only place into empty space, and thin out fluids so they pour
if (cell_mat(cells[i]) != 0u) return;
if ((mflags(uint(pc.brush_mat)) & F_STATIC) == 0u && (h & 3u) != 0u) return;
// bits 16-23: colour variation, bit 26: initial horizontal direction of gases
cells[i] = uint(pc.brush_mat) | (((h >> 8) & 0xFFu) << 16) | (((h >> 20) & 1u) << 26);
ext[i] = vec2(0.0);
birth[i] = birth_frame();
}
+38
View File
@@ -0,0 +1,38 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "fx_common.glsl"
// Particles requested by the host (effects of the rigid bodies: the splash of a body that hits something, ...): count, then per request (position, velocity), (life, gravity),
// (colour bits, mode, material). Mode 0: a cosmetic droplet that ends at the first cell it meets, stains it and leaves a cell of `material`, mode 2: the same without the cell (stains only), mode 1: a real particle of the material.
struct PartReq { vec4 a; vec4 b; uvec4 c; };
layout(std430, set = 0, binding = 51) buffer PartReqBuf { uvec4 rhead; PartReq reqs[]; };
void main() {
const uint i = gl_GlobalInvocationID.x;
if (i >= min(rhead.x, 1024u)) return;
const PartReq r = reqs[i];
if (r.c.y == 0u) emit_particle_hit(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z));
else if (r.c.y == 2u) { const uint ps = emit_particle_hit(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z)); if (ps != 0xFFFFFFFFu) fparts[ps].c.w = 1u; } // stains only, leaves no cell
else if (r.c.y == 3u) emit_particle(r.a.xy, r.a.zw, r.b.x, 0.0, r.c.x, true, true); // a light ray (SpriteParticleEmitterComponent, additive): fades, no cell
else if (r.c.y == 4u) { if ((r.c.w & 2u) == 0u || cell_free_at(r.a.xy)) {
// c.w bit 2: a glowing material (gfx_glow): additive, the colour scaled by the material's alpha; else bits 8-15 = 255 - alpha, the opacity
const bool glow = (r.c.w & 4u) != 0u;
const float al = 1.0 - float((r.c.w >> 8) & 0xFFu) / 255.0;
const uint rgb = glow ? ((uint(float((r.c.x >> 16) & 0xFFu) * al + 0.5) << 16) | (uint(float((r.c.x >> 8) & 0xFFu) * al + 0.5) << 8) | uint(float(r.c.x & 0xFFu) * al + 0.5)) : r.c.x;
const int ps = emit_particle_g(r.a.xy, r.a.zw, r.b.x, r.b.y, rgb, (r.c.w & 8u) == 0u, glow, (r.c.w & 1u) == 0u); // c.w bit 3: fade_based_on_lifetime 0
if (ps >= 0 && !glow) fparts[ps].c.z = (r.c.w >> 8) & 0xFFu;
if (ps >= 0 && r.b.z > 0.0) fparts[ps].c.w |= (min(uint(r.b.z * 1000.0 + 0.5), 4095u) << 20) | (min(uint(r.b.w * 4096.0 + 0.5), 4095u) << 8); // airflow_force (b.z), airflow_scale (b.w)
} } // a cosmetic particle (ParticleEmitterComponent emit_cosmetic_particles): fades, no cell; c.w bit 0: collide_with_grid 0, bit 1: cosmetic_force_create 0
else if (r.c.y == 5u) { // create_real_particles: the cell itself, in the empty cell (or over a gas / liquid unless only-if-space: c.w bit 1)
const ivec2 q = ivec2(floor(r.a.xy)) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
const int ci = q.y * pc.gw + q.x;
const uint nc = r.c.z | (((hash3(uint(ci), uint(pc.frame), 63u) >> 16) & 0xFFu) << 16);
const uint old = atomicCompSwap(cells[ci], 0u, nc);
if (old == 0u) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
else if ((r.c.w & 2u) == 0u && (mflags(cell_mat(old)) & (F_GAS | F_LIQUID)) != 0u && (mflags(cell_mat(old)) & (F_STATIC | F_SAND)) == 0u && atomicCompSwap(cells[ci], old, nc) == old) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
}
else if (r.c.y == 6u) emit_displaced(r.a.xy, r.a.zw, r.c.z | (((hash3(i, uint(pc.frame), 64u) >> 16) & 0xFFu) << 16), 2u, birth_frame()); // GameCreateParticle (007b3120 -> 00716af0): a cell of the material handed to the particle system like a displaced one (AddAsParticle 007168c0): it falls and goes back into the grid, waiting for a free place
else emit_material_particle(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z));
}
+119
View File
@@ -0,0 +1,119 @@
// Shared by terrain.comp and render.comp: the 70-scaled simplex noise, the biome-map topology lookup and the gradient of the procedural biomes (FUN_0087e8d0).
// Needs: perm (binding 8), ffloor, ps, atab (areas.glsl) declared before the include.
vec2 grad12(uint g) {
const vec2 G[12] = vec2[12](vec2(1, 1), vec2(-1, 1), vec2(1, -1), vec2(-1, -1), vec2(1, 0), vec2(-1, 0), vec2(1, 0), vec2(-1, 0), vec2(0, 1), vec2(0, -1), vec2(0, 1), vec2(0, -1));
return G[g];
}
float simplex70(float x, float y) {
const float F2 = 0.36602540378, G2 = 0.21132486540;
float s = (x + y) * F2;
int i = ffloor(x + s), j = ffloor(y + s);
float t = float(i + j) * G2;
float x0 = x - (float(i) - t), y0 = y - (float(j) - t);
int i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1;
float x1 = x0 - float(i1) + G2, y1 = y0 - float(j1) + G2;
float x2 = x0 - 1.0 + 2.0 * G2, y2 = y0 - 1.0 + 2.0 * G2;
uint ii = uint(i) & 255u, jj = uint(j) & 255u;
float n = 0.0;
float t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 >= 0.0) { t0 *= t0; n += t0 * t0 * dot(grad12(ps(ii + ps(jj)) % 12u), vec2(x0, y0)); }
float t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 >= 0.0) { t1 *= t1; n += t1 * t1 * dot(grad12(ps(ii + uint(i1) + ps(jj + uint(j1))) % 12u), vec2(x1, y1)); }
float t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 >= 0.0) { t2 *= t2; n += t2 * t2 * dot(grad12(ps(ii + 1u + ps(jj + 1u)) % 12u), vec2(x2, y2)); }
return 70.0 * n;
}
float fbm70(float x, float y) {
const vec4 M = vec4(0.84147, 0.5403, 0.5403, -0.84147);
const float K[3] = float[3](2.02, 2.33, 2.01);
float sum = simplex70(x, y) * 0.5;
float w = 0.25;
for (int o = 0; o < 3; ++o) {
float nx = (M.x * x + M.z * y) * K[o], ny = (M.y * x + M.w * y) * K[o];
x = nx; y = ny;
sum += simplex70(x, y) * w;
w *= 0.5;
}
return sum / 0.9375;
}
// The topology record (offset in atab) of a biome-map tile, 0xFFFFFFFF when there is none (FUN_0087d870: x wraps, y is clamped).
uint tile_rec(int tx, int ty) {
int W = int(atab[2]), H = int(atab[3]);
tx = ((tx % W) + W) % W;
ty = clamp(ty, 0, H - 1);
return atab[atab[4] + uint(ty * W + tx)];
}
// word 20 of the record: biome edge flags (bit 0 noise_biome_edges, bit 1 big_noise_biome_edges, bit 2 fat_biome_edges)
bool bedge(uint rec, uint bit) { return (atab[rec + 20u] & bit) != 0u; }
// FUN_0087d9a0: the biome-map tile whose biome a world cell belongs to. Within 42 cells of the edge of a tile whose neighbour is another biome, the lookup point is
// displaced by noise (wavy biome borders): small (2.5 cells) unless both biomes have big_noise_biome_edges (default on): 11 * simplex + 30 * sin / cos.
ivec2 border_tile(ivec2 w) {
const int ix = w.x + 17920, iy = w.y + 7168;
const int fx = ix & 511, fy = iy & 511;
const int W = int(atab[2]), H = int(atab[3]);
const int tx = ix >> 9, ty = clamp(iy >> 9, 0, H - 1);
const ivec2 here = ivec2(tx, ty);
const uint b = tile_rec(tx, ty);
if (b == 0xFFFFFFFFu || !bedge(b, 1u) || bedge(b, 4u)) return here;
ivec2 n = ivec2(-1000000);
const bool l = fx <= 41, r = fx >= 471, t = fy <= 41, bt = fy >= 471;
if (l && tile_rec(tx - 1, ty) != b) n = ivec2(tx - 1, ty);
else if (t && tile_rec(tx, ty - 1) != b) n = ivec2(tx, ty - 1);
else if (r && tile_rec(tx + 1, ty) != b) n = ivec2(tx + 1, ty);
else if (bt && tile_rec(tx, ty + 1) != b) n = ivec2(tx, ty + 1);
else if (l && t && tile_rec(tx - 1, ty - 1) != b) n = ivec2(tx - 1, ty - 1);
else if (l && bt && tile_rec(tx - 1, ty + 1) != b) n = ivec2(tx - 1, ty + 1);
else if (r && t && tile_rec(tx + 1, ty - 1) != b) n = ivec2(tx + 1, ty - 1);
else if (r && bt && tile_rec(tx + 1, ty + 1) != b) n = ivec2(tx + 1, ty + 1);
if (n.x == -1000000) return here;
const uint nb = tile_rec(n.x, n.y);
if (nb == 0xFFFFFFFFu || !bedge(nb, 1u)) return here;
const float xf = float(ix), yf = float(iy);
float xx, yy;
if (!bedge(b, 2u) || !bedge(nb, 2u)) {
const float d = simplex70(xf * 0.05, yf * 0.05) * 2.5;
xx = xf + d; yy = yf + d;
} else {
const float n11 = simplex70(xf * 0.05, yf * 0.05) * 11.0;
xx = xf + n11 + 30.0 * sin(yf * 0.005);
yy = yf + n11 + 30.0 * cos(xf * 0.005);
}
const ivec2 t2 = ivec2(int(xx) >> 9, int(yy) >> 9);
const uint r2 = tile_rec(t2.x, t2.y);
// the displaced tile must have its area loaded: otherwise the cell would be placeholder rock (a column of rock stood in the sky at the edge of the loaded region)
if (r2 != 0xFFFFFFFFu && bedge(r2, 1u) && t2.x >= 0 && t2.y >= 0 && t2.x < W && t2.y < H && atab[atab[1] + uint(t2.y) * atab[2] + uint(t2.x)] != 0u) return t2;
return here;
}
uint topo_rec(ivec2 w) { // the topology record of the biome the world cell belongs to
const ivec2 t = border_tile(w);
return tile_rec(t.x, t.y);
}
int area_of_cell(ivec2 w) { // the loaded area of that biome, -1 when none
const ivec2 t = border_tile(w);
if (t.x < 0 || t.y < 0 || t.x >= int(atab[2]) || t.y >= int(atab[3])) return -1;
return int(atab[atab[1] + uint(t.y) * atab[2] + uint(t.x)]) - 1;
}
float tf(uint rec, int k) { return uintBitsToFloat(atab[rec + uint(k)]); }
vec2 grad_limits(uint rec, float X) { // FUN_0087eaf0: the start and end of the gradient at X
float sY = tf(rec, 11), eY = tf(rec, 12);
int mode = int((atab[rec] >> 16) & 15u);
if (mode == 0) return vec2(sY, eY);
if (mode == 3) { float d = (X - tf(rec, 13)) * tf(rec, 14); return vec2(sY + d, eY + d); }
float xp = uintBitsToFloat(atab[6]) + X, sc = tf(rec, 15); // the noise offset of the world (header word 6)
float s = (mode == 2 ? fbm70(xp * sc, sY * sc) : float(simplex70(xp * sc, sY * sc))) * tf(rec, 16) + sY;
float e = float(simplex70(xp * sc, eY * sc)) * tf(rec, 17) + eY;
return vec2(s, e);
}
float proc_gradient(ivec2 w, uint rec) { // FUN_0087e8d0: 0 above the start, 1 below the end, linear between; blended with the left neighbour within 41 cells of the tile edge
float X = float(w.x), Y = float(w.y);
vec2 L = grad_limits(rec, X);
int fx = (w.x + 17920) & 511;
if (Y <= 380.0 && fx <= 41) {
ivec2 wl = ivec2(w.x - fx - 1, w.y);
uint recL = topo_rec(wl);
if (recL != 0xFFFFFFFFu && recL != rec) L = mix(L, grad_limits(recL, X), 1.0 - float(fx) / 41.0);
}
if (L.x >= Y) return 0.0;
if (Y >= L.y || L.y == L.x) return 1.0;
return (Y - L.x) / (L.y - L.x);
}
+25
View File
@@ -0,0 +1,25 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "fx_common.glsl"
#include "generated/proj_lua_generated.glsl"
// Serial prevents a recycled projectile slot from executing a previous task.
// x=cast serial, y=scheduled frame, z=1 pending / 2 complete, w=fault code.
layout(std430, set = 0, binding = 64) buffer LuaTasks { uvec4 luaTasks[]; };
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= uint(FP_MAX)) return;
uvec4 task = luaTasks[id];
if (task.z != 1u) return;
luaTasks[id].z = 2u;
FProj p = fprojs[id];
if (task.y != uint(pc.frame) || task.x != uint(p.d.w) || p.d.w == 0 || (p.d.z & (1 << 29)) != 0) { luaTasks[id].w = 13u; return; }
int owner = int(p.e.z) - 1;
if (owner < 0 || owner >= ctrl.inp.w) { luaTasks[id].w = 3u; return; }
uvec4 fault = ngp_execute(p, vec3(ents[owner].a.xy, 0.0), float(owner + 1));
luaTasks[id].w = fault.x;
// Bindings write only this projectile. No state races between invocations.
fprojs[id] = p;
}
+50
View File
@@ -0,0 +1,50 @@
// Shared by the runtime-compiled program and the offline reference target.
layout(std430,binding=64) buffer LuaTasks { uvec4 luaTasks[]; };
void main() {
uint id=gl_GlobalInvocationID.x;
if(id>=32768u)return;
FProj p=fprojs[id];
uvec4 task=luaTasks[id];
// Gather all additive fields into this target invocation. No invocation
// writes another projectile, so overlapping fields cannot race or lose force.
if(p.d.w!=0&&(p.d.z&(1<<29))==0&&p.d.x>=0&&p.d.x<512) {
uint flags=ft[p.d.x*18+11].y;
if((flags&(3u<<29))==(3u<<29)) {
uint count=luaTasks[32768].y==uint(pc.frame)?min(luaTasks[32768].x,512u):0u;
for(uint field=0u;field<count;++field) {
uvec4 data=luaTasks[32769u+field*2u];
uvec4 source=luaTasks[32770u+field*2u];
if((source.z&1u)!=0u&&source.x==id+1u&&source.y==uint(p.d.w))continue;
FProj candidate=p;
uvec4 fault=ngp_neighbor_dispatch(data.z,candidate,vec3(uintBitsToFloat(data.xy),0.0));
if(fault.x==0u)p=candidate;
else {luaTasks[id]=uvec4(uint(p.d.w),uint(pc.frame),2u,fault.x);}
}
if(count!=0u)fprojs[id]=p;
}
}
if(p.d.w!=0 && p.d.x>=0 && p.d.x<512) {
const uint flags=ft[p.d.x*18+11].y;
const uint period=(flags>>21)&255u;
// bit 30 transfers this instance to a CPU query's materialized twin.
if((flags&(1u<<20))!=0u && (p.d.z&(1<<30))==0 && period!=0u) {
if((uint(pc.frame)+uint(p.d.w))%period!=0u)return;
if(p.a.x<float(pc.view_x-128)||p.a.y<float(pc.view_y-128)||p.a.x>float(pc.view_x+pc.gw+128)||p.a.y>float(pc.view_y+pc.gh+128))return;
if(task.x==uint(p.d.w)&&task.y==uint(pc.frame)&&task.z==2u)return;
task=uvec4(uint(p.d.w),uint(pc.frame),257u,0u); // program 1, pending
luaTasks[id]=task;
}
}
if((task.z&255u)!=1u)return;
uint program=task.z>>8;
luaTasks[id].z=2u;
if(task.y!=uint(pc.frame)||task.x!=uint(p.d.w)||p.d.w==0||(p.d.z&(1<<29))!=0){luaTasks[id].w=13u;return;}
int owner=int(p.e.z)-1;
bool known=owner>=0&&owner<ctrl.inp.w;
if(known)known=(ents[owner].st.w&8)==0;
// The original chunk does nothing when mWhoShot is NULL_ENTITY.
vec3 transform=known?vec3(ents[owner].a.xy,0.0):vec3(0.0);
uvec4 fault=ngp_dispatch(program,p,transform,known?float(owner+1):0.0);
luaTasks[id].w=fault.x;
fprojs[id]=p;
}
+91
View File
@@ -0,0 +1,91 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "fx_common.glsl"
// Consumes the cast requests of this frame (written by the host for a trigger pull, or by proj_step for the sub-shots of triggers):
// a request names a shot node; every spec of the node becomes a projectile. A negative node is a visual-only explosion (exploding props).
shared int reqCount;
void main() {
int list = pc.frame & 1;
if (gl_LocalInvocationID.x == 0u) reqCount = min(list == 0 ? hhead.x : hhead.y, 256);
barrier();
for (int r = int(gl_LocalInvocationID.x); r < reqCount; r += 64) {
vec4 r0 = hcast[(list * 256 + r) * 2], r1 = hcast[(list * 256 + r) * 2 + 1];
int node = int(r1.x);
uint seed = uint(r1.z) * 2654435761u + uint(r) * 40503u + uint(pc.frame);
if (node < 0) { boom_fx(-node - 1, r0.xy, seed, -1.0); continue; }
vec4 n0 = hnodes[node * 4], n1 = hnodes[node * 4 + 1], n2 = hnodes[node * 4 + 2], n3 = hnodes[node * 4 + 3];
int first = int(n0.x), count = int(n0.y);
float base = atan(r0.w, r0.z);
for (int i = 0; i < count; ++i) {
ivec4 s0 = hspecs[(first + i) * 4];
int type = s0.x;
int tb = type * TYPE_STRIDE;
vec4 t0 = ftf(tb), t1 = ftf(tb + 1);
ivec4 t3 = fti(tb + 3);
uvec4 hs = uvec4(hash3(seed, uint(i), 1u), hash3(seed, uint(i), 2u), hash3(seed, uint(i), 3u), hash3(seed, uint(i), 4u));
const bool done = ((int(r1.y) >> 30) & 1) != 0; // the host has run the script_shot scripts on this shot: its velocity is final
const bool exact = (int(r1.y) & 4) != 0 && (fti(tb + 1).w < 0 || done); // a particle entity the host created (its velocity was drawn by SetStartVelocityComponent on the CPU): exactly that velocity
float ang = base + (rnd01(hs.x) - 0.5) * n1.x + (exact ? 0.0 : (rnd01(hs.y) - 0.5) * 2.0 * t1.z);
if (count > 1 && n1.y != 0.0) ang += -n1.y + 2.0 * n1.y * float(i) / float(n1.y < 3.1241393 ? count - 1 : count); // pattern_degrees (00c0b440): the aim of the first root projectile is turned by -pattern, then a step of 2 pattern / (N - 1, or N when the pattern is 179 degrees or more) per projectile
float speed = (t0.x + (t0.y - t0.x) * rnd01(hs.z)) * n1.z;
if ((int(r1.y) & 4) != 0) speed = length(r0.zw); // a script's shot: its own velocity
if ((fti(tb + 16).x & 256) != 0) speed = 5400.0 / 60.0; // projectile_type LIGHTNING (00c0d290 case 1): the aim direction x 5400 px/s, whatever the speed range
const int shooterSlot = int(r1.w) >> 1;
if (shooterSlot > 0 && !done && count == 1 && s0.y == 0 && fti(tb + 1).w >= 0 && (ents[shooterSlot - 1].ex8.x & 256) != 0 && ents[shooterSlot - 1].st.x != 3) {
// Message_Shot (00c0d290): the shooter's script_shot scripts see the projectile first; the host makes the runtime copy, lets them change it and shoots it again (event kind -3: two entries)
uint xi = uint(atomicAdd(xhead.x, 2));
if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -3.0, float(1 + type)); xe[xi + 1u] = vec4(ang, speed, base, float(shooterSlot + ((int(r1.y) & 3) << 20))); }
continue;
}
if ((fti(tb + 16).x & 512) != 0) { // projectile_type MATERIAL_PARTICLE (00c0d290 case 3): on_death_emit_particle_count real particles of on_death_emit_particle_type with the launch
// velocity and the VelocityComponent gravity, and no projectile (it is removed at once)
const int dp = fti(tb + 17).w, dmat = (dp & 0xFFFF) - 1, dn = min(dp >> 16, 64);
for (int k = 0; k < dn && dmat >= 0; ++k) emit_material_particle(r0.xy, vec2(cos(ang), sin(ang)) * speed, max(t0.w, 60.0), t1.x, mats[3u * uint(dmat)].x, dmat);
continue;
}
const uint serial = atomicAdd(fcnt[0], 1u); // the cast serial identifies the projectile beyond its ring slot
uint slot = serial % uint(FP_MAX);
for (uint k = 0u; k < 64u && fprojs[slot].d.w != 0; ++k) slot = (serial + k + 1u) % uint(FP_MAX); // (a live record is not overwritten: the next free one of 64, else the ring's)
if (fprojs[slot].d.w != 0) slot = serial % uint(FP_MAX);
FProj p;
p.a = vec4(r0.xy, cos(ang) * speed, sin(ang) * speed);
{ // lifetime: -1 never runs out (the timer of 00c06fc0 skips it), lifetime_randomness: + random(-r, r) (ProjectileComponent +50)
const int lr = fti(tb + 17).z;
const float lrand = lr > 0 ? float(int(hash3(seed, uint(i), 91u) % uint(2 * lr + 1)) - lr) : 0.0;
p.b = vec4(t0.z + n2.x, t0.w < 0.0 ? 1e9 : max(1.0, t0.w + n1.w + lrand), t1.x + n2.y, t1.y);
}
p.c = vec4(ftf(tb + 2).x + n2.w, ftf(tb + 2).y + n3.x, 0.0, n2.z);
p.e = vec4(rnd01(hs.w) * 6.2832, (fti(tb + 1).w & 32) != 0 ? (rnd01(hs.z) - 0.5) * 0.5 : speed, float(int(r1.w) >> 1), float((int(r1.y) >> 3) & 0xFFFFFF)); // e.y: the spin of a thrown body, else the speed at the start (mInitialSpeed, damage_scaled_by_speed) // e.z: the slot + 1 of the shooter (0 = unknown), e.w: its herd + 1
const int shotBits = int(n3.w); // bit 0 friendly fire, bits 1-12 trail material + 1, bits 13- its amount
p.f = vec4(n0.z, n0.w, n3.z, float(shotBits >> 1)); // the wand's electricity / ice adds, knockback add, trail
p.c.w += float(ft[tb + 7].y); // bounces_left of the projectile file
{ // the perks scale the shot once, here: the projectile damage and (unless not_scaled_by_gamefx) the explosion damage
const float dm = dealt_of(int(p.e.z));
p.b.x *= dm;
if ((fti(tb + 1).w & 32768) == 0) p.c.y *= dm;
}
p.d = ivec4(type, first + i, ((int(r1.y) & 3) << 8) | ((shotBits & 1) << 10) | (min(int(n3.y) & 8191, 4095) << 12) | (((int(n3.y) >> 13) & 15) << 24) | (n2.z > 0.0 ? (1 << 28) : 0), int(serial + 1u)); // bits 24-27: lightning_count, bit 28: the wand added bounces (00c0b440 then sets bounce_at_any_angle) // team in bits 8-9, the critical chance (percent) from bit 12 // team in bits 8-9 (1 the player's wand, 2 enemies), fired triggers in the low bits
fprojs[slot] = p;
if ((fti(tb + 11).y & 1) != 0) { // a projectile with components the GPU does not simulate: the host makes its runtime twin (event kind -4, two entries)
uint xi = uint(atomicAdd(xhead.x, 2));
if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -4.0, float(slot)); xe[xi + 1u] = vec4(float(serial + 1u), float(type), float(shooterSlot), float(int(r1.y) & 3)); }
}
if ((int(r1.w) & 1) != 0) { // muzzle flash of the first projectiles of a cast
ivec4 t6 = fti(tb + 6);
float fr = uf(ft[tb + 6].w);
if (fr > 0.0 && i == 0) add_anim(r0.xy, -1, min(fr, 40.0) * 0.5, 0.55, 5, uint(t6.z), 0.0);
int mb = int(ft[tb + 7].x);
if (mb >= 0 && i == 0) { // the muzzle flash sprite, along the shot
ivec4 m0 = fti(BOOM_BASE + mb * BOOM_STRIDE), m1 = fti(BOOM_BASE + mb * BOOM_STRIDE + 1);
add_anim(r0.xy, mb, 0.0, 0.0, max(m0.w, 1) * max(m1.x, 1) + 2, uint(m1.y), base);
}
// (the cast flash is the muzzle_flash_file sprite and the light only: no sparks of a colour of ours - the wall pieces of WALL_SQUARE made gold dust)
}
}
}
barrier();
if (gl_LocalInvocationID.x == 0u) { if (list == 0) hhead.x = 0; else hhead.y = 0; }
}
+722
View File
@@ -0,0 +1,722 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "fx_common.glsl"
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
#include "biome_mods.glsl"
// Flight of the wand projectiles, one invocation per projectile: gravity, air friction, collisions with the world (die or bounce)
// and with creatures, triggers (timer, hit, death) that queue the next shot of the wand, explosions (damage, holes, material, push of rigid
// bodies, visuals), particle emitters, light and the sprite.
const int TF_COLLISION_DIE = 1, TF_EXPLODE = 2, TF_HOLE = 4, TF_ROTATE = 8, TF_ADDITIVE = 16, TF_PHYSICS = 32, TF_DIE_IN_LIQUID = 64, TF_FRIENDLY = 128, TF_NO_PLAYER = 256, TF_HIT_EFFECTS = 512;
// The entity a projectile touches: the player or a creature / prop. The shooter's herd is spared (ex8.y of an entity holds its herd + 1) unless the projectile has friendly fire.
// What the shooter deals: the DAMAGE_MULTIPLIER game effect etc. (ex7.y of creatures and the player, set by the host)
// The critical hit of a hit: the chance (percent) of the shot, +100 with the CRITICAL_HIT_BOOST of the type while the hit entity has the condition, + what the shooter's game effects add (ex7.z);
// the damage is multiplied by 5 (DAMAGE_CRITICAL_HIT_MULTIPLIER); a chance above 100 % multiplies the multiplier by the extra, the victim's critical_damage_resistance scales it
float crit_mult(int chance, int flags, int shooter, int victim, uint seed) {
float c = float(chance);
const int cond = (flags >> 10) & 7;
if (cond != 0 && victim >= 0) { // the HitEffectComponent conditions are on the HIT entity
const Ent s = ents[victim];
const int z = s.ex8.z;
const bool fresh = ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF);
bool ok = false;
if (cond == 1) ok = s.ex4.w > 0.0;
else if (cond == 2) ok = fresh && (z & 4) != 0;
else if (cond == 3) ok = fresh && (z & 2) != 0;
else if (cond == 4) ok = fresh && (z & 1) != 0;
if (ok) c += 100.0;
}
if (shooter > 0) { const Ent s = ents[shooter - 1]; if (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) c += s.ex7.z; }
if (c <= 0.0) return 1.0;
// 00ab9a90: rand % 100 + 1 <= chance; m = the packet's multiplier (5), x chance / 100 above 100 %; the victim's critical_damage_resistance r gives (1 - r) (m - 1) + 1, no crit at r >= 1
const float r = victim >= 0 ? ents[victim].ex19.x : 0.0;
if (r >= 1.0 || float(hash3(seed, 17u, 99u) % 100u) >= c) return 1.0;
const float m = c > 100.0 ? 5.0 * c / 100.0 : 5.0;
return (1.0 - r) * (m - 1.0) + 1.0;
}
// A critical hit (00ab9a90: the multiplier after the resistance above 1): the host loads data/entities/particles/critical_hit.xml at the point of the message and marks the damage number (event kind -9)
void note_crit(vec2 at, int victim) {
uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(at, -9.0, float(victim));
if (victim >= 0) atomicOr(ents[victim].ex3.z, 0x10000); // a critical hit this tick: the victim's hit flash is red (00ab5b00: shader 6 + crit)
}
// DAMAGE_BOOST of a hit effect: the type flags hold the stain it needs on the victim (bits 24-26, 1 + the stain bit) and the factor (bits 27-30)
float boost_of(int flags, int victim) {
const int need = (flags >> 24) & 7, mult = (flags >> 27) & 15;
if (need == 0 || mult == 0) return 1.0;
const int z = ents[victim].ex8.z;
return (((z >> (need - 1)) & 1) != 0 && ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF)) ? float(mult) : 1.0;
}
vec2 gAhead; // the next point of the projectile's path (a body's pixel there is a hit on its entity)
// 00c08d10: the shooter (mWhoShot) is hit only when collide_with_shooter_frames is not -1 and has run out (bits 16-23 of the flags: 255 = -1, never); friendly_fire
// (00c091f0) only lets the shooter's herd be hit. A shot with the friendly_fire modifier (PIERCING_SHOT...) gets friendly_fire 1 and 6 frames (00c0b440): shot_flags.
bool shooter_safe(int flags, int age) { const int f = (flags >> 16) & 255; return f == 255 || age < f; }
int shot_flags(int typeFlags, int dz) { return ((dz >> 10) & 1) != 0 ? (typeFlags & ~(255 << 16)) | TF_FRIENDLY | (6 << 16) : typeFlags; }
bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit) {
hit = -1;
for (int k = 0; k < ctrl.inp.w; ++k) {
const int kind = ents[k].st.x;
if (kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5) { // a worm's segment (WormComponent hitbox_radius around every part): the hit goes to the worm
const int root = int(ents[k].ex19.w) - 1;
if (root <= 0 || root >= ctrl.inp.w || !(ents[root].st.x == 1 || ents[root].st.x == 2) || (ents[root].st.w & 8) != 0) continue;
if (root == shooter - 1 && shooter_safe(flags, age)) continue;
if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[root].ex8.y == herd) continue;
if (distance(pos, ents[k].a.xy) <= ents[k].ex19.z + 1.0) { hit = root; return true; }
continue;
}
if (k == 0 ? (kind != 3 || (flags & TF_NO_PLAYER) != 0) : !(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8))) continue;
if ((ents[k].st.w & (8 | 4096)) != 0) continue; // (4096: its sprites, damage model and hitbox are switched off - Olematon away from an Evil Eye)
if (k == shooter - 1 && shooter_safe(flags, age)) continue;
if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[k].ex8.y == herd && kind != 4) continue; // (a berserk shooter has no herd: its projectiles hurt everybody)
if (kind == 4 && ents[k].ex7.w > 0.5) { if (body_slot(gAhead) == k || body_slot(pos) == k) { hit = k; return true; } continue; } // no HitboxComponent: its pixels (00c073b0)
const float r = k == 0 ? 1.0 : 2.0;
const vec2 d = pos - ents[k].a.xy;
if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; }
}
return false;
}
bool creature_at(vec2 pos, float r, out int hit) {
hit = -1;
for (int k = 1; k < ctrl.inp.w; ++k) {
int kind = ents[k].st.x;
if (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & 8) != 0) continue;
vec2 d = pos - ents[k].a.xy;
if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; }
}
return false;
}
void queue_shot(int node, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) {
float len = length(vel);
vec2 dir = len < 1e-6 ? vec2(0.0, 1.0) : vel / len;
int next = (pc.frame + 1) & 1;
int slot = next == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1);
if (slot >= 256) return;
hcast[(next * 256 + slot) * 2] = vec4(pos, dir); // at the trigger's position itself (00c0b440 puts the new projectile there)
hcast[(next * 256 + slot) * 2 + 1] = vec4(float(node), float(team + (herd << 3)), float(seed & 0xFFFFFFu), float(shooter * 2));
}
// The triggers of a projectile (recorded by the natives 00b283e0 / 00b28510 / 00b28600 into three lists of the ProjectileComponent: timers +0x610, hit-world +0x61c, death +0x628;
// kinds here 1 timer, 2 hit world, 3 death). mode 1: the update's countdown (00c06c60): a timer whose delay has run out fires and goes; 2: a hit on the world (00c073b0 -> 00c06250):
// the hit-world and the waiting timers fire and go; 3: an impact on a creature (00c091f0): the hit-world and the waiting timers fire and stay (a projectile that pierces casts
// them again at its next impact); 4: the lifetime runs out (00c06fc0): the waiting timers fire; 5: the projectile is removed (00c0e3e0): the death triggers fire
void fire_triggers(int spec, inout int fired, int mode, int age, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) {
ivec4 s0 = hspecs[spec * 4];
for (int t = 0; t < min(s0.y, 3); ++t) {
ivec4 tr = hspecs[spec * 4 + 1 + t]; // kind, delay, node
if ((fired & (1 << t)) != 0 || tr.z < 0) continue;
// (mode 1: the counter starts at the delay and fires when the decrement reaches 0: a delay of 0 never does)
bool go = mode == 1 ? (tr.x == 1 && tr.y > 0 && age == tr.y) : (mode == 2 || mode == 3) ? (tr.x == 1 || tr.x == 2) : mode == 4 ? tr.x == 1 : tr.x == 3;
if (!go) continue;
vec2 v = vel;
if (mode == 5 && dot(vel, vel) * 3600.0 < 100.0) { // 00c0e3e0: nearly at rest (|v| < 10 px/s): a random direction (and length 20..80 px/s) for each death trigger
const float ang = rnd01(hash3(seed, uint(t), 131u)) * 6.2831855;
v = vec2(cos(ang), sin(ang)) * (rnd01(hash3(seed, uint(t), 132u)) * 60.0 + 20.0) / 60.0;
}
if (mode != 3) fired |= 1 << t;
queue_shot(mode == 2 ? tr.w : tr.z, pos, v, seed + uint(t), team, shooter, herd); // (.w: the payload with its child_speed_multiplier, 00c06250)
}
}
void explode(vec2 pos, float radius, float damage, int type, int team, int shooter, int herd, int critChance, uint critSeed) {
int tb = type * TYPE_STRIDE;
vec4 t2 = ftf(tb + 2);
ivec4 t1 = fti(tb + 1);
int cellMat = int(ft[tb + 2].w);
float cellProb = uf(ft[tb + 3].x);
if (radius <= 0.0) return;
for (int k = 1; k < ctrl.inp.w; ++k) // corpse parts in range bleed
if (ents[k].st.x == 4 && ents[k].ex7.y > 0.0 && distance(ents[k].a.xy, pos) <= radius) body_damage(k, pos, 1.0);
// 00657960 (docs/EXPLOSION_KNOCKBACK_AND_VINES.md): every entity whose position is within explosion_radius, except the config's dont_damage_this (the shooter only when the projectile has
// explosion_dont_damage_shooter), takes the config's damage in full (only with damage_mortals; the perks scaled it at the Shot) and gets a knockback: scalar = radius * knockback_force *
// 10 * (0.5 t + 0.5), t = 1 - distance / radius clamped; its direction is the radial physics force (lerp of physics_explosion_power.min / .max by t: zero = no kick); it is compared with
// minimum_knockback_force before the division by the CharacterDataComponent mass, the vertical part is always upward, and a killing hit gives none
const vec4 t8 = ftf(tb + 8);
const bool mortals = t8.w > 0.5;
const vec2 pw = unpackHalf2x16(ft[tb + 7].w); // physics_explosion_power.min / .max
if (mortals && damage > 0.0) {
for (int k = 0; k < ctrl.inp.w; ++k) {
int kind = ents[k].st.x;
if (k == 0 ? (ents[0].st.w & 8) != 0 : (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & (8 | 4096)) != 0)) continue;
if ((t1.w & 16384) != 0 && k == shooter - 1) continue;
const vec2 c = ents[k].a.xy;
const float dist = distance(c, pos);
if (dist > radius) continue;
const float ck = crit_mult(critChance, t1.w, shooter, k, critSeed + uint(k) * 7919u);
if (ck > 1.0) note_crit(c, k);
const float dk = damage * ents[k].ex9.y * ck; // the victim's explosion multiplier (and protections); the damage message carries the projectile's ConfigDamageCritical (00657960 copies chance / multiplier), TakeDamage rolls it per victim
note_block(k, damage, dk);
atomicAdd(ents[k].x.x, int(dk * 1000.0));
note_attacker(k, shooter);
if (type >= 0) ents[k].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF));
atomicOr(ents[k].ex3.z, 2);
const vec2 away = normalize(c - pos + vec2(0.0, -0.01));
{ // 00657960: the packet's impulse = the direction x ((power.max - power.min) x (1 - d / R) + power.min) x physics_multiplier_ragdoll_force x 3600 (the blood of the hit
// and the ragdoll take it); RAGDOLL_FX of the explosion packet: the constructor's NORMAL
const float a = ((pw.y - pw.x) * clamp(1.0 - dist / radius, 0.0, 1.0) + pw.x) * (float((fti(tb + 11).y >> 8) & 4095) / 256.0) * 3600.0;
if (k != 0) { // (not the player: its aux.x is the maximum hp - a blast there made the respawn hp 765)
atomicExchange(ents[k].aux.x, int(away.x * a * 1000.0)); atomicExchange(ents[k].aux.y, int(away.y * a * 1000.0));
atomicAnd(ents[k].ex3.z, ~0xF000); atomicOr(ents[k].ex3.z, 1 << 12);
}
}
const float tt = clamp(1.0 - dist / radius, 0.0, 1.0);
const float physF = mix(pw.x, pw.y, tt); // (the multiplier +0x108 defaults to 1)
const float sc = radius * t8.z * 10.0 * (0.5 * tt + 0.5);
if ((kind == 1 || kind == 3) && t8.z != 0.0 && physF != 0.0 && dist > 0.0 && ents[k].hp.x - dk > 0.0 && abs(sc) >= ents[k].ex11.x && (ents[k].ex8.x & 96) == 0 && ents[k].ex8.y > 0) { // (96: KNOCKBACK or KNOCKBACK_IMMUNITY on it; ex8.y > 0: it has a GenomeDataComponent)
const float mass = intBitsToFloat(ents[k].rg2.w);
vec2 imp = (c - pos) / dist * sc;
if (mass != 0.0) imp /= mass;
imp.y = -abs(imp.y);
ents[k].ex11.yz = imp / 60.0; // the pending kick (pixels per second -> cells per frame); the entity pass adds it, inverts it when confused and starts the knockback state
}
}
}
if ((t1.w & TF_HOLE) != 0) {
if (radius <= 64.0) add_edit(vec4(pos, radius, t2.z), vec4(2.0, ftf(tb + 7).z, cellMat >= 0 ? float(cellMat) : 0.0, cellMat >= 0 ? cellProb * 100.0 : 0.0)); // angular rays (explode_rays.comp), which also create the cells of create_cell_material
else add_edit(vec4(pos, radius * 0.5, t2.z), vec4(0.0));
}
if (((fti(tb + 9).w >> 12) & 255) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -2.0, float(1 + type)); } // config_explosion.load_this_entity: the host spawns the entities (event kind -2)
if (cellMat >= 0 && cellProb > 0.0 && ((t1.w & TF_HOLE) == 0 || radius > 64.0)) add_edit(vec4(pos, radius * 0.6, 0.0), vec4(1.0, float(cellMat), cellProb, 0.0));
{ // the force on the rigid bodies (00657960): the event is a pair, (x, y, radius in pixels, -1) then (force min, force max)
const uint ph = ft[tb + 7].w;
const vec2 fr = unpackHalf2x16(ph);
uint xp = uint(atomicAdd(xhead.x, 2));
if (xp + 1u < 512u) { xe[xp] = vec4(pos, radius, -1.0); xe[xp + 1u] = vec4(fr, float(1 + type), 0.0); } // (z: the type, for the explosion's sound)
}
}
// The sprite of a projectile, rotated along the flight when it is not round.
// scale: the entity's transform scale (a script's EntitySetTransform on the twin: summon_portal_vibrate.lua pulses the portal 0.1 .. 0.42); the host keeps it as -scale in e.x of
// a projectile that is no thrown body (e.x is the tumble angle of those only)
void draw_sprite(vec2 pos, vec2 vel, int age, ivec4 t1, ivec4 t5, ivec4 t6, float spin, float scale) {
ivec2 sz = ivec2(t5.y, t5.z);
scale = max(scale, 0.01);
if (sz.x > 0 && sz.y > 0) {
int frame = (age / max(t6.x, 1)) % max(t5.w, 1);
float ang = (t1.w & TF_PHYSICS) != 0 ? spin : ((t1.w & TF_ROTATE) != 0 ? atan(vel.y, vel.x) : 0.0); // thrown bodies tumble, long sprites follow the flight
float c = cos(ang), s = sin(ang);
float r = 0.5 * length(vec2(sz)) * scale + 1.0;
ivec2 lo = ivec2(floor(pos - vec2(r)));
int n = int(2.0 * r) + 2;
for (int j = 0; j < n * n; ++j) {
ivec2 w = lo + ivec2(j % n, j / n);
vec2 d = vec2(w) + 0.5 - pos;
ivec2 sp = ivec2(floor(vec2(sz) * 0.5 + vec2(c * d.x + s * d.y, -s * d.x + c * d.y) / scale));
if (sp.x < 0 || sp.y < 0 || sp.x >= sz.x || sp.y >= sz.y) continue;
ivec2 q = w - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
uint t = fxtex[t5.x + frame * sz.x * sz.y + sp.y * sz.x + sp.x];
if ((t >> 24) != 0u) entlayer[q.y * pc.gw + q.x] = (t1.w & TF_ADDITIVE) != 0 ? (0x01000000u | (t & 0xFFFFFFu)) : t; // alpha 1 marks additive light
}
} else if ((t1.w & 8192) == 0) { // (an entity with only emitters has no body to draw)
ivec2 w = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
ivec2 q = w + ivec2(x, y);
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = 0xFF000000u | uint(t6.y);
}
}
}
// ---- VERLET projectiles (the tentacles: projectile_type VERLET, a VerletPhysicsComponent chain in a child entity, a VerletWeaponComponent on the root) ----
// 00c0d290 (the shot, case 2): every point of the chain starts at the origin with prev = origin - aim x speed (so it moves speed px a frame), the projectile
// itself does not move; each frame the chain integrates (00c37d30: cur + (cur - prev) x velocity_dampening, liquid_damping in a liquid; a solid cell
// stops the point halfway or lifts it one cell) and relaxes twice (00c38310, stiffness x 0.5), its first point held at the origin (follow_entity_transform;
// the weapon system 00c3fdc0 puts the chain on the shooter's hand every frame); the projectile's position is the last point. 00c3fdc0 hurts with every point:
// min(damage_coeff x |v|^2, damage_max), nothing under damage_min_step, the boxes of the creatures within damage_radius (not the shooter; the player at most once in
// 20 frames, mPlayerCooldownEnd), times (lifetime left) / fade_duration_frames in the last frames. 17 records a slot: the 16 points (position, previous), then
// (the anchor: its offset from the shooter or its position, 1 when it follows the shooter, the player's cooldown end).
// TODO(to-do #38): the stains of the cells (stain_cells_probability).
layout(std430, set = 0, binding = 63) buffer VerletPts { vec4 vpts[]; };
const int VERLET_BASE = 13696, VERLET_STRIDE = 20;
void verlet_draw(uint id, int vi, int n, float fadeK, uint seed) {
const int vb = VERLET_BASE + vi * VERLET_STRIDE;
const int nparts = fti(vb + 1).w;
if (nparts <= 0) return;
const uint base = id * 17u;
for (int i = 0; i < n; ++i) {
const ivec4 s = fti(vb + 4 + min(i, nparts - 1));
const vec4 sf = ftf(vb + 4 + min(i, nparts - 1));
if (s.x < 0) continue;
const int w = s.y & 0xFFFF, h = s.y >> 16;
if (w <= 0 || h <= 0) continue;
const vec2 a = vpts[base + uint(i)].xy;
vec2 d = i + 1 < n ? vpts[base + uint(i) + 1u].xy - a : (i > 0 ? a - vpts[base + uint(i) - 1u].xy : vec2(1.0, 0.0));
float len = length(d);
const vec2 dir = len > 0.01 ? d / len : vec2(1.0, 0.0), nrm = vec2(-dir.y, dir.x);
if (i + 1 >= n || len < float(w)) len = max(len, float(w)); // (the last point's sprite at its own length; scale_sprite_x stretches the others along their link)
const vec2 lo = min(min(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), min(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w)));
const vec2 hi = max(max(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), max(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w)));
const ivec2 c0 = ivec2(floor(lo)) - 1, c1 = ivec2(ceil(hi)) + 1;
if (c1.x - c0.x > 64 || c1.y - c0.y > 64) continue;
for (int y = c0.y; y <= c1.y; ++y)
for (int x = c0.x; x <= c1.x; ++x) {
const vec2 rel = vec2(x, y) + 0.5 - a;
const float u = dot(rel, dir), v = dot(rel, nrm) + sf.w;
if (u < 0.0 || u >= len || v < 0.0 || v >= float(h)) continue;
const int sx = min(int(u / len * float(w)), w - 1), sy = int(v);
const uint t = fxtex[s.x + sy * w + sx];
if ((t >> 24) == 0u) continue;
if (fadeK < 1.0 && rnd01(hash3(uint(x * 73 + y * 151), seed, 11u)) > fadeK) continue; // (the fade: pixels drop out)
const ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = t;
}
}
}
bool verlet_blocking(vec2 p) { return solid_at(ivec2(int(floor(p.x)), int(ceil(p.y)))); }
void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team, int spec, int shooter, int herd, uint seed) {
const int vb = VERLET_BASE + vi * VERLET_STRIDE;
const ivec4 v0i = fti(vb), v1i = fti(vb + 1);
const vec4 v0 = ftf(vb), v1 = ftf(vb + 1), v2 = ftf(vb + 2);
const int n = clamp(v0i.x, 2, 16);
const float rest = v0.y, stiff = v0.z, damp = v0.w;
const uint base = id * 17u;
vec2 pts[16], prv[16];
const bool shooterOk = shooter > 0 && shooter - 1 < ctrl.inp.w && (ents[shooter - 1].st.w & 8) == 0;
if (age <= 1) { // the shot (00c0d290 case 2)
const vec2 origin = p.a.xy, v = p.a.zw * 60.0; // (the aim x the speed: speed px a frame)
for (int k = 0; k < n; ++k) { pts[k] = origin; prv[k] = origin - v; }
const bool follow = shooterOk && distance(origin, ents[shooter - 1].a.xy) < 24.0; // (cast from the hand: it follows the shooter; one a trigger made somewhere stays there)
vpts[base + 16u] = vec4(follow ? origin - ents[shooter - 1].a.xy : origin, follow ? 1.0 : 0.0, -1.0);
} else for (int k = 0; k < n; ++k) { const vec4 q = vpts[base + uint(k)]; pts[k] = q.xy; prv[k] = q.zw; }
vec4 aux = vpts[base + 16u];
const vec2 anchor = aux.z > 0.5 ? (shooterOk ? ents[shooter - 1].a.xy + aux.xy : pts[0]) : aux.xy;
if (age > 1) {
for (int k = 1; k < n; ++k) { // 00c37d30
vec2 delta = (pts[k] - prv[k]) * damp, np = pts[k] + delta;
if (v1i.x != 0 && verlet_blocking(np)) {
const vec2 mid = (pts[k] + np) * 0.5;
if (!verlet_blocking(mid)) np = mid;
else if (!verlet_blocking(mid - vec2(0.0, 1.0))) np = pts[k] - vec2(0.0, 1.0);
else np = pts[k];
} else if (liquid_nonsand(ivec2(floor(np)))) np = pts[k] + (pts[k] - prv[k]) * v1.y;
prv[k] = pts[k]; pts[k] = np;
}
for (int it = 0; it < 2; ++it) // 00c38310
for (int k = 0; k + 1 < n; ++k) {
const vec2 d = pts[k + 1] - pts[k];
const float l = length(d);
if (l < 1e-5) continue;
const float diff = (l - rest) / max(l, 0.01) * stiff * 0.5;
pts[k] += d * diff * 0.5; pts[k + 1] -= d * diff * 0.5;
}
}
pts[0] = anchor; prv[0] = anchor;
const int lifeLeft = int(p.b.y) - age, fade = max(v1i.z, 1);
const float fadeK = lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0;
// 00c3fdc0: the damage of every point
for (int k = 0; k < n; ++k) {
const vec2 v = pts[k] - prv[k];
float dmg = min(v2.w * dot(v, v), v2.z);
if (dmg < v2.y) continue;
dmg *= fadeK;
if (dmg <= 0.0) continue;
const float r = v2.x;
for (int e = 0; e < ctrl.inp.w; ++e) {
const int kind = ents[e].st.x;
if (!(kind == 1 || kind == 2 || kind == 3) || (ents[e].st.w & (8 | 4096)) != 0 || e == shooter - 1) continue;
const vec2 rel = pts[k] - ents[e].a.xy;
if (rel.x < ents[e].box.x - r || rel.x > ents[e].box.y + r || rel.y < ents[e].box.z - r || rel.y > ents[e].box.w + r) continue;
if (kind == 3) { if (float(pc.frame) < aux.w) continue; aux.w = float(pc.frame + 20); } // mPlayerCooldownEnd
if (kind != 3 && herd != 0 && ents[e].ex8.y == herd) continue; // (the shooter's herd is spared)
atomicAdd(ents[e].x.x, int(dmg * 1000.0));
note_attacker(e, shooter);
ents[e].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF));
atomicOr(ents[e].ex3.z, 1);
if (e != 0) { const vec2 imp = v * 60.0 * ftf(vb + 3).x; atomicExchange(ents[e].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[e].aux.y, int(imp.y * 1000.0)); } // the damage packet's impulse: impulse_coeff x the point's velocity (the blood and the ragdoll of the hit)
}
}
// 00c3fdc0: a point going back toward the shooter (its velocity toward the anchor) faster than 10 px a frame takes what is at it along: every entity whose position is within
// damage_radius of the point (not the shooter; creatures, the player, items) gets the point's velocity (VelocityComponent mVelocity, a knockback for a character)
for (int k = 1; k < n; ++k) {
const vec2 v = pts[k] - prv[k];
if (dot(anchor - pts[k], v) <= 0.0 || dot(v, v) <= 100.0) continue; // (|v|^2 > 100: faster than 10 px a frame)
const float r = v2.x;
for (int e = 0; e < ctrl.inp.w; ++e) {
const int kind = ents[e].st.x;
if (!(kind == 1 || kind == 2 || kind == 3 || kind == 4) || (ents[e].st.w & (8 | 32 | 128 | 4096)) != 0 || e == shooter - 1) continue; // (not dead, not a rigid body, not a hovering wand, not hidden)
const vec2 d = ents[e].a.xy - pts[k];
if (abs(d.x) > r || abs(d.y) > r) continue;
if (kind == 1 || kind == 3) ents[e].ex11.yz = v - ents[e].a.zw; // (00ab9910: a character's knockback; the entity pass adds the kick and holds it in the knockback state)
else ents[e].a.zw = v;
}
}
// 00c3fdc0: the physics bodies within physics_force_radius of a point (a Box2D query of the box) get their velocity moved toward the point's: the host applies
// v += (the point's displacement - v) x physics_impulse_coeff / 6 (event -30000 - the body's entity slot, the displacement as two halves)
if (ftf(vb + 3).z != 0.0) for (int k = 1; k < n; ++k) {
const float fr = ftf(vb + 3).y;
int bs = -1;
for (int q = 0; q < 5 && bs < 0; ++q) {
const vec2 o = q == 0 ? vec2(0.0) : (q == 1 ? vec2(fr, 0.0) : (q == 2 ? vec2(-fr, 0.0) : (q == 3 ? vec2(0.0, fr) : vec2(0.0, -fr))));
if (body_hit(pts[k] + o)) bs = body_slot(pts[k] + o);
}
if (bs < 0) continue;
uint xi = uint(atomicAdd(xhead.x, 1));
if (xi < 512u) xe[xi] = vec4(ftf(vb + 3).z, 0.0, -30000.0 - float(bs), uintBitsToFloat(packHalf2x16(pts[k] - prv[k]))); // (x: physics_impulse_coeff)
}
vpts[base + 16u] = aux;
const vec2 tip = pts[n - 1], tipV = pts[n - 1] - prv[n - 1];
bool alive = true, expired = false;
if (spec >= 0) fire_triggers(spec, fired, 1, age, tip, tipV, seed, team, shooter, herd);
{ // the ProjectileSystem's own hit (00c073b0 / 00c091f0) at the root, which 00c3fdc0 puts on the last point: the projectile's damage, and its hit triggers on every hit
// (00c091f0 casts the +0x61c and +0x610 lists and removes nothing: a piercing tentacle with a trigger casts its payload at each collision)
const int tflags = fti(tb + 11).y, tw = fti(tb + 1).w;
int hk;
gAhead = tip;
if (age > 1 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0)
&& victim_at(tip, shooter, herd, shot_flags(tw, p.d.z), age, hk)) {
const float hitDmg = hit_damage(hk, p.b.x, tb, p.f) * boost_of(tw, hk) * fadeK;
if (hitDmg > 0.0) atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0));
note_attacker(hk, shooter);
ents[hk].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF));
atomicOr(ents[hk].ex3.z, 1);
if (spec >= 0) fire_triggers(spec, fired, 3, age, tip, tipV, seed, team, shooter, herd);
if ((tflags & 4) == 0) alive = false;
}
}
if (age >= int(p.b.y)) { alive = false; expired = true; }
if (!alive && spec >= 0) { if (expired) fire_triggers(spec, fired, 4, age, tip, tipV, seed, team, shooter, herd); fire_triggers(spec, fired, 5, age, tip, tipV, seed, team, shooter, herd); }
for (int k = 0; k < n; ++k) vpts[base + uint(k)] = vec4(pts[k], prv[k]);
if (alive) verlet_draw(id, vi, n, fadeK, seed);
p.a = vec4(tip, 0.0, 0.0);
p.c.z = float(age);
p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF);
p.d.w = alive ? p.d.w : 0;
fprojs[id] = p;
}
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= uint(FP_MAX)) return;
FProj p = fprojs[id];
if (p.d.w == 0) return;
int type = p.d.x, tb = type * TYPE_STRIDE;
ivec4 t1 = fti(tb + 1), t3 = fti(tb + 3), t5 = fti(tb + 5), t6 = fti(tb + 6);
vec4 t4 = ftf(tb + 4);
const int vflag = fti(tb + 16).x;
if ((vflag & 4096) != 0 && pc.pass_ == 1) { // a VERLET chain between ticks: drawn as it is
const int vi = ((vflag >> 13) & 31) - 1;
const int fade = max(fti(VERLET_BASE + vi * VERLET_STRIDE + 1).z, 1), lifeLeft = int(p.b.y) - int(p.c.z);
verlet_draw(id, vi, clamp(fti(VERLET_BASE + vi * VERLET_STRIDE).x, 2, 16), lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0, hash3(id, uint(pc.frame), 5u));
return;
}
if (pc.pass_ == 1) { // between ticks: only draw, between the last two positions
draw_sprite(p.a.xy - p.a.zw * (1.0 - ctrl.view.z), p.a.zw, int(p.c.z), t1, t5, t6, p.e.x + p.e.y * ctrl.view.z, (t1.w & TF_PHYSICS) == 0 && p.e.x < 0.0 ? -p.e.x : 1.0);
return;
}
uint seed = hash3(id, uint(pc.frame), 5u);
vec2 start = p.a.xy, pos = p.a.xy, vel = p.a.zw;
{ // waiting at the edge of what is loaded (far.glsl): its next position outside the window, inside the far map, in a block not resident (a biome-map tile
// without its area yet, or a block not generated yet): it stays where it is, its age frozen, and goes on from there once the block is resident
const ivec2 nw = ivec2(floor(pos + vel));
const ivec2 lq = nw - ivec2(pc.view_x, pc.view_y);
if ((lq.x < 0 || lq.y < 0 || lq.x >= pc.gw || lq.y >= pc.gh) && far_in_map(nw) && far_slot(nw) == 0) return;
}
int age = int(p.c.z) + 1, fired = p.d.z & 255, team = (p.d.z >> 8) & 3, bounces = int(p.c.w), spec = p.d.y, shooter = int(p.e.z), herd = int(p.e.w);
if ((vflag & 4096) != 0) { verlet_step(id, p, tb, ((vflag >> 13) & 31) - 1, age, fired, team, spec, shooter, herd, seed); return; } // VERLET: the chain (verlet_step)
if ((fti(tb + 11).x & 1) != 0) { // HomingComponent (00b323f0): the nearest entity with the target tag within detect_distance (or the shooter with target_who_shot) pulls the velocity
const vec4 h = ftf(tb + 10); // detect_distance, homing_velocity_multiplier, homing_targeting_coeff, max_turn_rate
const int hf = fti(tb + 11).x, shooterSlot = int(p.e.z);
int target = -1;
float bestD = h.x * h.x;
// 00b323f0: predefined_target first (alive, not invisible), then the shooter with target_who_shot, then the nearest tagged entity; invisible ones (INVISIBILITY) never
const bool shooterAlive = shooterSlot > 0 && (ents[shooterSlot - 1].st.w & 8) == 0 && (ents[shooterSlot - 1].ex8.x & 8192) == 0;
const int pre = fti(tb + 11).w;
vec2 preAim = vec2(0.0); bool preOk = false;
if (pre > 0 && pre - 1 < ctrl.inp.w && (ents[pre - 1].st.w & 8) == 0 && (ents[pre - 1].ex8.x & 8192) == 0) target = pre - 1;
else if (pre < 0 && -pre - 1 < FP_MAX && fprojs[-pre - 1].d.w != 0) { preAim = fprojs[-pre - 1].a.xy; preOk = true; } // a projectile (homing_projectile.lua aims at other shots)
if (target >= 0 || preOk) {}
else if ((hf & 4) != 0 && shooterAlive) target = shooterSlot - 1;
else for (int k = 0; k < ctrl.inp.w; ++k) {
const int kind = ents[k].st.x;
if (!(kind == 1 || kind == 2 || kind == 3) || (ents[k].st.w & 8) != 0 || (ents[k].ex8.x & 8192) != 0 || k == shooterSlot - 1 || (ents[k].ex8.x & ((hf >> 4) & 0x1FFF)) == 0) continue;
const vec2 dd = pos - ents[k].a.xy;
const float dsq = dot(dd, dd);
if (dsq < bestD) { bestD = dsq; target = k; }
}
if (target >= 0 || preOk) {
const vec2 tp = preOk ? preAim : ents[target].a.xy + vec2(ents[target].box.x + ents[target].box.y, ents[target].box.z + ents[target].box.w) * 0.5; // the centre of its hitbox
const vec2 delta = tp - pos;
const float dist = length(delta);
if (dist < h.x) {
if ((hf & 2) == 0) { // the velocity keeps homing_velocity_multiplier of itself and gains homing_targeting_coeff x the offset x dt x (1 - distance / detect_distance), in pixels per second
const float k = max(1.0 - dist / h.x, 0.0);
vel = vel * h.y + h.z * delta * (1.0 / 60.0) * k / 60.0;
} else { // just_rotate_towards_target: the velocity turns toward the target by at most max_turn_rate
const float sp = length(vel);
const float av = sp > 0.0 ? atan(vel.y, vel.x) : 0.0, at = dist > 0.0 ? atan(delta.y, delta.x) : 0.0;
const float TAU = 6.2831855;
float A = av - at; A = A - TAU * floor(A / TAU);
const float turn = min(A, h.w);
const float ang = (TAU - A <= A) ? turn : -turn;
vel = vec2(cos(ang) * vel.x - sin(ang) * vel.y, cos(ang) * vel.y + sin(ang) * vel.x);
}
}
}
}
// VelocityComponent (00c370c0): gravity, then v -= v dt air_friction (a negative friction speeds it up), then the terminal velocity (apply_terminal_velocity, pixels per second)
const vec4 t15 = ftf(tb + 15), t16f = ftf(tb + 16), t17 = ftf(tb + 17);
const int f16 = fti(tb + 16).x;
vel.y += p.b.z * bm_gravity(pos); vel.x += t17.x; // (gravity_y x the biome's entity_gravity_y_multiplier, 00c370c0)
vel -= vel * p.b.w;
if ((f16 & 64) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 > t16f.z * t16f.z) vel = normalize(vel) * (t16f.z / 60.0);
const bool inert = ((p.d.z >> 11) & 1) != 0; // on_collision_remove_projectile: the entity stuck where it hit, without its ProjectileComponent
if (inert) vel = vec2(0.0);
const bool removed = ((p.d.z >> 29) & 1) != 0; // the host removed it (EntityKill of its twin): the component goes, nothing of the update runs
const bool wasStuck = (p.d.z & 0x800) != 0;
if (spec >= 0 && !inert && !removed) fire_triggers(spec, fired, 1, age, pos, vel, seed, team, shooter, herd); // the countdown runs before the flight of the update (00c06c60)
int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y)))));
float spin = p.e.x, omega = p.e.y;
bool alive = !removed, hitWorld = false, stuck = false, hitEntity = false;
bool touchedWorld = false; vec2 trigVel = vec2(0.0); // a world hit this frame (bounce or stop): the hit-world triggers fire with the velocity reflected about the surface (00c06250)
// a thrown physics body (the bomb) rests on its shape, not on its centre: the lowest point of its image counts as well (half the smaller side below the position)
const vec2 foot = (t1.w & TF_PHYSICS) != 0 && t5.y > 0 ? vec2(0.0, 0.5 * float(min(t5.y, t5.z))) : vec2(0.0);
const int tflags = fti(tb + 11).y; // 2: collide_with_world 0 (goes through terrain), 4: penetrate_entities (hits every damage_every_x_frames frames, does not die), 8: collide_with_entities 0
const bool noWorld = (tflags & 2) != 0 || inert;
// penetrate_world (00c073b0): no collision with the world; when the path of the frame crosses it, the step is penetrate_world_velocity_coeff of the velocity
const bool penWorld = (f16 & 1) != 0 && !noWorld;
vec2 stepVel = vel;
if (penWorld) {
for (int i = 1; i <= steps; ++i) if (solid_at(ivec2(floor(pos + vel * (float(i) / float(steps)))))) { stepVel = vel * t15.x; break; }
}
const float gpEnergy = (f16 & 1) == 0 ? t15.y : 0.0; // ground_penetration_coeff x VelocityComponent mass
for (int i = 0; i < steps && alive; ++i) {
vec2 np = pos + stepVel / float(steps);
const bool bodyAt = body_hit(np) && (shooter <= 0 || body_slot(np) != shooter - 1); // (not the body of its shooter: a physics creature shoots from inside itself)
if (!noWorld && !penWorld && bodyAt && !solid_at(ivec2(floor(np)))) body_damage(body_slot(np), pos, p.b.x); // (the hit point: the last free position)
bool blocked = !noWorld && !penWorld && (solid_at(ivec2(floor(np))) || solid_at(ivec2(floor(np + foot))) || bodyAt);
if (blocked && gpEnergy > 0.0 && solid_at(ivec2(floor(np))) && !bodyAt) {
// ground_penetration (00c08a30, per cell of the ray): the kinetic energy coeff x mass x 0.5 x speed^2 is spent on the cell's hp; a cell that loses all of it is removed and the
// projectile flies on, slower; a cell harder than ground_penetration_max_durability_to_destroy or a speed of 10 px/s or less stops it
const ivec2 c = ivec2(floor(np));
const ivec2 q = c - ivec2(pc.view_x, pc.view_y);
const float v2 = dot(vel, vel) * 3600.0;
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && v2 > 100.0) {
const int ci = q.y * pc.gw + q.x;
const uint m = cells[ci] & 0xFFFFu;
const float dur = uintBitsToFloat(mats[3u * m + 2u].y);
const int maxd = fti(tb + 15).z;
if (!(maxd > 0 && dur > float(maxd))) {
const float energy = gpEnergy * 0.5 * v2;
const float hp = float((mats[3u * m + 2u].z >> 20) * 50u);
vel *= 1.0 - min(hp, energy) / energy;
stepVel = vel;
if (hp - min(hp, round(energy)) < 1.0) { cells[ci] = 0u; ext[ci] = vec2(0.0); blocked = false; }
}
}
}
if (blocked) {
if (t1.w < 0 && solid_at(ivec2(floor(np)))) { vel = vec2(0.0); break; } // a particle entity (SimplePhysicsComponent, 00c17fc0): a hit on the terrain keeps the position before it and zeroes the velocity (DieIfSpeedBelow then removes it)
if ((t1.w & TF_PHYSICS) != 0) { // a thrown body: bounces with loss, rolls and comes to rest; only the fuse makes it explode
const int brk = (fti(tb + 11).z >> 16) & 0xFFFF; // a breakable one (the egg): an impact faster than its break speed kills it, it explodes (projectiles.cpp break_speed)
if (brk > 0 && length(vel) * 60.0 > float(brk)) { alive = false; hitWorld = true; break; }
bool bx = solid_at(ivec2(floor(vec2(np.x, pos.y)))) || solid_at(ivec2(floor(vec2(np.x, pos.y) + foot))), by = solid_at(ivec2(floor(vec2(pos.x, np.y)))) || solid_at(ivec2(floor(vec2(pos.x, np.y) + foot)));
if (bx || !by) { vel.x = -vel.x * 0.4; vel.y *= 0.85; omega = -omega * 0.5 + vel.y * 0.15; }
if (by || !bx) { vel.y = -vel.y * 0.4; vel.x *= 0.85; omega = omega * 0.6 + vel.x * 0.2; }
if (length(vel) < 0.25) vel = vec2(0.0);
break;
}
if (body_hit(np) && t16f.w > 0.0 && body_slot(np) >= 0) { // 00c073b0: the body hit gets ApplyLinearImpulse(the frame's displacement x physics_impulse_coeff x VelocityComponent mass / 10) at the point (event -10000 - the entity slot)
uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(np, -10000.0 - float(body_slot(np)), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0)));
}
// the hit (00c073b0): bounces_left counts down at every hit; with some left, the velocity is reflected about the surface normal (16 rays of 5 px around the last free position,
// 0063fd80) and scaled by bounce_energy, unless the reflection points back into the wall (r.v <= 0.75, bounce_at_any_angle allows it); bounce_always then turns it around with a
// random turn of +-0.1 rad. Without a bounce the projectile stops there (velocity 0) and the death of 00c08b90 follows: on_collision_die kills it, on_collision_remove_projectile
// leaves it stuck, otherwise it stays where it stopped.
const int bl = bounces;
bounces = max(bl - 1, -1);
bool bounced = false;
vec2 nsum = vec2(0.0); int nh = 0; // the surface normal: 16 rays of 5 px around the last free position (0063fd80)
for (int k = 0; k < 16; ++k) {
const float a = float(k) * (6.2831855 / 16.0);
const vec2 d = vec2(-sin(a), cos(a)) * 5.0;
for (int s = 1; s <= 5; ++s) if (solid_at(ivec2(floor(pos + d * (float(s) / 5.0)))) || body_hit(pos + d * (float(s) / 5.0))) { nsum += d; ++nh; break; }
}
touchedWorld = true;
trigVel = (nh > 0 && dot(nsum, nsum) > 0.0) ? vel - normalize(nsum) * 2.0 * dot(normalize(nsum), vel) : -vel;
if (bl > 0) {
const vec2 v = vel * 60.0;
if (nh > 0 && dot(nsum, nsum) > 0.0) {
const vec2 n = normalize(nsum);
const vec2 r = v - n * 2.0 * dot(n, v);
if (dot(r, v) > 0.75 || (f16 & 4) != 0 || ((p.d.z >> 28) & 1) != 0) { vel = r * t15.w / 60.0; bounced = true; }
}
if (!bounced && (f16 & 2) != 0) {
const float a = rnd01(hash3(seed, uint(i), 97u)) * 0.2 - 0.1;
const vec2 rv = -vel * t15.w;
vel = vec2(cos(a) * rv.x - sin(a) * rv.y, sin(a) * rv.x + cos(a) * rv.y);
bounced = true;
}
}
if (bounced && (f16 & 2048) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -8.0, float(id)); } // bounce_fx_file: the host loads it at the bounce (00c073b0; event -8, the projectile's slot)
if (bounced) { pos += vel; break; } // (the position of the frame: the last free one + the new velocity x dt)
vel = vec2(0.0);
if ((t1.w & TF_COLLISION_DIE) != 0) { alive = false; hitWorld = true; }
else if ((f16 & 8) != 0) stuck = true;
break;
}
pos = np;
if (!inert && !stuck && (t1.w & TF_PHYSICS) == 0 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0)) {
int hk; // the herds decide who is hurt: everything except the herd of the shooter (unless friendly fire); never the player with dont_hit_player
gAhead = pos + stepVel / float(steps);
if (victim_at(pos, shooter, herd, shot_flags(t1.w, p.d.z), age, hk)) { // (the shot's friendly_fire modifier)
float speedScale = 1.0; // damage_scaled_by_speed: speed / mInitialSpeed (or damage_scale_max_speed when > 0)
if ((f16 & 128) != 0) { const float s0 = t17.y > 0.0 ? t17.y / 60.0 : p.e.y; speedScale = s0 > 0.0 ? length(vel) / s0 : 1.0; }
const float cm = crit_mult((p.d.z >> 12) & 4095, t1.w, shooter, hk, seed);
const float hitDmg = hit_damage(hk, p.b.x * speedScale, tb, p.f) * boost_of(t1.w, hk) * cm;
if (cm > 1.0) note_crit(pos, hk);
note_block(hk, p.b.x, hitDmg);
atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0));
if (ents[hk].st.x == 4) body_damage(hk, pos, hitDmg); // a prop with a liquid inventory leaks
if (ents[hk].st.x == 4 && ents[hk].ex7.w > 0.5 && t16f.w > 0.0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(gAhead, -10000.0 - float(hk), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0))); } // and the impulse on its body
{ // 00c091f0: knockback scalar = ProjectileComponent.knockback_force x speed (pixels per second) x the projectile's VelocityComponent mass; the force vector = velocity x ragdoll_force_multiplier;
// then the victim's gate (00ab9a90): abs(scalar) against minimum_knockback_force before the division by its mass, upward y, never on a killing hit
const vec4 t9 = ftf(tb + 9);
const int vk = ents[hk].st.x;
const vec2 fv = vel * 60.0 * t9.z;
const float kfh = t9.x + p.f.z; // ProjectileComponent knockback_force + the shot's knockback_force modifier
const float sc = kfh * length(vel * 60.0) * t9.y;
if (kfh != 0.0 && (vk == 1 || vk == 3) && length(fv) > 0.0 && ents[hk].hp.x - hitDmg > 0.0 && abs(sc) >= ents[hk].ex11.x && (ents[hk].ex8.x & 96) == 0 && ents[hk].ex8.y > 0) {
const float mass = intBitsToFloat(ents[hk].rg2.w);
vec2 imp = normalize(fv) * sc;
if (mass != 0.0) imp /= mass;
imp.y = -abs(imp.y);
ents[hk].ex11.yz = imp / 60.0;
}
}
note_attacker(hk, shooter);
ents[hk].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF)); // the projectile of the hit (script_damage_received's projectile_thats_responsible)
atomicOr(ents[hk].ex3.z, 1); // hit by a projectile
if ((t1.w & TF_HIT_EFFECTS) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(float(hk), float(shooter), -1.0, float(1 + type)); } // the hit effects of the projectile: the host applies them (victim slot, shooter slot + 1, type + 1)
if (hk != 0) { // the packet of 00c091f0: the impulse = velocity (px/s) x ragdoll_force_multiplier (the blood of the hit and the ragdoll take it), its RAGDOLL_FX
const vec2 imp = vel * 60.0 * ftf(tb + 9).z;
atomicExchange(ents[hk].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[hk].aux.y, int(imp.y * 1000.0));
atomicAnd(ents[hk].ex3.z, ~0xF000); atomicOr(ents[hk].ex3.z, ((fti(tb + 11).y >> 4) & 15) << 12);
}
if (spec >= 0) fire_triggers(spec, fired, 3, age, pos, vel, seed, team, shooter, herd); // the impact (00c091f0)
if ((tflags & 4) == 0) { alive = false; hitEntity = true; } // (a penetrating projectile flies on)
}
}
}
if (alive && t1.w < 0 && length(vel) < float(fti(tb + 9).w >> 20 & 4095) / 256.0) alive = false; // DieIfSpeedBelowComponent of a particle entity
// 00c370c0: the count N of liquid cells (not powder) in the 3x3 around the new integer position (float coordinates truncate toward zero), taken only when that position changed.
// Every candidate counts, then (displace_liquid on, 75 % of the draws) its cell becomes a particle that flies off against the motion (00717670: rotated, 0.1 x); the velocity keeps
// 1 - liquid_drag * dt * N of itself (VelocityComponent liquid_drag, unclamped), and N above liquid_death_threshold kills the projectile (liquid_death_threshold, default 0: off).
if (alive && any(notEqual(ivec2(pos), ivec2(start)))) {
int n = 0;
const ivec2 q = ivec2(pos);
for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(q + ivec2(dx, dy))) ++n;
if (n > 0 && fti(tb + 8).y != 0) {
for (int k = 0; k < 9; ++k) {
const ivec2 c = q + ivec2(k % 3 - 1, k / 3 - 1);
if (liquid_at_pos(vec2(c) + 0.5) == 0u || (hash3(seed, uint(k), 81u) % 101u) >= 75u) continue;
const float ang = 0.6 * float(hash3(seed, uint(k), 82u) & 0x7FFFu) / 32767.0 - 0.3;
displace_cell(c, -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel)));
}
}
vel *= 1.0 - ftf(tb + 8).x * (1.0 / 60.0) * float(n);
// (liquid_death_threshold of the VelocityComponent defaults to 0: no death by the count; die_on_liquid_collision is the collision with a liquid cell)
if ((t1.w & TF_DIE_IN_LIQUID) != 0 && liquid_nonsand(ivec2(pos))) { alive = false; hitWorld = true; }
}
if (alive && !inert && (f16 & 16) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 <= t16f.y * t16f.y) { // die_on_low_velocity: the death of a hit (00c08b90)
if ((t1.w & TF_COLLISION_DIE) != 0) alive = false; else if ((f16 & 8) != 0) stuck = true;
}
bool expired = false;
if (alive && !inert && !stuck && age >= int(p.b.y)) { alive = false; expired = true; } // lifetime out (the inert entity has no ProjectileComponent, so no lifetime)
if (!alive && fti(tb + 17).w != 0) { // on_death_emit_particle: on_death_emit_particle_count real particles of the material
const int dp = fti(tb + 17).w, dmat = (dp & 0xFFFF) - 1, dn = min(dp >> 16, 64);
for (int k = 0; k < dn; ++k) {
const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 93u)), rnd01(hash3(seed, uint(k), 94u))) - 0.5) * 2.0 / 60.0 * 30.0;
emit_material_particle(pos, ev, 30.0, PART_GRAV, mats[3u * uint(dmat)].x, dmat);
}
}
if (!alive && (hitWorld || hitEntity) && (f16 & 1024) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -6.0, float(type)); } // on_collision_spawn_entity: the host loads spawn_entity there (00c08b90)
// triggers (see fire_triggers): a hit on the world fires the hit-world and waiting timers in the reflected direction (also when the projectile bounces and lives on); the end of the
// lifetime fires the waiting timers; the removal of the projectile, whatever ended it, fires the death triggers
if (touchedWorld && spec >= 0) fire_triggers(spec, fired, 2, age, pos, trigVel, seed, team, shooter, herd);
if (stuck && !wasStuck && spec >= 0) fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd); // on_collision_remove_projectile: the ProjectileComponent goes, its removal (00c0e3e0, slot 15 of the system) casts the death triggers
if (!alive) {
if (spec >= 0) {
if (expired) fire_triggers(spec, fired, 4, age, pos, vel, seed, team, shooter, herd);
fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd);
}
if ((t1.w & TF_EXPLODE) != 0) {
explode(pos, p.c.x, p.c.y, type, team, shooter, herd, (p.d.z >> 12) & 4095, seed);
if (((p.d.z >> 24) & 15) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -7.0, float((p.d.z >> 24) & 15)); } // lightning_count: that many electricity.xml at the blast (event kind -7)
if (t3.y >= 0) boom_fx(t3.y, pos, seed, p.c.x);
} // (a projectile that does not explode leaves nothing of its own when it dies: no sparks - wall pieces absorbed by Spells To Power vanish into their red poofs only)
}
if (alive && p.f.w > 0.5) { // the shot's trail_material: real particles of the material along the path (00c0b440: an emitter with count = amount, velocity +- sqrt(amount) x 0.5)
const int tm = int(p.f.w), tmat = (tm & 4095) - 1, amount = tm >> 12;
const float tv = sqrt(float(amount)) * 0.5 / 60.0;
for (int k = 0; k < min(amount, 8); ++k) {
const vec2 ep = mix(start, pos, rnd01(hash3(seed, uint(k), 71u)));
const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 72u)), rnd01(hash3(seed, uint(k), 73u))) - 0.5) * 2.0 * tv;
emit_material_particle(ep, ev, 20.0, PART_GRAV, mats[3u * uint(tmat)].x, tmat);
}
}
if (alive) {
// particle emitters (cosmetic)
for (int e = 0; e < t3.w; ++e) {
int eb = EMIT_BASE + (t3.z + e) * EMIT_STRIDE;
ivec4 e0 = fti(eb), e2 = fti(eb + 2);
vec4 e0f = ftf(eb), e1 = ftf(eb + 1), e3 = ftf(eb + 3);
const int iv2 = max(e2.y & 0xFF, 2); // the mean emission interval in half frames (the min .. max interval of 00bc75e0 is random; 1..2 frames emits 2 times in 3 frames, not every frame)
const float away = float(e2.y >> 16) / 60.0; // velocity_always_away_from_center, cells per frame
const uint attr = uint(e2.y >> 8) & 0xFFu; // attractor_force
{ const int ph = age + int(id) + e * 3; if ((ph * 2) / iv2 == (ph * 2 - 2) / iv2) continue; }
if (age < (e2.z >> 16)) continue; // delay_frames (summon_portal.xml's burst comes at frame 118)
const int c0 = e2.z & 0xFFFF;
int n = c0 + int(rnd01(hash3(seed, uint(e), 51u)) * float(e2.w - c0 + 1));
for (int k = 0; k < n && k < 64; ++k) { // (the giga black hole's emitters make 40 - 60 a frame)
float tt = (e0.y & 1) != 0 ? rnd01(hash3(seed, uint(e * 64 + k), 52u)) : 1.0;
vec2 ep = mix(start, pos, tt);
ep += (vec2(rnd01(hash3(seed, uint(e * 64 + k), 59u)), rnd01(hash3(seed, uint(e * 64 + k), 60u))) - 0.5) * 2.0 * e3.zw; // x/y_pos_offset ranges
{ // area_circle_radius (00bc75e0): a point of the disc, radius sqrt(u) between min and max (quarter pixels in bits 8-19 / 20-31 of the flags)
const float rmax = float((uint(e0.y) >> 8) & 0xFFFu) * 0.25, rmin = float((uint(e0.y) >> 20) & 0xFFFu) * 0.25;
if (rmax > 0.0) { const float rr = sqrt(rnd01(hash3(seed, uint(e * 64 + k), 61u))) * (rmax - rmin) + rmin, aa = rnd01(hash3(seed, uint(e * 64 + k), 62u)) * 6.2831853; ep += vec2(cos(aa), sin(aa)) * rr; }
}
vec2 ev = vec2(mix(e1.x, e1.y, rnd01(hash3(seed, uint(e * 64 + k), 53u))), mix(e1.z, e1.w, rnd01(hash3(seed, uint(e * 64 + k), 54u))));
ev += (vec2(rnd01(hash3(seed, uint(k), 55u)), rnd01(hash3(seed, uint(k), 56u))) - 0.5) * 2.0 * e3.x;
if (away != 0.0) { const vec2 dd = ep - pos; const float dl = length(dd); if (dl > 1e-4) ev += dd / dl * away; } // (00bc75e0: along the way out from the emitter)
float life = max(2.0, mix(e0f.z, e0f.w, rnd01(hash3(seed, uint(e * 64 + k), 57u))));
if ((e0.y & 4) != 0) { // create_real_particles (00bc75e0 route +0x60): the material is created in the grid at the position (velocity and lifetime are ignored) through the cell
// factory: into an empty cell, or over a gas or liquid one unless emit_only_if_there_is_space (+0x67, flag 64)
const ivec2 q = ivec2(floor(ep)) - ivec2(pc.view_x, pc.view_y);
const int mat = int(ft[eb + 3].y);
if (mat > 0 && q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
const int ci = q.y * pc.gw + q.x;
const uint cur = cells[ci], cm = cell_mat(cur);
const bool free = cm == 0u || ((e0.y & 64) == 0 && (mflags(cm) & (F_GAS | F_LIQCELL)) != 0u && (mflags(cm) & F_STATIC) == 0u);
if (free && atomicCompSwap(cells[ci], cur, uint(mat) | (((hash3(seed, uint(e * 8 + k), 58u) >> 16) & 0xFFu) << 16)) == cur) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
}
}
else if ((e0.y & 16) == 0 || cell_free_at(ep)) { // (bit 3: collide_with_grid 0, bit 4: cosmetic_force_create 0, bit 5: a material that does not glow - blended with its alpha)
const bool blended = (e0.y & 32) != 0;
const int ps = emit_particle_g(ep, ev, life, uf(ft[eb + 2].x), uint(e0.x) & 0xFFFFFFu, (e0.y & 2) != 0, !blended, (e0.y & 8) == 0);
if (ps >= 0 && blended) fparts[ps].c.z = 255u - ((uint(e0.x) >> 24) & 0xFFu);
if (ps >= 0 && (attr != 0u || (e0.y & 128) != 0)) fparts[ps].c.z |= (id & 0x7FFu) << 8 | attr << 19 | ((e0.y & 128) != 0 ? 0x8000000u : 0u); // the attractor (this projectile, its force) and draw_as_long, for fx_particles.comp
}
}
}
if (t4.w > 0.0) add_light(pos, min(t4.w, 36.0) * 0.55, 0.32, t4.xyz);
draw_sprite(pos, vel, age, t1, t5, t6, spin, (t1.w & TF_PHYSICS) == 0 && spin < 0.0 ? -spin : 1.0);
}
if ((t1.w & TF_PHYSICS) != 0) { if (length(vel) < 0.01) omega *= 0.8; spin += omega; omega *= 0.99; }
p.e = vec4(spin, omega, p.e.z, p.e.w); // (the shooter slot + 1 and its herd stay with the projectile)
if (alive && !inert) vel *= bm_drag(pos); // 00c06c60: after the move the velocity is multiplied by the biome's projectile_drag_coeff (MOIST 0.965)
p.a = vec4(pos, vel);
p.c.z = float(age); p.c.w = float(bounces);
if (!alive && (fti(tb + 11).y & 1) != 0) { uint xi = uint(atomicAdd(xhead.x, 2)); if (xi + 1u < 512u) { xe[xi] = vec4(pos, -5.0, float(id)); xe[xi + 1u] = vec4(float(p.d.w), 0.0, 0.0, 0.0); } } // the twin's projectile died (event kind -5)
p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF) | (p.d.z & 0x400) | (stuck ? 0x800 : 0); // bit 11: stuck without its ProjectileComponent (on_collision_remove_projectile); bits 12-23 crit, 24-27 lightning_count, 28 bounce at any angle stay
p.d.w = alive ? p.d.w : 0;
fprojs[id] = p;
}
+75
View File
@@ -0,0 +1,75 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "common.glsl"
// Projectiles shot by creatures: move in one-cell steps, die in solid cells or when they hit the player, drawn as a 3x3 blob into the entity layer.
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
struct Proj { vec4 a; vec4 b; vec4 c; }; // pos, vel / damage, life, gravity, colour bits / owner (0 creature, 1 player)
layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; };
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 25) buffer Projs { Proj projs[]; };
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; } ctrl;
bool solid_at(ivec2 w) {
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) return true;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
}
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= 512u) return;
Proj p = projs[id];
if (p.b.y <= 0.0) return;
if (pc.pass_ == 1) { // between ticks: only draw
ivec2 w0 = ivec2(floor(p.a.xy)) - ivec2(pc.view_x, pc.view_y);
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
ivec2 q = w0 + ivec2(x, y);
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = floatBitsToUint(p.b.w);
}
return;
}
vec2 pos = p.a.xy, vel = p.a.zw;
vel.y += p.b.z;
int n = max(int(ceil(length(vel))), 1);
bool dead = false;
vec4 pl = ents[0].a, plbox = ents[0].box;
bool player_alive = (ents[0].st.w & 8) == 0 && ents[0].st.x == 3;
for (int i = 0; i < n && !dead; ++i) {
pos += vel / float(n);
if (solid_at(ivec2(floor(pos)))) { dead = true; break; }
if (p.c.x > 0.5) { // the player's bolt: hits the first living creature whose box contains it
for (int k = 1; k < ctrl.inp.w; ++k) {
if (ents[k].st.x == 0 || ents[k].st.x == 3 || (ents[k].st.w & 8) != 0) continue;
vec2 dk = pos - ents[k].a.xy;
if (dk.x >= ents[k].box.x - 1.0 && dk.x <= ents[k].box.y + 1.0 && dk.y >= ents[k].box.z - 1.0 && dk.y <= ents[k].box.w + 1.0) {
atomicAdd(ents[k].x.x, int(p.b.x * ents[k].ex9.x * 1000.0));
dead = true;
break;
}
}
if (dead) break;
continue;
}
vec2 d = pos - pl.xy;
if (player_alive && d.x >= plbox.x - 1.0 && d.x <= plbox.y + 1.0 && d.y >= plbox.z - 1.0 && d.y <= plbox.w + 1.0) {
atomicAdd(ents[0].x.x, int(p.b.x * ents[0].ex9.x * 1000.0));
dead = true;
}
}
float life = dead ? 0.0 : p.b.y - 1.0;
projs[id].a = vec4(pos, vel);
projs[id].b = vec4(p.b.x, life, p.b.z, p.b.w);
if (life > 0.0) { // draw
uint col = floatBitsToUint(p.b.w);
ivec2 w = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
ivec2 q = w + ivec2(x, y);
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = col;
}
}
}
+127
View File
@@ -0,0 +1,127 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
// Material reactions (liquids mixing, acid, water on lava, ...). The grid is cut into disjoint pairs of neighbouring cells; pc.pass_
// (0..3) selects the pairing: 0/1 horizontal pairs starting at even/odd columns, 2/3 vertical pairs. Both cells of a pair evaluate the
// same reaction (symmetric hash) and each applies its own side, so no cell is written twice. Four passes visit every adjacency once per frame.
layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; }; // [a * n + b] = (offset << 8) | count
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]; ivec4 reactN; ivec4 reactEv[64]; } ctrl; // reactEv: entities to spawn (x, y, entity index + 1)
layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; }; // 5 words per entry: outs (a | b << 16, 0xFFFF = same), probability / 65536
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= pc.gw || p.y >= pc.gh) return;
int ph = pc.pass_ & 3;
ivec2 q;
if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0);
else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
int i = p.y * pc.gw + p.x, j = q.y * pc.gw + q.x;
uint ci = cells[i], cj = cells[j];
uint mi = cell_mat(ci), mj = cell_mat(cj);
if (mi == 0u && mj == 0u) return;
uint n = mats[mats.length() - 1u].w;
uint h = rhead[mi * n + mj];
uint cnt = h & 255u;
if (cnt == 0u) return;
uint lo = uint(min(i, j)), hi = uint(max(i, j));
uint rr = hash3(lo, hi, uint(pc.frame * 4 + ph) ^ 0x51u);
uint k = 0u; // the record of the pair table: R % count with the roll R that also gates the attempt (0070eaf0)
// 0070eaf0: every cell that has a reaction threshold (CellData + 0x22c: the largest probability of a reaction it is the first input of; air is no cell and never starts one) tries once per
// update with ONE random neighbour of four, rolls R (0..10000) against min(that threshold, the record's probability) and the record must accept the direction: the pair gets the chance of
// each of its two cells separately (rec / 4 for a material next to air, not rec / 2).
{
const bool iLo = i < j;
const uint mA = iLo ? mi : mj, mB = iLo ? mj : mi;
const ivec2 pA = iLo ? p : q, pB = iLo ? q : p;
const uint hA = rhead[mA * n + mB], hB = rhead[mB * n + mA];
const uint eA = ((hA >> 8) + k % max(hA & 255u, 1u)) * 5u, eB = ((hB >> 8) + k % max(hB & 255u, 1u)) * 5u;
const uint pwA = rrec[eA + 1u], pwB = rrec[eB + 1u];
const ivec2 dAB = pB - pA;
const uint relA = dAB.y < 0 ? 1u : dAB.y > 0 ? 2u : dAB.x < 0 ? 3u : 4u, relB = relA == 1u ? 2u : relA == 2u ? 1u : relA == 3u ? 4u : 3u;
const uint dirA = (pwA >> 16) & 7u, dirB = (pwB >> 16) & 7u;
// each cell has its own roll R = next15 % 10001: the material threshold (CellData + 0x22c, positive) and the record's threshold both accept R <= threshold, the record is R % count
const uint RA = (hash(rr ^ 0xA2u) & 0x7FFFu) % 10001u, RB = (hash(rr ^ 0xB2u) & 0x7FFFu) % 10001u;
const uint kA = RA % max(hA & 255u, 1u), kB = RB % max(hB & 255u, 1u);
const uint eA2 = ((hA >> 8) + kA) * 5u, eB2 = ((hB >> 8) + kB) * 5u;
const uint matA = rhead[n * n + mA], matB = rhead[n * n + mB];
const bool fireA = mA != 0u && (hA & 255u) != 0u && matA > 0u && RA <= matA && RA <= (rrec[eA2 + 1u] & 0xFFFFu) && (((rrec[eA2 + 1u] >> 16) & 7u) == 0u || ((rrec[eA2 + 1u] >> 16) & 7u) == relA) && (hash(rr ^ 0xA1u) & 3u) == 0u;
const bool fireB = mB != 0u && (hB & 255u) != 0u && matB > 0u && RB <= matB && RB <= (rrec[eB2 + 1u] & 0xFFFFu) && (((rrec[eB2 + 1u] >> 16) & 7u) == 0u || ((rrec[eB2 + 1u] >> 16) & 7u) == relB) && (hash(rr ^ 0xB1u) & 3u) == 0u;
if (!fireA && !fireB) return;
k = fireA ? kA : kB;
}
uint e = ((h >> 8) + k % cnt) * 5u;
uint outs = rrec[e];
uint third = rrec[e + 2u];
uint blob = rrec[e + 3u];
uint entityIdx = rrec[e + 4u] & 0x7FFFFFFFu;
bool second = (mi == mj) && (i > j); // equal materials: the lower cell takes the first output
uint o = second ? (outs >> 16) : (outs & 0xFFFFu);
const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u; // (the record is the mirrored one when the first input is the second material of the pair)
const uint reqLife = blob >> 20;
if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return; // req_lifetime: the first cell must be old enough
if (((blob >> 19) & 1u) != 0u && firstIsI0 && !second) { // destroy_horizontally_lonely_pixels: the first output becomes air when both horizontal neighbours are empty (both probed)
const bool le = p.x > 0 && cell_mat(cells[i - 1]) == 0u, ri = p.x + 1 < pc.gw && cell_mat(cells[i + 1]) == 0u;
if (le && ri) o = 0u;
}
// three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side)
// must hold the third material; the thread of the first cell writes its output
if ((third & 0xFFFFu) != 0xFFFFu) {
const bool firstIsI = firstIsI0;
const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p;
const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
ivec2 p3 = pf + dirs[(rr >> 20) & 3u];
if (p3 == ps) p3 = 2 * pf - ps;
if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return;
const int k3 = p3.y * pc.gw + p3.x;
const uint c3 = cells[k3];
if (cell_mat(c3) != (third & 0xFFFFu)) return;
const uint o3 = (third >> 16) & 0xFFFFu;
if (firstIsI && o3 != 0xFFFFu && o3 != cell_mat(c3)) { atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)); birth[k3] = birth_frame(); }
}
if (o == 0xFFFFu || o == mi) return; // this side stays as it is
cells[i] = o | (ci & 0x00FF0000u); // keeps the colour variation, clears burning and direction bits
ext[i] = vec2(0.0);
birth[i] = birth_frame();
if (entityIdx != 0u && firstIsI0 && !second) { // the reaction loads an entity at the position of the first input (the host spawns it)
const int ei = atomicAdd(ctrl.reactN.x, 1);
if (ei < 64) ctrl.reactEv[ei] = ivec4(p.x + pc.view_x, p.y + pc.view_y, int(entityIdx), 0);
else atomicAdd(ctrl.reactN.x, -1);
}
// blob_radius with blob_restrict_to_input_material (0071ab50 conditional mode): trunc(r * 2 pi + 0.5) rays of unit steps from the cell; a cell of the input material converts,
// one that already holds the output lets the ray go on, anything else ends it (a flood of the connected input material up to the radius)
{
const uint rad = second ? ((blob >> 8) & 255u) : (blob & 255u);
const bool restricted = ((blob >> (second ? 17 : 16)) & 1u) != 0u;
if (rad > 0u && restricted) {
const int nr = int(float(rad) * 6.2831853 + 0.5);
for (int ray = 0; ray < nr; ++ray) {
const float a = float(ray) * 6.2831853 / float(nr);
const vec2 dir = vec2(cos(a), sin(a));
for (int st = 1; st <= int(rad); ++st) {
const ivec2 t = p + ivec2(trunc(dir * float(st)));
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) break;
const int ti = t.y * pc.gw + t.x;
const uint tc = cells[ti], tm = cell_mat(tc);
if (tm == o) continue;
if (tm != mi) break;
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } // a converted cell starts afresh (0070d0b0)
}
}
} else if (rad > 0u) { // direct mode (0071ab50, restriction explicitly off): every loaded cell of the disk dx^2 + dy^2 < r^2 takes the output, whatever it holds
const int ri = int(rad);
for (int dy = -ri; dy <= ri; ++dy)
for (int dx = -ri; dx <= ri; ++dx) {
if (dx * dx + dy * dy >= ri * ri) continue;
const ivec2 t = p + ivec2(dx, dy);
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) continue;
const int ti = t.y * pc.gw + t.x;
const uint tc = cells[ti];
if (cell_mat(tc) == o) continue;
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); }
}
}
}
}
+457
View File
@@ -0,0 +1,457 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
#include "areas.glsl"
vec4 unpack_argb(uint c) {
return vec4(float((c >> 16) & 0xFFu), float((c >> 8) & 0xFFu), float(c & 0xFFu), float(c >> 24)) / 255.0;
}
layout(std430, set = 0, binding = 13) readonly buffer TexData { uint texdata[]; }; // ARGB texels of all material textures
layout(std430, set = 0, binding = 14) readonly buffer TexInfo { uvec4 texinfo[]; }; // per material: offset, width, height, 0 = none
layout(std430, set = 0, binding = 12) readonly buffer SceneMat { uint scene_mat[]; }; // pixel scenes: material + 1
layout(std430, set = 0, binding = 15) readonly buffer BgTile { uint bgtile[]; }; // biome background tile (ARGB)
layout(std430, set = 0, binding = 16) readonly buffer SceneVis { uint scene_vis[]; }; // scene visual colours (ARGB)
layout(std430, set = 0, binding = 17) readonly buffer SceneBg { uint scene_bg[]; }; // scene backgrounds (ARGB)
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint perm[]; }; // [0,512) simplex: the gradient of the procedural biomes decides where the background wall is
int ffloor(float v) { return int(floor(v)); }
uint ps(uint i) { return perm[i]; }
#include "procgrad.glsl"
layout(std430, set = 0, binding = 21) readonly buffer EntLayer { uint entlayer[]; };
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // positions before the last tick // entity sprites drawn this frame (ARGB per window cell)
layout(std430, set = 0, binding = 26) readonly buffer Overlay { uint overlay[]; }; // CPU-drawn HUD (ARGB, output pixels)
struct FLight { vec4 a; vec4 b; }; // x, y, radius (world cells), strength / rgb
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
layout(std430, set = 0, binding = 38) readonly buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; }; // lights of projectiles and explosions this frame
layout(std430, set = 0, binding = 44) readonly buffer Sky { uint sky[]; }; // src/world/sky.h
layout(std430, set = 0, binding = 47) readonly buffer GlowA { vec4 glowA[]; }; // the blurred glow of the materials at half resolution (glow.comp)
layout(std430, set = 0, binding = 49) readonly buffer Fog { uint fog[]; };
layout(std430, set = 0, binding = 50) readonly buffer Vis { vec4 fogvis[]; }; // line-of-sight visibility per 4x4 block of the window (fog.comp) // the explored map, one byte per 8x8 world cells (fog.comp)
layout(std430, set = 0, binding = 57) readonly buffer LitCells { uvec2 litc[]; }; // the light of the entities per cell (light_cells.comp sums lent once per cell)
layout(std430, set = 0, binding = 45) readonly buffer Lit { uvec4 lhead; vec4 lent[]; }; // the lights near the window: count, then (x, y, radius, strength), (r, g, b, 0) per light
uvec4 sky_img(int i) { return uvec4(sky[4 + 3 * i], sky[5 + 3 * i], sky[6 + 3 * i], 0u); }
vec4 sky_texel(int i, int x, int y) { // ARGB texel of image i (wrapping in x, clamped in y; transparent outside)
uvec4 h = sky_img(i);
if (y < 0 || y >= int(h.z)) return vec4(0.0);
x = ((x % int(h.y)) + int(h.y)) % int(h.y);
return unpack_argb(sky[h.x + uint(y) * h.y + uint(x)]);
}
vec3 sky_table(float tod, int row) { // parallax_colors.bmp: the colour of a row over the day (x = time of day)
return sky_texel(0, int(fract(tod) * 511.0), row).rgb;
}
// The sky behind the terrain (the parallax of 00888c80 / 008897e0 / 00889cb0, docs/BACKLOG.md 22): the colour of layer k is row 20 k of parallax_colors.bmp at
// x = time of day (lerped); the images are masks tinted by it. Layer 0 (weather_background, white) fills the screen with row 0; 1 (parallax_background x row
// 20, alpha gradient_sky_alpha) and 2 (parallax_sunset_line x row 40, alpha the sunset alpha) are repeated from the top of the screen; then in the order 3, 10, 7,
// 4, 9, 6, 5, 8 the image of layer k repeats horizontally from x = -(camera x / depth) + rec.x, its top at y = camera y / depth - rec.y - parallax_offset_y
// (virtual pixels from the top of the screen, at least screen height - image height), tinted by its row and its alpha. Depths 50, 30, 25, 25, 40, 20, 35, 45
// (layers 3..10); rec.x / rec.y / alphas are the host's (sky words 48..: cloud 1 x, cloud 1 y, cloud 2 y, mountains 1 y, mountains 2 y, storm y, storm alpha,
// gradient alpha, sunset alpha, parallax_offset_y). (The rain particles, layer 11, are the GameEmitRainParticles of init.lua.)
vec4 sky_tex(int i, vec2 t) { // image i at texel t, repeating in x, transparent outside in y
const uvec4 h = sky_img(i);
if (h.y == 0u || t.y < 0.0 || t.y >= float(h.z)) return vec4(0.0);
const int x = ((int(floor(t.x)) % int(h.y)) + int(h.y)) % int(h.y);
return unpack_argb(sky[h.x + uint(int(t.y)) * h.y + uint(x)]);
}
vec4 sky_tex_wrap(int i, vec2 t) { // repeating in both directions
const uvec4 h = sky_img(i);
if (h.y == 0u || h.z == 0u) return vec4(0.0);
return sky_tex(i, vec2(t.x, float(((int(floor(t.y)) % int(h.z)) + int(h.z)) % int(h.z))));
}
vec3 sky_row(float tod, int row) { // 008897e0: index (w - 1) x time, lerped to the next column
const uvec4 h = sky_img(0);
const float f = (float(h.y) - 1.0) * fract(tod);
const int i0 = int(floor(f));
const vec3 a = sky_texel(0, i0, row).rgb, b = sky_texel(0, min(i0 + 1, int(h.y) - 1), row).rgb;
return mix(a, b, f - float(i0));
}
float skp(int k) { return uintBitsToFloat(sky[48u + uint(k)]); }
// TODO(to-do #18): layer 0 shader variant, the biome source of gradient / sunset alpha, the camera origin
vec3 sky_colour(vec2 w, vec2 cam, float tod, float tsec, vec2 scr) {
const vec2 camTL = cam - vec2(213.5, 121.0); // (the virtual screen: 427 x 242 virtual pixels, 1 per world pixel)
const vec2 sv = w - camTL;
vec3 col = sky_row(tod, 0);
{ const float ga = skp(7);
if (ga > 0.0) { const vec4 c = sky_tex_wrap(1, sv); col = mix(col, c.rgb * sky_row(tod, 20), c.a * ga); } }
{ const float sa = skp(8);
if (sa > 0.0) { const vec4 c = sky_tex_wrap(2, sv); col = mix(col, c.rgb * sky_row(tod, 40), c.a * sa); } }
const int order[8] = int[8](3, 10, 7, 4, 9, 6, 5, 8);
const float depth[11] = float[11](1.0, 1.0, 1.0, 50.0, 30.0, 25.0, 25.0, 40.0, 20.0, 35.0, 45.0);
for (int o = 0; o < 8; ++o) {
const int k = order[o];
float rx = 0.0, ry = 0.0, a = 1.0;
if (k == 3) { rx = skp(0); ry = skp(1); }
else if (k == 4) ry = skp(2);
else if (k == 5 || k == 6) ry = skp(3);
else if (k == 7) ry = skp(4);
else { ry = skp(5); a = skp(6); }
if (a <= 0.0) continue;
const uvec4 h = sky_img(k);
const float top = max(camTL.y / depth[k] - ry - skp(9), 242.0 - float(h.z));
const vec4 c = sky_tex(k, vec2(sv.x - camTL.x / depth[k] + rx, sv.y - top));
if (c.a > 0.0) col = mix(col, c.rgb * sky_row(tod, 20 * k), c.a * a);
}
return col;
}
// Lighting of the foreground (cells and sprites), after post_final.frag: the lights of the entities (LightComponent, cubic falloff), the glow of the materials, the light of
// the sky (the sky exposure of the cell: open sky, the depth below the first solid cell, the sides of an opening; times the colour of the time of day), gamma, then the
// fog of war (unexplored cells are black unless the sky lights them) and the baseline 0.35 - sky ambient of the explored map. The background (sky, cave walls) is not lit.
// sky[3]: bit 0 lighting on (F4), bit 2 fog of war off (F6), bit 3 glow off (F7).
const int FOG_TW = 1152, FOG_TH = 896, FOG_OX = 18432, FOG_OY = 14336; // the fog map of fog.comp: 32 x 32 pixel texels from the world origin, the clear amount (255 - fog)
float fog_clear(ivec2 t) {
if (t.x < 0 || t.y < 0 || t.x >= FOG_TW || t.y >= FOG_TH) return 0.0;
return float(min(fog[t.y * FOG_TW + t.x], 255u)) / 255.0;
}
vec2 gCpf = vec2(0.0); // window position of the pixel in cells, at sub-cell precision
float bm_dust_r(vec2 w) { // the biome modifier dust_amount of the map tile under w (biome_mods.glsl's table, read through perm)
const uint n = mats[mats.length() - 1u].w;
const uint fb = 1024u + 9u * n + 132u + n;
const int bw = int(perm[fb]), bh = int(perm[fb + 1u]);
const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14);
if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return 0.0;
return uintBitsToFloat(perm[fb + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u + 4u * uint(t.y * bw + t.x) + 3u]);
}
float perlin_tex(vec2 uv) { // tex_perlin_noise (data/shaders/noise_perlin.png, sky image 11): bilinear, repeating; uv in texture units (1 = the image)
const uvec4 h = sky_img(11);
if (h.y == 0u || h.z == 0u) return 0.5;
const vec2 q = uv * vec2(float(h.y), float(h.z)) - 0.5;
const ivec2 e = ivec2(floor(q));
const vec2 g = q - vec2(e);
const int W = int(h.y), H = int(h.z);
float t[4];
for (int k = 0; k < 4; ++k) {
const int x = (((e.x + (k & 1)) % W) + W) % W, y = (((e.y + (k >> 1)) % H) + H) % H;
t[k] = float((sky[h.x + uint(y) * h.y + uint(x)] >> 16) & 255u) / 255.0;
}
return mix(mix(t[0], t[1], g.x), mix(t[2], t[3], g.x), g.y);
}
float sky_cell(ivec2 b) { // one block of the exposure grid (sky words: [7] = its offset, then the first block column / row, then the values)
const uint o = sky[60];
const ivec2 b0 = ivec2(int(sky[o]), int(sky[o + 1u]));
const ivec2 q = clamp(b - b0, ivec2(0), ivec2(1024 / 64 + 1, 576 / 64 + 1));
return uintBitsToFloat(sky[o + 2u + uint(q.y * (1024 / 64 + 2) + q.x)]);
}
float sky_bilinear(vec2 w) { // texel centres at the block centres
const vec2 q = w / 64.0 - 0.5;
const ivec2 e = ivec2(floor(q));
const vec2 g = q - vec2(e);
return mix(mix(sky_cell(e), sky_cell(e + ivec2(1, 0)), g.x), mix(sky_cell(e + ivec2(0, 1)), sky_cell(e + ivec2(1, 1)), g.x), g.y);
}
float sky_amount(vec2 w) { // post_final.frag HIQ: (centre + 0.25 x the four neighbours) x 0.5
return (sky_bilinear(w) + 0.25 * (sky_bilinear(w - vec2(64.0, 0.0)) + sky_bilinear(w + vec2(64.0, 0.0)) + sky_bilinear(w - vec2(0.0, 64.0)) + sky_bilinear(w + vec2(0.0, 64.0)))) * 0.5;
}
float fog_bilinear(vec2 w) { // the fog map between the texel centres (32 n + 16), as a linear-filtered tex_fog
const vec2 q = w / 32.0 - 0.5;
const ivec2 e = ivec2(floor(q));
const vec2 g = q - vec2(e);
return mix(mix(fog_clear(e), fog_clear(e + ivec2(1, 0)), g.x), mix(fog_clear(e + ivec2(0, 1)), fog_clear(e + ivec2(1, 1)), g.x), g.y);
}
float explored(ivec2 cp, ivec2 wp) { // 0 fog .. 1 clear: the fog map read bilinearly between the texel centres (32 n + 16), as post_final.frag samples tex_fog
if ((sky[3] & 4u) != 0u) return 1.0;
const vec2 ww = gCpf + vec2(float(pc.view_x), float(pc.view_y));
const vec2 w = ww + vec2(FOG_OX, FOG_OY);
// post_final.frag HIQ: the centre plus four samples at +-0.25 texel diagonally and four at 0.75 texel on the axes, / 9 (tex_fog: FOG_TEX_SIZE 64 at zoom 1, 32 px texels)
const float clr = (fog_bilinear(w) + fog_bilinear(w + vec2(8.0, -8.0)) + fog_bilinear(w + vec2(-8.0, -8.0)) + fog_bilinear(w + vec2(-8.0, 8.0)) + fog_bilinear(w + vec2(8.0, 8.0))
+ fog_bilinear(w - vec2(24.0, 0.0)) + fog_bilinear(w + vec2(24.0, 0.0)) + fog_bilinear(w - vec2(0.0, 24.0)) + fog_bilinear(w + vec2(0.0, 24.0))) * 0.1111111;
// the holes of the frame (main.cpp: fog_of_war_hole sprites): post_final.frag fog x (1 - the hole alpha); the disc of explosion_flare_fog_of_war_hole.png ~ exp(-4.5 t^2)
const uint n = mats[mats.length() - 1u].w;
const uint hb = 1024u + 9u * n + 132u + n + 2u + 8192u;
const int nh = min(int(uintBitsToFloat(perm[hb])), 64);
const uint ib = hb + 1u + 64u * 6u + 1u + 16u * 3u;
float hole = 0.0;
for (int k = 0; k < nh; ++k) { // the hole sprite's alpha (its animation frame by the age), bilinear (smooth_filtering), scaled about its offset
const uint o = hb + 1u + uint(k) * 6u;
const vec2 hp = vec2(uintBitsToFloat(perm[o]), uintBitsToFloat(perm[o + 1u]));
const float sc = max(uintBitsToFloat(perm[o + 2u]), 0.01);
const int img = int(uintBitsToFloat(perm[o + 4u]));
float a = 0.0;
if (img >= 0 && img < 8) {
const uint ia = ib + uint(img) * 3272u;
const float iw = uintBitsToFloat(perm[ia]), ih = uintBitsToFloat(perm[ia + 1u]);
const vec2 sp = (ww - hp) / sc + vec2(uintBitsToFloat(perm[ia + 2u]), uintBitsToFloat(perm[ia + 3u])) - 0.5;
if (sp.x < -1.0 || sp.y < -1.0 || sp.x > iw || sp.y > ih) continue;
const uint nf = max(perm[ia + 4u] & 255u, 1u), wt = max(perm[ia + 4u] >> 8, 1u);
const uint fr = (uint(uintBitsToFloat(perm[o + 5u])) / wt) % nf;
const ivec2 s0 = ivec2(floor(sp));
const vec2 f = sp - vec2(s0);
for (int q = 0; q < 4; ++q) {
const ivec2 s = s0 + ivec2(q & 1, q >> 1);
if (s.x < 0 || s.y < 0 || s.x >= int(iw) || s.y >= int(ih)) continue;
a += float(perm[ia + 5u + fr * 1089u + uint(s.y * int(iw) + s.x)]) / 255.0 * ((q & 1) != 0 ? f.x : 1.0 - f.x) * ((q >> 1) != 0 ? f.y : 1.0 - f.y);
}
} else { // (no image: a soft disc of 16 px x the scale)
const float t = distance(ww, hp) / (16.0 * sc);
a = t < 1.0 ? exp(-4.5 * t * t) : 0.0;
}
hole = max(hole, uintBitsToFloat(perm[o + 3u]) * a);
}
return 1.0 - (1.0 - clr) * (1.0 - min(hole, 1.0));
}
#define FPAL_TABLE perm
// ---- the fire palettes of the cell blitting (main.cpp loads them after the tables; 00722490 the lookup, 00724580 the use) ----
uint fpal_word(uint k) { return FPAL_TABLE[1024u + 9u * mats[mats.length() - 1u].w + k]; }
vec3 fpal_rgb(uint w) { return vec3(float((w >> 16) & 255u), float((w >> 8) & 255u), float(w & 255u)) / 255.0; }
vec3 fire_lookup(uint at, uint row, uint w, float t) { // the row of the image at t x width: the first / last pixel outside, else the two neighbours mixed
const float x = float(w) * t;
const uint r0 = at + row * w;
if (x >= float(w)) return fpal_rgb(fpal_word(r0 + w - 1u));
if (x <= 0.0) return fpal_rgb(fpal_word(r0));
const uint i0 = uint(floor(x));
if (float(i0) == x || i0 + 1u >= w) return fpal_rgb(fpal_word(r0 + i0));
return mix(fpal_rgb(fpal_word(r0 + i0)), fpal_rgb(fpal_word(r0 + i0 + 1u)), x - float(i0));
}
uint fire_row(uint m) { return min(fpal_word(132u + m), 5u); } // CellData +0x68 fire_colors_index
float fire_age(uint b) { return float(birth_age(b)) + float(hash3(b & BIRTH_MASK, 77u, 5u) % 21u); } // 00709ef0: the flame's frame is the creation frame - uniform(0, 20)
vec3 fire_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(0u, fire_row(m), 8u, a * a); } // colors_fire.png at (age / 40)^2
vec3 fire_glow_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(48u, fire_row(m), 8u, fire_age(b) < 15.0 ? a : a * a); } // colors_fire_glow.png (young: age / 40)
vec3 burning_colour(uint m, uint h) { return fpal_rgb(fpal_word(96u + fire_row(m) * 6u + h % 6u)); } // colors_lava.bmp: a random pixel of the row
vec3 glow_at(ivec2 cp) { // bilinear in the half-resolution glow
if ((sky[3] & 8u) != 0u) return vec3(0.0);
const int hw = (pc.gw + 1) / 2, hh = (pc.gh + 1) / 2;
const vec2 q = (vec2(cp) + 0.5) * 0.5 - 0.5;
const ivec2 q0 = ivec2(floor(q));
const vec2 f = q - vec2(q0);
vec3 r = vec3(0.0);
for (int k = 0; k < 4; ++k) {
const ivec2 s = clamp(q0 + ivec2(k & 1, k >> 1), ivec2(0), ivec2(hw - 1, hh - 1));
r += glowA[s.y * hw + s.x].rgb * (((k & 1) != 0 ? f.x : 1.0 - f.x) * ((k >> 1) != 0 ? f.y : 1.0 - f.y));
}
return max(vec3(0.0), r - 0.008); // (post_final.frag)
}
float gFog = 1.0;
float gSky = 0.0, gLum = 0.0; // (the squared sky amount and the luminosity of the lights of the last lighting() call: the dust)
vec3 gGlow = vec3(0.0);
vec3 lighting(ivec2 cp, ivec2 wp, uint emissive) {
gFog = 1.0; gGlow = vec3(0.0); gSky = 1.0; gLum = 1.0;
if ((sky[3] & 1u) == 0u) return vec3(1.0);
gGlow = glow_at(cp);
if (emissive != 0u) { gFog = explored(cp, wp); return vec3(1.0); }
const uvec2 lc = litc[cp.y * pc.gw + cp.x]; // (the sum over the lights near the window, light_cells.comp: a cell's light is the same for all its pixels)
vec3 L = vec3(unpackHalf2x16(lc.x), unpackHalf2x16(lc.y).x);
// post_final.frag: lights = pow(tex_lights x 0.8, 1.5) + glow; the sky: sky_light_color x sky_ambient_amount, the exposure grid (tex_skylight, 64-pixel blocks of the
// coarse map, main.cpp) read bilinearly with the HIQ blur (0.5 centre + 0.125 x the four neighbours one block away); lights = max(lights, sky), at most 1, gamma 1 / 2.2;
// then the fog of war: s = the sky amount faded out between y 250 and 350, fog = min(1, max(2 x 1.4 (0.6, 0.5, 0.45) max(0, 1 - fog - sqrt(s)), s)),
// lights x fog + max(0.35 - s, 0) x fog
vec3 lights = pow(max(L * 0.8, vec3(0.0)), vec3(1.5)) + gGlow;
float amount = sky_amount(vec2(wp) + 0.5);
amount *= amount; // (post_final.frag: sky_ambient_amount *= sky_ambient_amount after the sample)
const float tod = uintBitsToFloat(sky[0]);
const vec3 topc = sky_table(tod, 4);
const float f = smoothstep(0.12, 0.5, dot(topc, vec3(0.299, 0.587, 0.114)));
// TODO(to-do #17): sky_light_color from the PostFx keyframes (00890330)
const vec3 skyc = mix(vec3(0.12, 0.16, 0.30), vec3(1.0), f); // (sky_light_color: the engine's day keyframes are not traced yet; this follows the sky's brightness)
lights = min(max(lights, skyc * amount), vec3(1.0));
lights = pow(lights, vec3(1.0 / 2.2));
const float vis = explored(cp, wp);
const float s = amount * (1.0 - clamp((float(wp.y) - 250.0) / 100.0, 0.0, 1.0));
vec3 fog = 1.4 * vec3(0.6, 0.5, 0.45) * max(0.0, 1.0 - (1.0 - vis) - sqrt(s));
fog = min(vec3(1.0), max(2.0 * fog, vec3(s)));
L = lights * fog + max(0.35 - s, 0.0) * fog;
gSky = amount; gLum = sqrt(min(1.0, dot(L, vec3(0.299, 0.587, 0.114))));
gFog = max(fog.r, max(fog.g, fog.b));
return min(L, vec3(1.35));
}
// The picture is made at the game's own resolution, one pixel per world cell, then scaled up to the window (Noita draws its world at a fixed internal
// resolution: the window size changes only the final scaling). pass_ 1, the world pass: one thread per cell of the view, the colour of the cell (background,
// material, lights, fog of war, entity layer, projectile lights, grading, glow, blindness) into wcell. pass_ 0, the output pass: one thread per window pixel,
// the cell under it (camera shake and the camera between two ticks at sub-cell precision), the player at its sub-cell position, the low-health vignette, the HUD.
layout(std430, set = 0, binding = 60) writeonly buffer ClipRaw { uint clipraw[]; }; // the window picture without the HUD (the replay recorder)
layout(std430, set = 0, binding = 59) buffer WorldCells { uvec4 wcell[]; }; // per cell: the colour (rgba8), the light Lv (half r g, half b + the fog amount)
float gScreenY = 0.0;
vec3 world_cell(ivec2 cp, out vec3 Lv) {
uint c = cells[cp.y * pc.gw + cp.x];
float t = gScreenY; // (the screen row of the cell centre, for the background gradient)
vec3 bg = mix(vec3(0.05, 0.05, 0.09), vec3(0.02, 0.02, 0.04), t);
ivec2 wp0 = cp + ivec2(pc.view_x, pc.view_y); // world cell
const int bai = area_of_cell(wp0);
bool bgOk = true, bgDrawn = false;
{ // a procedural (surface) biome has no background wall: the sky and the surface caves of the hills show none
const uint trec = topo_rec(wp0);
if (trec != 0xFFFFFFFFu && (atab[trec] & 1u) != 0u) bgOk = false;
}
if (bgOk && bai >= 0 && arec(bai, 12) > 0) {
// the biome's background wall, tiled in world space
const int bw = arec(bai, 12), bh = arec(bai, 13);
uint tx = uint(((wp0.x % bw) + bw) % bw), ty = uint(((wp0.y % bh) + bh) % bh);
vec4 bc = unpack_argb(bgtile[uint(arec(bai, 11)) + ty * uint(bw) + tx]);
bg = mix(bg, bc.rgb, bc.a);
}
uint sidx = 0xFFFFFFFFu;
for (int a = 0; a < int(atab[0]); ++a) { // the first area whose scene has a material here; the first scene background
int si = scene_index(a, wp0);
if (si < 0) continue;
if (sidx == 0xFFFFFFFFu && scene_mat[si] != 0u) sidx = uint(si);
uint sb = scene_bg[si];
if (sb != 0u) { vec4 bc = unpack_argb(sb); bg = mix(bg, bc.rgb, bc.a); bgDrawn = bgDrawn || bc.a > 0.01; }
}
const uint trecSky = topo_rec(wp0);
bool skyPix = false;
if (sky[2] != 0u && trecSky != 0xFFFFFFFFu && (atab[trecSky] & 0x1000000u) != 0u && !bgDrawn && ((atab[trecSky] & 0x2000000u) == 0u || cp.y < int(sky[64 + cp.x])) && cell_mat(c) == 0u) { // (the scene biomes show the sky wherever no scene background is drawn; the procedural ones (hills) have caves with no wall behind: there the column must be open) // open sky: no wall behind, nothing solid above in the window
const vec2 cam = vec2(float(pc.view_x) + float(pc.gw) * 0.5, float(pc.view_y) + float(pc.gh) * 0.5);
skyPix = true;
bg = sky_colour(vec2(wp0), cam, uintBitsToFloat(sky[0]), uintBitsToFloat(sky[1]), vec2(0.0, float(wp0.y - pc.view_y) - float(pc.gh) * 0.5));
}
if ((sky[3] & 2u) != 0u && cell_mat(c) == 0u) { // F5: the sky mask: green = the sky is drawn, red = a solid cell above in the window column (the row of that cell is yellow), blue = not an outdoor biome, magenta = a scene background is drawn
const int top = int(sky[64 + cp.x]);
vec3 dbg = vec3(0.0);
if (!(trecSky != 0xFFFFFFFFu && (atab[trecSky] & 0x1000000u) != 0u)) dbg = vec3(0.0, 0.0, 0.8);
else if (bgDrawn) dbg = vec3(0.8, 0.0, 0.8);
else if (wp0.y >= 150) dbg = vec3(0.8, 0.8, 0.0);
else if ((atab[trecSky] & 0x2000000u) != 0u && cp.y >= top) dbg = vec3(0.8, 0.0, 0.0);
else dbg = vec3(0.0, 0.7, 0.0);
bg = mix(bg, dbg, 0.5);
}
{ // the weather fog (post_final.frag): the background mixes toward FOG_BACKGROUND 0.7 (FOG_BACKGROUND_NIGHT 0.2 by night_amount) by fog_amount_background =
// WorldStateComponent.fog (00c42d30 eases it toward max(fog_target, fog_target_extra); init.lua's weather sets fog_target_extra 0.3 .. 0.85 when it rains or snows)
const float wfog = uintBitsToFloat(sky[61]);
if (wfog > 0.0) {
const float tod = uintBitsToFloat(sky[0]);
const float night = clamp((0.55 - abs(fract(tod) - 0.57) - 0.4) * 10.0, 0.0, 1.0);
bg = mix(bg, vec3(mix(0.7, 0.2, night)), wfog);
}
}
if (sky[63] != 0u && !skyPix) bg = vec3(0.022, 0.02, 0.028); // the spell test room: a dark background
vec3 rgb = bg;
uint m = cell_mat(c);
gCpf = vec2(cp) + 0.5;
Lv = lighting(cp, wp0, m != 0u && ((mats[3u * m + 2u].z >> 17) & 1u) != 0u ? 1u : 0u); // fire and lava light themselves
rgb = skyPix ? bg : bg * gFog; // (the fog of war darkens the background too; the sky lights it)
if (m != 0u) {
vec4 col = unpack_argb(mats[3u * m].x);
float shade = 1.0 + (float((c >> 16) & 0xFFu) / 255.0 - 0.5) * 0.18; // per-cell variation
uvec4 ti = texinfo[m];
if (ti.y != 0u) { // the game's material texture, tiled in world space (one texel per cell)
ivec2 wp = cp + ivec2(pc.view_x, pc.view_y);
uint tx = uint(((wp.x % int(ti.y)) + int(ti.y)) % int(ti.y));
uint ty = uint(((wp.y % int(ti.z)) + int(ti.z)) % int(ti.z));
vec4 tc = unpack_argb(texdata[ti.x + ty * ti.y + tx]);
col = vec4(tc.rgb, col.a);
shade = 1.0;
}
if ((mflags(m) & F_FIRE) != 0u) { col = vec4(fire_colour(m, birth[cp.y * pc.gw + cp.x]), 1.0); shade = 1.0; } // a flame by its age (00724580: colors_fire.png, opaque)
if (sidx != 0xFFFFFFFFu && scene_mat[sidx] == m + 1u && scene_vis[sidx] != 0u) { // pixel-scene visual image // pixel-scene visual image
vec4 vc = unpack_argb(scene_vis[sidx]);
col = vec4(vc.rgb, col.a);
shade = 1.0;
}
if ((c & 0x80000000u) != 0u && (mflags(m) & F_STATIC) != 0u) col.rgb = mix(col.rgb, vec3(0.42, 0.03, 0.03), 0.6); // stained with blood (fx_particles.comp)
if ((c & (FROZEN_BIT | BURN_BIT)) == FROZEN_BIT && (mflags(m) & F_STATIC) != 0u && (mflags(m) & F_FIRE) == 0u) col.rgb = mix(col.rgb, vec3(0.72, 0.88, 1.0), 0.55); // the frozen stain (edits.comp mode 12; the icy look is an approximation of the engine's stain colour)
if ((mflags(m) & (F_STATIC | F_LIQCELL)) == F_STATIC && ext[cp.y * pc.gw + cp.x].x >= 8.0) // a solid that burnt: 0070e060 takes one off every channel per eight of its burn age (burn.comp, ext.x), down to 0
col.rgb = max(col.rgb - vec3(floor(ext[cp.y * pc.gw + cp.x].x / 8.0) / 255.0), vec3(0.0));
if ((mflags(m) & F_STATIC) != 0u && (c & BURN_BIT) == 0u && ((c >> 28) & 7u) != 0u && max(col.r, max(col.g, col.b)) > 18.5 / 255.0) col.rgb = max(col.rgb * (1.0 - float((c >> 28) & 7u) / 7.0), vec3(19.0 / 255.0)); // burnt by explosions (0070e760: every channel x (1 - s), at least 0x13; the level in bits 28-30, explode_rays.comp)
if ((c & BURN_BIT) != 0u) { // a burning cell: a random pixel of colors_lava.bmp every frame (00724580), opaque
col = vec4(burning_colour(m, hash3(uint(cp.x + pc.view_x), uint(cp.y + pc.view_y), uint(pc.frame))), 1.0);
shade = 1.0;
}
vec3 fgc = col.rgb * shade * Lv;
{ // the dust (post_final.frag): the biome modifier dust_amount (tex_fog .g) hazes the lit foreground toward FOG_FOREGROUND 0.6 (night or underground 0.2):
// amount = mix(dust, fog_amount_foreground (the weather fog, sky word 61), sky) x max(sky, dust x luminosity x min(1, 2 perlin^2)); night_amount = clamp((0.55 - |tod - 0.57| - 0.4) x 10, 0, 1)
const vec2 wq = vec2(cp + ivec2(pc.view_x, pc.view_y));
const float dust = bm_dust_r(wq), wfog = uintBitsToFloat(sky[61]);
if (dust > 0.0 || (wfog > 0.0 && gSky > 0.0)) {
const float tod = uintBitsToFloat(sky[0]);
const float night = clamp((0.55 - abs(fract(tod) - 0.57) - 0.4) * 10.0, 0.0, 1.0);
const float pn = perlin_tex(wq * 0.0004 + vec2(0.0, uintBitsToFloat(sky[1]) * 0.005)); // noise_perlin2 = texture2D(tex_perlin_noise, world_pos * 0.0004 + (0, noise_time * 0.005))
const float amt = mix(dust, wfog, gSky) * max(gSky, dust * gLum * min(1.0, pn * pn * 2.0));
fgc = mix(fgc, vec3(mix(0.6, 0.2, max(night, 1.0 - gSky))), amt);
}
}
rgb = mix(bg, fgc, col.a);
}
uint ec = entlayer[cp.y * pc.gw + cp.x];
const bool playerPix = (ec >> 24) == 254u; // the player is drawn below, at its sub-cell position (the sprite layer itself has whole-cell resolution)
if (playerPix) ec = 0u;
if ((ec >> 24) >= 1u && (ec >> 24) <= 3u) rgb += unpack_argb(ec | 0xFF000000u).rgb * gFog; // (the fog of war hides them: post_final multiplies the light buffer they go to by fog_of_war) // additive light (alpha 1: additive projectiles and animations, 2: particles, 3: additive sprites such as glowing eyes)
else if ((ec >> 24) != 0u) { vec4 e4 = unpack_argb(ec); rgb = mix(rgb, e4.rgb * Lv, e4.a); }
return rgb;
}
vec3 post_cell(vec3 rgb, ivec2 cp, vec3 Lv) {
{ // light of projectiles, flashes and explosions: additive glow
vec2 wpos = vec2(cp + ivec2(pc.view_x, pc.view_y)) + 0.5; // (the cell centre: the world is drawn at one pixel per cell)
for (uint i = 0u; i < min(lcount, 512u); ++i) {
float d = distance(wpos, lights[i].a.xy) / lights[i].a.z;
if (d < 1.0) rgb += lights[i].b.rgb * ((1.0 - d) * (1.0 - d) * lights[i].a.w) * gFog;
}
}
{ // the color grading of the biome modifiers (post_final.frag: mix toward the grey by .a, then times .rgb; main.cpp moves it toward the camera's biome; the last 4 words of Tables)
const uint n = mats[mats.length() - 1u].w;
const uint gi = 1024u + 9u * n + 132u + n + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u + 8192u * 4u + 2u * n;
const vec4 cg = vec4(uintBitsToFloat(perm[gi]), uintBitsToFloat(perm[gi + 1u]), uintBitsToFloat(perm[gi + 2u]), uintBitsToFloat(perm[gi + 3u]));
if (cg != vec4(1.0, 1.0, 1.0, 0.0) && cg != vec4(0.0)) { rgb = mix(rgb, vec3((rgb.r + rgb.g + rgb.b) * 0.3333), cg.a); rgb *= cg.rgb; }
}
if ((sky[3] & 1u) != 0u && (gGlow.r + gGlow.g + gGlow.b) > 0.002) { // the glow, blended like post_final.frag: max(color + glow * 0.6 - 0.6 * lights, screen(color, glow))
const vec3 g = gGlow * min(vec3(1.0), 2.8 * vec3(0.6, 0.5, 0.45) * gFog); // (post_final.frag: fog_of_war = min(1, 2 x 1.4 x (0.6, 0.5, 0.45) x (1 - fog)): the glow keeps its strength until the fog is thick)
rgb = max(rgb + g * 0.6 - 0.6 * Lv, clamp(rgb + g - rgb * g, 0.0, 1.0));
}
rgb *= 1.0 - clamp(ctrl.hand2.w, 0.0, 1.0); // BLINDNESS (00b015d0 case 9): a black screen over the world, below the HUD
return rgb;
}
void main() {
// camera shake of explosions: the whole picture (not the HUD) is offset by a random amount
const float scl = float(pc.scale) / 16.0; // output pixels per cell (fixed point, 1/16)
float amp = float(lpad[0]) * 0.001 * scl;
ivec2 so = amp > 0.5 ? ivec2((vec2(float(hash3(uint(pc.frame), 1u, 77u) & 255u), float(hash3(uint(pc.frame), 2u, 77u) & 255u)) / 255.0 - 0.5) * 2.0 * amp) : ivec2(0);
so += ivec2(round(ctrl.view.xy * scl)); // the camera is drawn between its last two ticks, at sub-cell precision (host: view.xy = displayed camera - centre of the window, in cells)
const ivec2 vis = ivec2((vec2(pc.gw, pc.gh) * scl - vec2(pc.ow, pc.oh)) * 0.5); // the window is the centre of the simulated grid (zoomed)
if (pc.pass_ == 1) { // the world pass: the cells from one before the window's first to past its last (main.cpp dispatches ow / scl + 4 by oh / scl + 4)
const ivec2 cp = ivec2(floor(vec2(so + vis) / scl)) - 1 + ivec2(gl_GlobalInvocationID.xy);
if (cp.x < 0 || cp.y < 0 || cp.x >= pc.gw || cp.y >= pc.gh) return;
gScreenY = ((float(cp.y) + 0.5) * scl - float(so.y + vis.y)) / float(pc.oh);
vec3 Lv;
vec3 rgb = world_cell(cp, Lv);
rgb = post_cell(rgb, cp, Lv);
wcell[cp.y * pc.gw + cp.x] = uvec4(packUnorm4x8(vec4(clamp(rgb, 0.0, 1.0), 1.0)), packHalf2x16(Lv.rg), packHalf2x16(vec2(Lv.b, gFog)), 0u);
return;
}
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= pc.ow || p.y >= pc.oh) return;
ivec2 cp = clamp(ivec2(floor(vec2(p + so + vis) / scl)), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1));
vec3 rgb = unpackUnorm4x8(wcell[cp.y * pc.gw + cp.x].x).rgb; // (only the colour: the light is read for the player's pixels)
if (ctrl.inp2.y != 0 && (ents[0].st.w & 8) == 0) {
vec2 P = ents[0].a.xy;
const vec2 pv = prevpos[0].xy;
if (distance(pv, P) < 10.0) P = mix(pv, P, ctrl.view.z);
const ivec2 cq = clamp(ivec2(floor(vec2(p + so + vis) / scl - (P - floor(P)))), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1));
const uint pe = entlayer[cq.y * pc.gw + cq.x];
if ((pe >> 24) == 254u) { // the player's pixel: lit like the cell under it, then the cell's post effects
const uvec4 wc = wcell[cp.y * pc.gw + cp.x];
const vec3 Lv = vec3(unpackHalf2x16(wc.y), unpackHalf2x16(wc.z).x);
gFog = unpackHalf2x16(wc.z).y; gGlow = glow_at(cp);
rgb = post_cell(unpack_argb(pe | 0xFF000000u).rgb * Lv, cp, Lv);
}
}
// TODO(to-do #20): damage_flash_interpolation, additive_overlay_color / overlay_color
{ // the low health indicator (post_final.frag): + LOW_HEALTH_INDICATOR_COLOR (0.7, 0.1, 0) x (1.3 x the distance from the centre)^4 x low_health_indicator_alpha (sky word 62)
const float lh = uintBitsToFloat(sky[62]);
if (lh > 0.0) {
float a = length(vec2(p) / vec2(float(pc.ow), float(pc.oh)) - 0.5) * 1.3;
a *= a; a *= a;
rgb += vec3(0.7, 0.1, 0.0) * a * lh;
}
}
{ // the picture without the HUD for the replay recorder (the game's GIFs have no UI): clip_half.comp / main.cpp copy it
const uvec3 q0 = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5);
clipraw[p.y * pc.ow + p.x] = 0xFF000000u | (q0.r << 16) | (q0.g << 8) | q0.b;
}
const uint ovTile = uint((p.y >> 5) * ((pc.ow + 31) >> 5) + (p.x >> 5)); // the HUD overlay: a mask word per 32 x 32 tile, then 1024 words per tile (main.cpp kOvlTiles)
const uint ov = overlay[ovTile] != 0u ? overlay[3600u + ovTile * 1024u + uint((p.y & 31) * 32 + (p.x & 31))] : 0u;
if ((ov >> 24) != 0u) { vec4 o4 = unpack_argb(ov); rgb = mix(rgb, o4.rgb, o4.a); }
uvec3 q = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5);
pixels[p.y * pc.ow + p.x] = 0xFF000000u | (q.r << 16) | (q.g << 8) | q.b; // BGRA8 in memory
}
+123
View File
@@ -0,0 +1,123 @@
#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; }
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;
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) {
// 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;
}
}
+11
View File
@@ -0,0 +1,11 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "rigid_common.glsl"
// Copies the sub-step result back into the body state.
void main() {
uint b = gl_GlobalInvocationID.x;
if (int(b) >= bhdr.x) return;
bodies[b] = bnext[b];
}
+71
View File
@@ -0,0 +1,71 @@
// Shared declarations of the GPU rigid-body engine (rigid_solve.comp, rigid_commit.comp, rigid_frame.comp).
// The engine is the GPU port of src/physics/rigid.cpp: bodies are pixel masks, terrain is the live cell grid, contacts are aggregated
// into one manifold per pair and resolved with impulses. Bodies are solved Jacobi-style: every body reads the state of the others
// from the previous sub-step (`bodies`) and writes its own result to `bnext`; rigid_commit copies bnext back.
#include "common.glsl"
struct Ent {
vec4 a, box, mv, ai;
ivec4 st;
vec4 hp;
ivec4 x, rg, rg2, aux;
vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0
vec4 ex2; // ranged attack: projectile speed, gravity
ivec4 ex3; // blood material + 1, blood colour, damage types taken since the last check (bit 1 projectile, 2 explosion, 4 fire, 8 melee), 0
vec4 ex4; // air in the lungs (frames), air maximum (0 = does not need air), damage per frame without air, frames left on fire
vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag
vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y)
vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left
ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending
};
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
// shape table: 16 words per shape (w, h, mask offset, sample offset, sample count, com x, com y, mass, inertia, radius as float bits),
// followed by the pixel masks (one word per pixel) and the outline samples (two float words each).
layout(std430, set = 0, binding = 30) readonly buffer Shapes { uint sh[]; };
struct Body {
vec4 p; // centre of mass x, y, angle, angular velocity
vec4 v; // velocity x, y, restitution, friction
ivec4 i; // shape, entity slot, flags (1 asleep, 2 removed), quiet frames
vec4 q; // anchor x, y (the entity origin in image coordinates), density, probe counter
vec4 l; // x, y, angle at the last sleep check
vec4 n; // fragile: impact speed (cells per frame) above which it is damaged, 0 = unbreakable / nail point relative to the entity origin (x, y) / radius in which a surface holds the nail; flag B_NAILED
vec4 m; // collision group (bodies of one group do not collide), y = 1: passable for creatures and projectiles (corpses)
ivec4 jn; // joint partners (body indices, -1 = none)
vec4 ja0, ja1, ja2, ja3; // joint anchors (x, y on this body, x, y on the partner), relative to the centres of mass, in the bodies' own frames
vec4 jb; // break_force multiplier of each joint slot (0 = the joint does not break from force); l.w is the multiplier of the nail
vec4 d; // component values: buoyancy, linear damping, angular damping
};
layout(std430, set = 0, binding = 31) buffer Bodies { ivec4 bhdr; Body bodies[]; }; // bhdr.x = body count
layout(std430, set = 0, binding = 32) buffer BNext { ivec4 nhdr; Body bnext[]; };
const int B_ASLEEP = 1, B_REMOVED = 2, B_NAILED = 4; // nailed: hangs on the wall until the cells around the nail are gone
int shi(int s, int k) { return int(sh[s * 16 + k]); }
float shf(int s, int k) { return uintBitsToFloat(sh[s * 16 + k]); }
bool shape_solid(int s, int x, int y) {
int w = shi(s, 0), h = shi(s, 1);
return x >= 0 && y >= 0 && x < w && y < h && sh[shi(s, 2) + y * w + x] != 0u;
}
vec2 sample_of(int s, int k) {
int o = shi(s, 3) + 2 * k;
return vec2(uintBitsToFloat(sh[o]), uintBitsToFloat(sh[o + 1]));
}
vec2 rot2(float c, float s, vec2 v) { return vec2(c * v.x - s * v.y, s * v.x + c * v.y); }
bool solid_at(ivec2 w) {
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) return false; // no ground outside the window: bodies there are frozen
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m == 0u) return false;
return (mflags(m) & (F_STATIC | F_SAND)) != 0u;
}
bool liquid_at(ivec2 w) {
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m == 0u) return false;
uint f = mflags(m);
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
}
+53
View File
@@ -0,0 +1,53 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "rigid_common.glsl"
// Explosions (from projectiles on the GPU, from props on the host) push the rigid bodies and wake them.
layout(std430, set = 0, binding = 37) buffer Xev { ivec4 xhead; vec4 xe[512]; };
void main() {
uint bi = gl_GlobalInvocationID.x;
if (int(bi) >= bhdr.x) return;
Body B = bodies[bi];
if ((B.i.z & B_REMOVED) != 0) return;
int s = B.i.x;
float mass = shf(s, 7), radius = shf(s, 9);
bool touched = false;
for (int k = 0; k < min(xhead.x, 512); ++k) {
vec4 e = xe[k]; // x, y, radius, power
if (e.w < -0.5) { // an explosion with the force of 00657960: this entry and the next one (force min, force max)
vec4 f2 = xe[min(k + 1, 511)];
++k;
const float pixels = float(shi(s, 4) > 0 ? int(shf(s, 7)) : 1);
const float massKg = max(pixels * B.q.z * 0.01, 1.0e-4);
vec2 O = e.xy, C = B.p.xy;
float dist = length(C - O) * 0.1, Ru = e.z * 0.1; // physics units (10 pixels each)
if (dist > Ru) continue;
// the original needs the angular bin of the explosion to reach this body: approximated by a line of sight through the grid that no solid cell interrupts
bool blocked = false;
const int steps = int(dist * 10.0);
for (int q = 1; q < steps && !blocked; ++q) blocked = solid_at(ivec2(floor(mix(O, C, float(q) / float(steps)))));
if (blocked) continue;
float t = Ru == 0.0 ? 1.0 : 1.0 - dist / Ru;
float A = (f2.y - f2.x) * t + f2.x;
vec2 n = dist >= 1.19e-7 ? normalize(C - O) : vec2(0.0);
// F = n A (x mass if the body weighs less than 1), x 360 N, dv = F / m / 360 per frame in pixels: = n A min(1, m) / m
B.v.xy += n * A * min(1.0, massKg) / massKg;
const float inertiaKg = max(shf(s, 8) * B.q.z * 1.0e-4, 1.0e-6);
const float tau = float(hash3(bi, uint(xhead.x), 91u) & 0x7FFFu) / 32767.0 * 30.0 - 15.0; // the random torque of the original
B.p.w += tau / (3600.0 * inertiaKg);
touched = true;
continue;
}
vec2 d = B.p.xy - e.xy - vec2(0.0, 2.0);
float dist = length(d);
if (dist > e.z + radius) continue;
float f = e.w * max(0.0, 1.0 - dist / (e.z + radius)) / sqrt(max(1.0, mass * B.q.z) / 20.0);
vec2 n = dist > 1e-3 ? d / dist : vec2(0.0, -1.0);
B.v.xy += n * f;
B.p.w += (n.x > 0.0 ? 1.0 : -1.0) * f * 0.05;
touched = true;
}
if (touched) { B.i.z &= ~B_ASLEEP; B.i.w = 0; bodies[bi] = B; }
}
+67
View File
@@ -0,0 +1,67 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "rigid_common.glsl"
// Once per frame: sleeping (resting bodies stop being simulated, sleeping ones re-check their support), and the entity's position and angle.
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits: the flames of a burning corpse
void main() {
uint bi = gl_GlobalInvocationID.x;
if (int(bi) >= bhdr.x) return;
Body B = bodies[bi];
if ((B.i.z & B_REMOVED) != 0) return;
int s = B.i.x;
int nS = shi(s, 4);
vec2 com = vec2(shf(s, 5), shf(s, 6));
float c = cos(B.p.z), sn = sin(B.p.z);
if ((B.i.z & 8) != 0) {
// simulated on the CPU (src/phys): the sleep state comes from there
} else if ((B.i.z & B_ASLEEP) != 0) {
B.q.w += 1.0;
if (B.q.w >= 12.0) { // re-check the support now and then
B.q.w = 0.0;
bool supported = false;
for (int k = 0; k < nS && !supported; ++k) {
vec2 r = rot2(c, sn, sample_of(s, k));
supported = solid_at(ivec2(floor(B.p.x + r.x), floor(B.p.y + r.y + 1.5)));
}
if (!supported) { B.i.z &= ~B_ASLEEP; B.i.w = 0; }
}
} else {
// 009be020: the sleep timer counts while the velocity stays under the tolerances (0.01 units per second linear, 2 degrees per second angular; pixels per frame at scale 10); a body
// sleeps when it has been quiet for half a second AND every body joined to it has too (the island's minimum timer)
const bool quiet = length(B.v.xy) < 0.01 * 10.0 / 60.0 && abs(B.p.w) < 0.034906585 / 60.0;
B.i.w = quiet ? B.i.w + 1 : 0;
if (B.i.w >= 30) {
bool island = true;
for (int k = 0; k < 4 && island; ++k) {
const int j = B.jn[k];
if (j < 0) continue;
const Body C = bodies[j];
if ((C.i.z & B_REMOVED) != 0 || (C.i.z & B_ASLEEP) != 0) continue;
island = C.i.w >= 30;
}
if (island) { B.i.z |= B_ASLEEP; B.v.xy = vec2(0.0); B.p.w = 0.0; B.i.w = 0; }
}
B.l = vec4(B.p.xy, B.p.z, 0.0);
}
if (B.m.z > 0.0) { // a burning corpse part: flames now and then, water puts it out (the pixels of burnable props burn in rigid_burn.comp)
B.m.z -= 1.0;
ivec2 cc = ivec2(floor(B.p.xy)) - ivec2(pc.view_x, pc.view_y);
if (cc.x >= 0 && cc.y >= 0 && cc.x < pc.gw && cc.y < pc.gh) {
uint cm = cell_mat(cells[cc.y * pc.gw + cc.x]);
if (cm != 0u && ((mats[3u * cm + 2u].z >> 18) & 1u) != 0u) B.m.z = 0.0;
}
if (B.m.z > 0.0 && (hash3(bi, uint(pc.frame), 17u) & 3u) == 0u) {
uint ei = uint(atomicAdd(ehead.x, 1));
vec2 fp = B.p.xy + vec2(0.0, -1.0);
if (ei < 256u) { ee[2u * ei] = vec4(fp, 1.5, 0.0); ee[2u * ei + 1u] = vec4(1.0, float(mats[mats.length() - 1u].x), 0.45, 0.0); }
}
}
bodies[bi] = B;
if (B.i.y >= 0) {
vec2 d = rot2(c, sn, B.q.xy - com);
ents[B.i.y].a = vec4(B.p.xy + d, B.p.z, 0.0);
}
}
+28
View File
@@ -0,0 +1,28 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "rigid_common.glsl"
// Marks the cells covered by the pixel masks of the rigid bodies (those that are solid for creatures and projectiles) in a per-cell grid, so that
// creatures and projectiles test a cell in one read instead of looping over all bodies. One workgroup per body.
layout(std430, set = 0, binding = 40) buffer BodyGrid { uint bodyGrid[]; };
void main() {
int bi = int(gl_WorkGroupID.x), t = int(gl_LocalInvocationID.x);
if (bi >= bhdr.x) return;
Body b = bodies[bi];
if ((b.i.z & 2) != 0 || b.m.y > 0.5) return; // removed, or a corpse part (passable)
int s = b.i.x;
float rad = shf(s, 9);
int n = int(2.0 * rad) + 2;
ivec2 lo = ivec2(floor(b.p.xy - vec2(rad)));
float cs = cos(b.p.z), sn = sin(b.p.z);
for (int k = t; k < n * n; k += 64) {
ivec2 w = lo + ivec2(k % n, k / n);
ivec2 q = w - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
vec2 dd = vec2(w) + 0.5 - b.p.xy;
vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6));
if (shape_solid(s, int(floor(im.x)), int(floor(im.y)))) bodyGrid[q.y * pc.gw + q.x] = (b.d.w > 0.5 ? uint(b.d.w + 0.5) : uint(b.i.y) + 1u) | ((b.i.z & 1024) != 0 ? 0x80000000u : 0u); // (bit 31: flag 1024, no platform: characters pass) // the entity slot + 1 (a part body of a PhysicsBody2 entity: the owner's, d.w; the engine's body user data is the entity)
}
}
+310
View File
@@ -0,0 +1,310 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "rigid_common.glsl"
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after the noise words: per material friction and restitution of a rigid contact (6 words per material precede)
// One sub-step of the rigid-body engine, one workgroup per body (a thread per outline sample). Port of PhysicsWorld::step.
shared vec4 sP, sV; // p, v of the body being solved
shared int accN, accPX, accPY, accNX, accNY, accD, accWet, woke;
shared float sImpact; // fastest approach speed into a contact during this sub-step (fragile bodies take damage from it)
void contact_static(inout vec3 u, vec2 r, vec2 n, float e, float mu, float invM, float invI) {
vec2 vc = u.xy + u.z * vec2(-r.y, r.x);
float vn = dot(vc, n);
if (vn >= 0.0) return;
float rn = r.x * n.y - r.y * n.x;
float k = invM + rn * rn * invI;
float rest = -vn < 0.1667 ? 0.0 : e; // 009c1ec0: restitution only for vn < -1 unit/s (10 px/s = 0.1667 cells per frame, pixel scale 10)
float j = -(1.0 + rest) * vn / k;
u.xy += j * n * invM;
u.z += invI * (r.x * (j * n.y) - r.y * (j * n.x));
vec2 wc = u.xy + u.z * vec2(-r.y, r.x);
vec2 t = vec2(-n.y, n.x);
float vt = dot(wc, t);
float rt = r.x * t.y - r.y * t.x;
float kt = invM + rt * rt * invI;
float jt = clamp(-vt / kt, -mu * j, mu * j);
u.xy += jt * t * invM;
u.z += invI * (r.x * (jt * t.y) - r.y * (jt * t.x));
}
vec2 snap_axis(vec2 g) {
if (abs(g.x) > 2.0 * abs(g.y)) g.y = 0.0; else if (abs(g.y) > 2.0 * abs(g.x)) g.x = 0.0;
return g;
}
vec2 terrain_normal(ivec2 c) {
vec2 g = vec2(0.0);
for (int dy = -2; dy <= 2; ++dy)
for (int dx = -2; dx <= 2; ++dx)
if ((dx != 0 || dy != 0) && solid_at(c + ivec2(dx, dy))) {
float d = length(vec2(dx, dy));
g -= vec2(dx, dy) / (d * d);
}
g = snap_axis(g);
float l = length(g);
return l < 1e-6 ? vec2(0.0, -1.0) : g / l;
}
// A world point inside the mask of body C? Gives the outward normal of C's surface (world, pointing into the point's owner) and the depth.
bool mask_contact(vec2 wpnt, vec4 cp, int sc, out vec2 nw, out float depth) {
float qc = cos(cp.z), qs = sin(cp.z);
vec2 d = wpnt - cp.xy;
vec2 l = vec2(qc * d.x + qs * d.y, -qs * d.x + qc * d.y);
vec2 im = l + vec2(shf(sc, 5), shf(sc, 6));
ivec2 ic = ivec2(floor(im));
nw = vec2(0.0, -1.0); depth = 0.5;
if (!shape_solid(sc, ic.x, ic.y)) return false;
vec2 g = vec2(0.0);
for (int dy = -2; dy <= 2; ++dy)
for (int dx = -2; dx <= 2; ++dx)
if ((dx != 0 || dy != 0) && shape_solid(sc, ic.x + dx, ic.y + dy)) {
float dd = length(vec2(dx, dy));
g -= vec2(dx, dy) / (dd * dd);
}
g = snap_axis(g);
float gl = length(g);
if (gl < 1e-6) { g = l; gl = length(g); if (gl < 1e-6) { g = vec2(0.0, -1.0); gl = 1.0; } }
g /= gl;
for (float t = 0.5; t <= 6.0; t += 0.5) {
depth = t;
if (!shape_solid(sc, int(floor(im.x + g.x * t)), int(floor(im.y + g.y * t)))) break;
}
nw = rot2(qc, qs, g);
return true;
}
void add_contact(vec2 rel, vec2 n, float depth) { // rel: world point relative to the centre of mass of the body being solved
atomicAdd(accN, 1);
atomicAdd(accPX, int(round(rel.x * 256.0)));
atomicAdd(accPY, int(round(rel.y * 256.0)));
atomicAdd(accNX, int(round(n.x * 1024.0)));
atomicAdd(accNY, int(round(n.y * 1024.0)));
atomicMax(accD, int(depth * 256.0));
}
void main() {
int bi = int(gl_WorkGroupID.x);
int t = int(gl_LocalInvocationID.x);
if (bi >= bhdr.x) return;
Body B = bodies[bi];
bool asleep = (B.i.z & B_ASLEEP) != 0;
vec2 wp0 = B.p.xy - vec2(pc.view_x, pc.view_y);
if ((B.i.z & B_REMOVED) != 0 || wp0.x < 4.0 || wp0.y < 4.0 || wp0.x > float(pc.gw - 4) || wp0.y > float(pc.gh - 4)) { // frozen outside the window
if (t == 0) bnext[bi] = B;
return;
}
if ((B.i.z & B_NAILED) != 0 && (B.i.z & B_REMOVED) == 0) { // hangs on the wall while there is terrain around the nail
vec2 org = B.p.xy + rot2(cos(B.p.z), sin(B.p.z), B.q.xy - vec2(shf(B.i.x, 5), shf(B.i.x, 6)));
ivec2 nc = ivec2(floor(org + B.n.yz));
bool held = false;
int nr = int(B.n.w);
for (int dy = -nr; dy <= nr; ++dy)
for (int dx = -nr; dx <= nr; ++dx) held = held || (dx * dx + dy * dy <= nr * nr && solid_at(nc + ivec2(dx, dy))); // a surface near the nail point
if (held && B.l.w > 0.0) { // the joint breaks when its reaction force is above (mass * 160) * break_force (docs/IMPLEMENTATION_BLOCKERS.md): the nail holds the body still, so the
// reaction is the whole acceleration it would have had: gravity plus the velocity change the impulses of this frame gave it (explosions, kicks), in cells per frame^2 (x 360 = m/s^2)
const vec2 a = B.v.xy + vec2(0.0, 0.1667);
const float lim = 160.0 * B.l.w / 360.0;
if (dot(a, a) > lim * lim) held = false;
}
if (held) { B.v.xy = vec2(0.0); B.p.w = 0.0; if (t == 0) bnext[bi] = B; return; } // (the impulses of the frame went into the wall)
B.i.z &= ~(B_NAILED | B_ASLEEP); // the wall is gone or the joint broke: it falls
B.i.w = 0;
asleep = false;
}
int s = B.i.x;
int nS = shi(s, 4);
float mass = shf(s, 7), inertia = shf(s, 8), radius = shf(s, 9);
float invM = 1.0 / (mass * B.q.z), invI = 1.0 / (inertia * B.q.z);
const float dt = 0.5;
if (t == 0) { sP = B.p; sV = B.v; accWet = 0; woke = 0; accN = 0; accPX = 0; accPY = 0; accNX = 0; accNY = 0; accD = 0; sImpact = 0.0; }
barrier();
if (!asleep) {
// buoyancy and drag in liquids
// 00bda390 / 00bde0f0 (docs/LIQUID_PHYSICS_COMPLETE_GUIDE.md section 8): ONE probe at (int(x), int(y) + 8) decides; in a liquid (not a powder) the force is -b m (v + g gravity_scale) at the centre,
// b = the component's buoyancy (PhysicsBody2Component constructor 00672980: 0.7). dv/dt = g (1 - b) - b v, no submerged fraction, no density, no extra rotation damping.
float c = cos(sP.z), sn = sin(sP.z);
if (t == 0) { // the probe sits at the ENTITY ORIGIN (the image point B.q, not the centre of mass) plus 8 in y, float coordinates truncating toward zero
const vec2 org = sP.xy + rot2(c, sn, B.q.xy - vec2(shf(s, 5), shf(s, 6)));
if (liquid_at(ivec2(int(org.x), int(org.y) + 8))) accWet = 1;
}
barrier();
if (t == 0) {
const float buoy = B.d.x * float(accWet); // the component's buoyancy
vec4 P = sP, V = sV;
const float h = dt / 60.0; // 009be020: v = (v + h (g + F / m)) / (1 + h linear_damping), the damping acts on the gravity just added
V.y += 0.1667 * dt * (1.0 - buoy);
V.xy *= (1.0 - buoy * dt / 60.0) / (1.0 + h * B.d.y);
P.w /= 1.0 + h * B.d.z;
float sp = length(V.xy);
if (sp > 2500.0 / 60.0) V.xy *= (2500.0 / 60.0) / sp; // the initial cap: 250 physics units per second (x 10 pixels)
P.w = clamp(P.w, -1.5707963, 1.5707963); // 009be020: |h omega| <= pi / 2
vec2 stepv = V.xy * dt;
if (length(stepv) > 20.0) stepv *= 20.0 / length(stepv); // after the constraints a translation of at most 2 physics units (20 pixels) per step
P.xy += stepv;
P.z += P.w * dt;
sP = P; sV = V;
}
barrier();
// terrain contacts: outline samples inside solid cells form one aggregated manifold
c = cos(sP.z); sn = sin(sP.z);
if (t < nS) {
vec2 r = rot2(c, sn, sample_of(s, t));
vec2 pnt = sP.xy + r;
ivec2 cell = ivec2(floor(pnt));
if (solid_at(cell)) {
vec2 nm = terrain_normal(cell);
float depth = 0.5;
for (float tt = 0.5; tt <= 6.0; tt += 0.5) {
depth = tt;
if (!solid_at(ivec2(floor(pnt + nm * tt)))) break;
}
add_contact(r, nm, depth);
}
}
barrier();
if (t == 0 && accN > 0) {
vec2 cp = vec2(float(accPX), float(accPY)) / (256.0 * float(accN));
vec2 an = vec2(float(accNX), float(accNY)) / 1024.0;
float al = length(an);
if (al > 1e-6) {
vec3 u = vec3(sV.xy, sP.w);
sImpact = max(sImpact, -dot(u.xy + u.z * vec2(-cp.y, cp.x), an / al));
// the contacted terrain material: its own solid_friction / solid_restitution (009c18c0: friction sqrt(fA fB), restitution max)
float tf = 0.3, tr = 0.2;
{
const ivec2 cc = ivec2(floor(sP.xy + cp));
for (int k = 0; k < 9; ++k) {
const ivec2 q = cc + ivec2(k % 3 - 1, k / 3 - 1) - 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 tm = cell_mat(cells[q.y * pc.gw + q.x]);
if (tm != 0u && (mflags(tm) & F_STATIC) != 0u) {
const uint tb = 1024u + 6u * mats[mats.length() - 1u].w + 2u * tm;
tf = uintBitsToFloat(tbl[tb]); tr = uintBitsToFloat(tbl[tb + 1u]);
break;
}
}
}
for (int it = 0; it < 2; ++it) contact_static(u, cp, an / al, max(B.v.z, tr), sqrt(B.v.w * tf), invM, invI);
sV.xy = u.xy; sP.w = u.z;
float push = min(float(accD) / 256.0, 3.0) * 0.7;
sP.xy += an / al * push;
}
}
barrier();
}
// contacts with other bodies: my outline in their mask and theirs in mine
for (int j = 0; j < bhdr.x; ++j) {
if (j == bi) continue;
Body C = bodies[j];
if ((C.i.z & B_REMOVED) != 0) continue;
if (B.m.x > 0.5 && B.m.x == C.m.x) continue; // parts of one corpse do not collide with each other
bool casleep = (C.i.z & B_ASLEEP) != 0;
if (asleep && casleep) continue;
int sc = C.i.x;
float rc = shf(sc, 9);
if (distance(sP.xy, C.p.xy) > radius + rc) continue;
if (t == 0) { accN = 0; accPX = 0; accPY = 0; accNX = 0; accNY = 0; accD = 0; }
barrier();
vec4 myp = vec4(sP.xy, sP.z, 0.0);
float c = cos(sP.z), sn = sin(sP.z);
if (t < nS) {
vec2 w = sP.xy + rot2(c, sn, sample_of(s, t));
vec2 nw; float depth;
if (mask_contact(w, C.p, sc, nw, depth)) add_contact(w - sP.xy, nw, depth);
}
int ncs = shi(sc, 4);
if (t < ncs) {
float cc = cos(C.p.z), cs = sin(C.p.z);
vec2 w = C.p.xy + rot2(cc, cs, sample_of(sc, t));
vec2 nw; float depth;
if (mask_contact(w, myp, s, nw, depth)) add_contact(w - sP.xy, -nw, depth);
}
barrier();
if (t == 0 && accN > 0) {
if (asleep && !casleep && length(C.v.xy) > 0.15) woke = 1; // a moving neighbour wakes a sleeping body; resting contacts do not
if (!asleep) {
float invMc = 1.0 / (shf(sc, 7) * C.q.z), invIc = 1.0 / (shf(sc, 8) * C.q.z);
float e = max(B.v.z, C.v.z), mu = sqrt(B.v.w * C.v.w);
vec2 ra = vec2(float(accPX), float(accPY)) / (256.0 * float(accN));
vec2 rcv = sP.xy + ra - C.p.xy;
vec2 sn_ = vec2(float(accNX), float(accNY)) / 1024.0;
float al = length(sn_);
if (al > 1e-6) {
vec2 n = sn_ / al;
vec3 u = vec3(sV.xy, sP.w), uc = vec3(C.v.xy, C.p.w); // uc follows the partner's response virtually, only my side is applied
for (int it = 0; it < 2; ++it) {
vec2 va = u.xy + u.z * vec2(-ra.y, ra.x), vc = uc.xy + uc.z * vec2(-rcv.y, rcv.x);
float vn = dot(va - vc, n);
if (vn >= 0.0) break;
if (it == 0) sImpact = max(sImpact, -vn);
float ran = ra.x * n.y - ra.y * n.x, rcn = rcv.x * n.y - rcv.y * n.x;
float kk = invM + invMc + ran * ran * invI + rcn * rcn * invIc;
float jn = -(1.0 + (-vn < 0.1667 ? 0.0 : e)) * vn / kk;
u.xy += jn * n * invM; u.z += invI * (ra.x * (jn * n.y) - ra.y * (jn * n.x));
uc.xy -= jn * n * invMc; uc.z -= invIc * (rcv.x * (jn * n.y) - rcv.y * (jn * n.x));
vec2 wa = u.xy + u.z * vec2(-ra.y, ra.x), wc = uc.xy + uc.z * vec2(-rcv.y, rcv.x);
vec2 tg = vec2(-n.y, n.x);
float vt = dot(wa - wc, tg);
float rat = ra.x * tg.y - ra.y * tg.x, rct = rcv.x * tg.y - rcv.y * tg.x;
float kt = invM + invMc + rat * rat * invI + rct * rct * invIc;
float jt = clamp(-vt / kt, -mu * jn, mu * jn);
u.xy += jt * tg * invM; u.z += invI * (ra.x * (jt * tg.y) - ra.y * (jt * tg.x));
uc.xy -= jt * tg * invMc; uc.z -= invIc * (rcv.x * (jt * tg.y) - rcv.y * (jt * tg.x));
}
sV.xy = u.xy; sP.w = u.z;
float push = min(float(accD) / 256.0, 3.0) * 0.6;
sP.xy += n * push * invM / (invM + invMc);
}
}
}
barrier();
}
// pin joints (ragdolls): the two anchors are pulled together; every body applies its own side of the impulse
int brokenJoints = 0;
if (t == 0) {
for (int k = 0; k < 4; ++k) {
int j = B.jn[k];
if (j < 0) continue;
Body C = bodies[j];
if ((C.i.z & B_REMOVED) != 0) { brokenJoints |= 1 << k; continue; } // the partner is gone (cut away, burnt): the joint is dropped and the body wakes
if (asleep) { if ((C.i.z & B_ASLEEP) == 0 && length(C.v.xy) > 0.15) woke = 1; continue; }
vec4 ja = k == 0 ? B.ja0 : (k == 1 ? B.ja1 : (k == 2 ? B.ja2 : B.ja3));
int sc = C.i.x;
float invMc = 1.0 / (shf(sc, 7) * C.q.z), invIc = 1.0 / (shf(sc, 8) * C.q.z);
vec2 ra = rot2(cos(sP.z), sin(sP.z), ja.xy), rb = rot2(cos(C.p.z), sin(C.p.z), ja.zw);
vec2 err = (C.p.xy + rb) - (sP.xy + ra);
vec2 va = sV.xy + sP.w * vec2(-ra.y, ra.x), vb = C.v.xy + C.p.w * vec2(-rb.y, rb.x);
vec2 r = (vb - va) + err * 0.35; // residual relative velocity of the anchors, with a pull toward each other
float m = invM + invMc;
mat2 K = mat2(m + invI * ra.y * ra.y + invIc * rb.y * rb.y, -invI * ra.x * ra.y - invIc * rb.x * rb.y,
-invI * ra.x * ra.y - invIc * rb.x * rb.y, m + invI * ra.x * ra.x + invIc * rb.x * rb.x);
vec2 J = inverse(K) * r;
const float jbk = k == 0 ? B.jb.x : (k == 1 ? B.jb.y : (k == 2 ? B.jb.z : B.jb.w));
if (jbk != 0.0) { // force of the joint: impulse / dt, in newtons (mass 0.01 per pixel, 360 = cells/frame^2 -> m/s^2); limit = (m_a + m_b) * 160 * break_force,
// for the ragdoll kinds 0x2b67 / 0x2b68 (stored negative) max((m_a + m_b) * 400, 200) * break_force
const float M = (shf(s, 7) * B.q.z + shf(sc, 7) * C.q.z) * 0.01;
const float F = length(J) * 0.01 * 360.0 / dt, lim = jbk > 0.0 ? M * 160.0 * jbk : max(M * 400.0, 200.0) * -jbk;
if (F * F > lim * lim) brokenJoints |= 1 << k;
}
sV.xy += J * invM;
sP.w += invI * (ra.x * J.y - ra.y * J.x);
sP.xy += err * 0.15 * invM / m; // and a little of the error is corrected directly
woke = max(woke, 0);
}
}
barrier();
if (t == 0) {
if (B.n.x > 0.0 && B.i.y >= 0 && sImpact > B.n.x) atomicAdd(ents[B.i.y].x.x, int((sImpact - B.n.x) * 750.0)); // fall / impact damage of fragile bodies
Body o = B;
o.p = sP;
o.v.xy = sV.xy;
for (int k = 0; k < 4; ++k) if ((brokenJoints & (1 << k)) != 0) { o.jn[k] = -1; woke = 1; }
if (woke != 0) { o.i.z &= ~B_ASLEEP; o.i.w = 0; }
bnext[bi] = o;
}
}
+23
View File
@@ -0,0 +1,23 @@
// (needs the Tables buffer `tbl`: its first 512 words are the permutation table)
// 00872560: Gustavson's 3D simplex noise (skew 1/3, unskew 1/6, falloff 0.6, x 32) on the permutation table at the start of Tables (00fdf530 holds the same values)
const ivec3 G3[12] = ivec3[12](ivec3(1,1,0), ivec3(-1,1,0), ivec3(1,-1,0), ivec3(-1,-1,0), ivec3(1,0,1), ivec3(-1,0,1), ivec3(1,0,-1), ivec3(-1,0,-1), ivec3(0,1,1), ivec3(0,-1,1), ivec3(0,1,-1), ivec3(0,-1,-1));
float corner3(int gi, vec3 d) { float t = 0.6 - dot(d, d); if (t < 0.0) return 0.0; t *= t; return t * t * dot(vec3(G3[gi]), d); }
float simplex3(vec3 v) {
const float F3 = 1.0 / 3.0, G = 1.0 / 6.0;
const float s = (v.x + v.y + v.z) * F3;
const ivec3 ijk = ivec3(floor(v + s));
const float t = float(ijk.x + ijk.y + ijk.z) * G;
const vec3 d0 = v - (vec3(ijk) - t);
ivec3 o1, o2;
if (d0.x >= d0.y) { if (d0.y >= d0.z) { o1 = ivec3(1,0,0); o2 = ivec3(1,1,0); } else if (d0.x >= d0.z) { o1 = ivec3(1,0,0); o2 = ivec3(1,0,1); } else { o1 = ivec3(0,0,1); o2 = ivec3(1,0,1); } }
else { if (d0.y < d0.z) { o1 = ivec3(0,0,1); o2 = ivec3(0,1,1); } else if (d0.x < d0.z) { o1 = ivec3(0,1,0); o2 = ivec3(0,1,1); } else { o1 = ivec3(0,1,0); o2 = ivec3(1,1,0); } }
const vec3 d1 = d0 - vec3(o1) + G, d2 = d0 - vec3(o2) + 2.0 * G, d3 = d0 - 1.0 + 3.0 * G;
const int ii = ijk.x & 255, jj = ijk.y & 255, kk = ijk.z & 255;
#define P(n) int(tbl[uint(n)])
const int g0 = P(ii + P(jj + P(kk))) % 12;
const int g1 = P(ii + o1.x + P(jj + o1.y + P(kk + o1.z))) % 12;
const int g2 = P(ii + o2.x + P(jj + o2.y + P(kk + o2.z))) % 12;
const int g3 = P(ii + 1 + P(jj + 1 + P(kk + 1))) % 12;
#undef P
return 32.0 * (corner3(g0, d0) + corner3(g1, d1) + corner3(g2, d2) + corner3(g3, d3));
}
+18
View File
@@ -0,0 +1,18 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "common.glsl"
// The sky is visible in a window column above its first solid cell (static or powder); this finds that cell for every column (the sky buffer: words 64 ...).
layout(std430, set = 0, binding = 44) buffer Sky { uint sky[]; };
void main() {
int x = int(gl_GlobalInvocationID.x);
if (x >= pc.gw) return;
int top = pc.gh;
for (int y = 0; y < pc.gh; ++y) {
uint m = cell_mat(cells[y * pc.gw + x]);
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) { top = y; break; }
}
sky[64 + x] = uint(top);
}
+132
View File
@@ -0,0 +1,132 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "common.glsl"
// Draws entity sprites into the per-cell entity layer. One workgroup per sprite instance.
// The sprite is placed at its entity current position (entities.comp moves them), mirrored when the entity faces left, and
// animated: mobile entities play their walk animation while moving and the idle one otherwise.
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
layout(std430, set = 0, binding = 19) readonly buffer EntTexels { uint enttex[]; };
layout(std430, set = 0, binding = 20) readonly buffer EntInst { ivec4 entinst[]; }; // 4 per instance, see main.cpp
layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; };
layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // positions before the last tick
layout(std430, set = 0, binding = 54) buffer EntDepth { uint entdepth[]; };
// the rigid bodies (rigid_common.glsl) and the burn state of their pixels (rigid_burn.comp: bits 0-15 the burn age): a burnt pixel is charred
struct SBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0, ja1, ja2, ja3; vec4 jb; vec4 d; };
layout(std430, set = 0, binding = 31) readonly buffer Bodies { ivec4 bhdr; SBody bodies[]; };
layout(std430, set = 0, binding = 41) readonly buffer Burn { uint burnpx[]; };
shared int sBody;
// the burning-cell palette (main.cpp puts colors_lava.bmp after the tables; render.comp / glow.comp read it the same way)
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
uint fpal_word(uint k) { return tbl[1024u + 9u * mats[mats.length() - 1u].w + k]; }
uint burning_texel(uint m, uint h) { // 00724580: a burning cell takes a random pixel of colors_lava.bmp (the row of its fire_colors_index) every frame; alpha 253 marks it for glow.comp
const uint row = min(fpal_word(132u + m), 5u);
return (fpal_word(96u + row * 6u + h % 6u) & 0x00FFFFFFu) | 0xFD000000u;
} // per cell: the depth key of the nearest opaque texel (cleared to ~0 every frame)
// The depth of an instance: SpriteComponent z_index (larger = further back; the instance keeps z x 100), then the later instance in front of an earlier one of the same z
uint gKey = 0xFFFFFFFFu;
uint gFlash = 0u; // the hit flash (entities.comp hit_flash): 0, or the colour every opaque texel takes (white; red for a critical hit), keeping its alpha
float gFade = 0.0; // LifetimeComponent fade_sprites (00b9a010): the sprites' alpha is 1 - this (ex10.w of an entity that is not a creature; the host sets it)
// Two passes: 0 draws the ordinary texels, 1 the additive ones (alpha 3: the glow of emissive sprites). The glow is added to what is already in the cell (the creature under
// its eyes keeps its pixels); drawing both kinds in one dispatch let the writers of a cell race, which made the creature vanish behind its glow in some frames.
void put_texel(int idx, uint t) {
uint a = t >> 24;
if (a == 0u) return;
if (gFade > 0.0 && a >= 4u && a != 253u && a != 254u) { // (the alpha byte keeps clear of the markers 1-3, 253, 254)
a = uint(float(a) * (1.0 - gFade) + 0.5);
if (a < 4u) return;
a = min(a, 252u);
t = (t & 0x00FFFFFFu) | (a << 24);
}
const bool additive = a == 3u;
if (gFlash != 0u && !additive && a >= 4u) t = (t & 0xFF000000u) | gFlash;
if (pc.pass_ == 2) { if (!additive) atomicMin(entdepth[idx], gKey); return; } // depth pass
if (!additive && entdepth[idx] != gKey) return; // hidden by a nearer texel
if (additive != (pc.pass_ == 1)) return;
if (!additive) { entlayer[idx] = a == 1u ? (t | 0x02000000u) & 0xFEFFFFFFu : t; return; }
const uint cur = entlayer[idx];
if (cur == 0u) { entlayer[idx] = t; return; }
const uvec3 s = uvec3((cur >> 16) & 255u, (cur >> 8) & 255u, cur & 255u) + uvec3((t >> 16) & 255u, (t >> 8) & 255u, t & 255u);
const uvec3 c = min(s, uvec3(255u));
entlayer[idx] = (cur & 0xFF000000u) | (c.r << 16) | (c.g << 8) | c.b;
}
void main() {
uint id = gl_WorkGroupID.x;
ivec4 h0 = entinst[4u * id], hi = entinst[4u * id + 1u], hw = entinst[4u * id + 2u];
Ent e = ents[h0.x];
gKey = (uint(clamp(h0.w + 32768, 0, 65535)) << 16) | (65535u - (id & 65535u));
if ((e.st.w & (8 | 4096)) != 0) return; // dead, or its sprites switched off (4096: a creature, main.cpp on_components_enabled)
if (e.st.x != 4 && (e.ex8.x & 8192) != 0) return;
{ // the hit flash: creatures, the player and physics items (a worm segment, kind 0, keeps its root slot in ex19.w)
const int fc = int(e.ex19.w + 0.5);
if (e.st.x != 0 && (fc & 7) >= 3) gFlash = (fc & 8) != 0 ? 0xFF0000u : 0xFFFFFFu;
}
gFade = (e.st.x == 1 || e.st.x == 2 || e.st.x == 3) ? 0.0 : clamp(e.ex10.w, 0.0, 1.0); // INVISIBILITY active (a prop's ex8.xy is the hit point of its leak) (00b015d0 case 0x51: its sprites are told not to draw)
{ vec2 pv = prevpos[h0.x].xy; if (distance(pv, e.a.xy) < 10.0) e.a.xy = mix(pv, e.a.xy, ctrl.view.z); } // drawn between the last two ticks
const bool rigid = (e.st.w & (32 | 64)) != 0; // drawn rotated by the body angle in a.z (64: the part of a Verlet chain, angle in ex7.x)
bool mobile = e.st.x != 0 && !rigid;
bool moving = mobile && abs(e.a.z) > 0.08;
ivec4 an = moving ? hw : hi; // tex, w, h, frames | wait << 16
int w = an.y, h = an.z, frames = an.w & 0xFFFF, wait = max(an.w >> 16, 1);
int frame = (pc.frame / wait + int(h0.x)) % max(frames, 1);
{ // an action the host played (the attacks of the AnimalAI): its animation once from its start frame (row 4 of the instance: texture, frames | wait << 16, start frame, w | h << 16)
const ivec4 hx = entinst[4u * id + 3u];
const bool loopA = hx.z < 0; // (a negative start: start + 1 negated, a movement animation that loops: jump_up, jump_fall, swim_*, fly_*, burn)
const int af = max(hx.y & 0xFFFF, 1), aw = max(hx.y >> 16, 1), t = pc.frame - (loopA ? -hx.z - 1 : hx.z);
if (hx.x > 0 && t >= 0 && (loopA || t < af * aw) && (hx.w & 0xFFFF) > 0 && (hx.w >> 16) > 0) { w = hx.w & 0xFFFF; h = hx.w >> 16; an = ivec4(hx.x, w, h, hx.y); frames = af; wait = aw; frame = loopA ? (t / aw) % af : t / aw; } // (its own frame size: the zombie attacks in 19 x 20 frames, stands in 18 x 19; the sprite offset is the file's)
}
if (rigid) {
if (gl_LocalInvocationID.x == 0u) sBody = -1;
barrier();
for (int k = int(gl_LocalInvocationID.x); k < min(bhdr.x, 1024); k += 64) if (bodies[k].i.y == int(h0.x) && (bodies[k].i.z & 2) == 0) sBody = k;
barrier();
const int body = (e.st.w & 64) == 0 && w * h <= 4096 ? sBody : -1;
int rf = (pc.frame / wait + int(h0.x)) % max(frames, 1);
const float ang = (e.st.w & 64) != 0 ? e.ex7.x : e.a.z;
const float sx = (e.st.w & 64) != 0 && e.ex7.y > 0.0 ? e.ex7.y : 1.0; // a part stretched along its length (the segments of a limb: IKLimbComponent)
float c = cos(ang), s = sin(ang);
vec2 anchor = vec2(float(h0.y), float(h0.z));
float r = length(max(anchor, vec2(float(w), float(h)) - anchor) * vec2(max(sx, 1.0), 1.0)) + 1.5;
int n = int(2.0 * r) + 2;
ivec2 lo = ivec2(floor(e.a.xy - vec2(r))) - ivec2(pc.view_x, pc.view_y);
int rbase = an.x + rf * w * h;
for (int i = int(gl_LocalInvocationID.x); i < n * n; i += 64) {
ivec2 p = lo + ivec2(i % n, i / n);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
vec2 d = vec2(p + ivec2(pc.view_x, pc.view_y)) + vec2(0.5) - e.a.xy;
ivec2 sp = ivec2(floor(anchor + vec2((c * d.x + s * d.y) / sx, -s * d.x + c * d.y)));
if (sp.x < 0 || sp.y < 0 || sp.x >= w || sp.y >= h) continue;
uint t = enttex[uint(rbase + sp.y * w + sp.x)];
if (body >= 0 && (t >> 24) > 3u) { // 0070e060: a burnt solid loses one of every channel per eight of its burn age
const uint bst = burnpx[uint(body) * 4096u + uint(sp.y * w + sp.x)];
const uint bm = uint(e.ex5.z + 0.5); // (the body's material + 1, as rigid_burn.comp reads it)
if ((bst & 0x80000000u) != 0u && bm > 0u) { put_texel(p.y * pc.gw + p.x, burning_texel(bm - 1u, hash3(uint(sp.x + sp.y * w), uint(body), uint(pc.frame)))); continue; } // a burning pixel: the fire colours
const uint dk = (bst & 0xFFFFu) >> 3;
if (dk > 0u) { const uvec3 c3 = uvec3((t >> 16) & 255u, (t >> 8) & 255u, t & 255u); const uvec3 r3 = uvec3(max(ivec3(c3) - int(dk), ivec3(0))); t = (t & 0xFF000000u) | (r3.r << 16) | (r3.g << 8) | r3.b; }
}
put_texel(p.y * pc.gw + p.x, t); // alpha 1 is the marker of additive light in render.comp: faint pixels (the halo of the portal) take alpha 2
}
return;
}
bool flip = mobile && e.st.y < 0;
ivec2 pos = ivec2(floor(e.a.xy));
int ox = flip ? (w - 1 - h0.y) : h0.y;
ivec2 origin = pos - ivec2(ox, h0.z) - ivec2(pc.view_x, pc.view_y); // window position of the sprite top-left
if (origin.x >= pc.gw || origin.y >= pc.gh || origin.x + w <= 0 || origin.y + h <= 0) return;
int base = an.x + frame * w * h;
for (int i = int(gl_LocalInvocationID.x); i < w * h; i += 64) {
int sxp = i % w;
ivec2 p = origin + ivec2(sxp, i / w);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue;
uint t = enttex[uint(base + (flip ? (i / w) * w + (w - 1 - sxp) : i))];
if (h0.x == 0u && (t >> 24) == 255u) t = (t & 0x00FFFFFFu) | 0xFE000000u; // the player's pixels (alpha 254): render.comp draws them at the sub-cell position
put_texel(p.y * pc.gw + p.x, t);
}
}
+451
View File
@@ -0,0 +1,451 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
#include "areas.glsl"
#include "far.glsl"
// Terrain generation on the GPU: GLSL port of the engine's wang-image -> cell path
// (docs/ENGINE_ANALYSIS.md section 18). One invocation per grid cell.
layout(std430, set = 0, binding = 6) readonly buffer Field { float field[]; }; // wang solidity plane
layout(std430, set = 0, binding = 7) readonly buffer MatPlane { uint matplane[]; }; // CellData index + 1, 0 = none
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint perm[]; }; // [0,512) simplex, [512,1024) perlin
layout(std430, set = 0, binding = 9) readonly buffer MatParams { vec2 mparams[]; }; // per material: warp, threshold
layout(std430, set = 0, binding = 10) readonly buffer CellsOld { uint cells_old[]; };
layout(std430, set = 0, binding = 11) readonly buffer ExtOld { vec2 ext_old[]; };
layout(std430, set = 0, binding = 52) readonly buffer BirthOld { uint birth_old[]; }; // the ages before the scroll
layout(std430, set = 0, binding = 12) readonly buffer Scene { uint scene[]; }; // pixel scenes: 0 = none, else material + 1
layout(std430, set = 0, binding = 18) readonly buffer Override { uint ovr[]; }; // cells restored from the persistent store (0xFFFFFFFF = none)
int ffloor(float v) { return int(floor(v)); }
float fade(float t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
float sstep(float t) { return (3.0 - t * 2.0) * t * t; }
float sgrad(uint hsh, float x, float y) {
uint h = hsh & 7u;
float u = h < 4u ? x : y;
float v = h < 4u ? y : x;
return (((h & 1u) != 0u) ? -u : u) + (((h & 2u) != 0u) ? -2.0 * v : 2.0 * v);
}
uint ps(uint i) { return perm[i]; }
uint pp(uint i) { return perm[512u + i]; }
float simplex2(float x, float y) {
const float F2 = 0.366025404, G2 = 0.211324865;
float s = (x + y) * F2;
int i = ffloor(x + s), j = ffloor(y + s);
float t = float(i + j) * G2;
float x0 = x - (float(i) - t), y0 = y - (float(j) - t);
int i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1;
float x1 = x0 - float(i1) + G2, y1 = y0 - float(j1) + G2;
float x2 = x0 - 1.0 + 2.0 * G2, y2 = y0 - 1.0 + 2.0 * G2;
uint ii = uint(i) & 255u, jj = uint(j) & 255u;
float n0 = 0.0, n1 = 0.0, n2 = 0.0;
float t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 >= 0.0) { t0 *= t0; n0 = t0 * t0 * sgrad(ps(ii + ps(jj)), x0, y0); }
float t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 >= 0.0) { t1 *= t1; n1 = t1 * t1 * sgrad(ps(ii + uint(i1) + ps(jj + uint(j1))), x1, y1); }
float t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 >= 0.0) { t2 *= t2; n2 = t2 * t2 * sgrad(ps(ii + 1u + ps(jj + 1u)), x2, y2); }
return 40.0 * (n0 + n1 + n2);
}
// 8 gradients of FUN_00872a20
vec2 pgrad(uint g) {
const vec2 G[8] = vec2[8](vec2(-1, 0), vec2(1, 0), vec2(0, -1), vec2(0, 1), vec2(-1, -1), vec2(-1, 1), vec2(1, -1), vec2(1, 1));
return G[g];
}
float perlin2(float x, float y) {
int ix = ffloor(x), iy = ffloor(y);
uint X = uint(ix) & 255u, Y = uint(iy) & 255u, X1 = uint(ix + 1) & 255u, Y1 = uint(iy + 1) & 255u;
float fx = x - float(ix), fy = y - float(iy);
uint pa = pp(X), pb = pp(X1);
float na = dot(pgrad(pp(pa + Y) & 7u), vec2(fx, fy));
float nb = dot(pgrad(pp(pb + Y) & 7u), vec2(fx - 1.0, fy));
float nc = dot(pgrad(pp(pa + Y1) & 7u), vec2(fx, fy - 1.0));
float nd = dot(pgrad(pp(pb + Y1) & 7u), vec2(fx - 1.0, fy - 1.0));
float v = fade(fy), u = fade(fx);
return mix(mix(na, nc, v), mix(nb, nd, v), u);
}
float value2(float x, float y) {
int ix = ffloor(x), iy = ffloor(y);
uint X = uint(ix) & 255u, Y = uint(iy) & 255u, X1 = uint(ix + 1) & 255u, Y1 = uint(iy + 1) & 255u;
float fx = x - float(ix), fy = y - float(iy);
uint pa = pp(X), pb = pp(X1);
float v00 = float(pp(pa + Y)) / 255.0, v01 = float(pp(pa + Y1)) / 255.0;
float v10 = float(pp(pb + Y)) / 255.0, v11 = float(pp(pb + Y1)) / 255.0;
float v = fade(fy), u = fade(fx);
return (mix(mix(v00, v01, v), mix(v10, v11, v), u) - 0.5) * 2.0;
}
int wrapi(int v, int n) { v = v % n; return v < 0 ? v + n : v; }
int gpw, gph, gfo, gmo; // the area of the cell being generated: plane size and offsets
float field_at(int x, int y) { return field[gfo + wrapi(y, gph) * gpw + wrapi(x, gpw)]; }
// FUN_008712d0: nearest wang pixel (smoothstep(f) >= 0.5 rounds up); CellData index or -1
int material_pixel(float px, float py) {
int ix = ffloor(px), iy = ffloor(py);
float fx = px - float(ix), fy = py - float(iy);
if (!(0.5 > sstep(fy))) ++iy;
if (!(0.5 > sstep(fx))) ++ix;
return int(matplane[gmo + wrapi(iy, gph) * gpw + wrapi(ix, gpw)]) - 1;
}
// FUN_00870e60: smoothstep-weighted bilinear sample of the solidity plane
float bilinear(float px, float py) {
int ix = ffloor(px), iy = ffloor(py);
float sx = sstep(px - float(ix)), sy = sstep(py - float(iy));
float top = mix(field_at(ix, iy), field_at(ix + 1, iy), sx);
float bot = mix(field_at(ix, iy + 1), field_at(ix + 1, iy + 1), sx);
return mix(top, bot, sy);
}
// ---- biome <Materials> evaluator (FUN_0086d2a0) and its noise: 70-scaled simplex (FUN_00872300), 4-octave fbm (FUN_009296d0), polka dots (FUN_00872140)
#include "procgrad.glsl"
// FUN_0086fd40 / FUN_0086fe30: the cell hash (one value, and three more from the same product)
float cell_u(vec2 c) {
float cx = floor(c.x * 0.014084507), cy = floor(c.y * 0.014084507);
float a = c.x - cx * 71.0 + 26.0, b = c.y - cy * 71.0 + 161.0;
return b * b * (a * a);
}
float polka(float x, float y, float rlow, float rhigh, bool boxed, float prob) {
vec2 c = vec2(floor(x), floor(y));
float u = cell_u(c);
if (fract(u * 0.0010131986) >= prob) return 0.0;
float fx = x - c.x, fy = y - c.y;
float o0 = fract(u * 0.0010513747), o1 = fract(u * 0.0015553312), o2 = fract(u * 0.0012450161);
float r = (rhigh - rlow) * o2 + rlow;
if (r <= 0.0) return 0.0;
float inv = 2.0 / r;
float dx = fx * inv - (inv - 1.0) + (inv - 2.0) * o0;
float dy = fy * inv - (inv - 1.0) + (inv - 2.0) * o1;
float d;
if (boxed) { d = dy * dy; d *= d; float e = dx * dx; d += e * e; if (d > 1.0) return 0.0; }
else { d = min(dx * dx + dy * dy, 1.0); }
float q = 1.0 - d;
return q * q * q;
}
// area a's component list: 20 words each (see AreaStore): [0] material, [1] flags, [2..3] min max, [4..5] add_perlin scale, [6..7] limit min max, [8..9] rare scale,
// [10..11] rare offset, [12..13] required min max, [14..15] polka radius low high, [16] polka probability
uint eval_materials_at(int base, int cnt, float v, ivec2 w) {
float fx = float(w.x), fy = float(w.y);
for (int k = 0; k < cnt; ++k) {
int o = base + 20 * k;
uint mat = atab[o];
uint fl = atab[o + 1];
if (mat == 0xFFFFFFFFu) continue;
if ((fl & 1u) != 0u && (fy < uintBitsToFloat(atab[o + 6]) || fy > uintBitsToFloat(atab[o + 7]))) continue;
float vv = v;
if ((fl & 2u) != 0u) vv += simplex70(fx * uintBitsToFloat(atab[o + 4]), fy * uintBitsToFloat(atab[o + 5]));
if (!(vv >= uintBitsToFloat(atab[o + 2]) && vv < uintBitsToFloat(atab[o + 3]))) continue;
if ((fl & 4u) == 0u) return mat;
float px = uintBitsToFloat(atab[o + 10]) + fx * uintBitsToFloat(atab[o + 8]);
float py = uintBitsToFloat(atab[o + 11]) + fy * uintBitsToFloat(atab[o + 9]);
float rmin = uintBitsToFloat(atab[o + 12]), rmax = uintBitsToFloat(atab[o + 13]);
if ((fl & 8u) != 0u) {
float n = (fl & 16u) != 0u ? fbm70(px, py) : simplex70(px, py);
if (rmin >= n || n > rmax) continue;
}
if ((fl & 32u) == 0u) return mat;
float n2 = polka(px, py, uintBitsToFloat(atab[o + 14]), uintBitsToFloat(atab[o + 15]), (fl & 64u) != 0u, uintBitsToFloat(atab[o + 16]));
if (rmin < n2 && n2 <= rmax) return mat;
}
return 0u;
}
uint eval_materials(int a, float v, ivec2 w) { return eval_materials_at(arec(a, 14), arec(a, 15), v, w); }
// ---- procedural biomes (FUN_0087e110): gradient between the biome's y limits, inside perlin, noise-type blend; the BitmapCaves image is not generated (value 1 everywhere)
float sin_big(float t) { // sin of a large float: reduce by 2*pi in two steps first (the GPU sin loses its accuracy for large arguments)
float k = floor(t * 0.15915494309189535 + 0.5);
float r = fma(-k, 6.28125, t);
r = fma(-k, 0.0019353071795864769, r);
return sin(r);
}
float iq_hash(float n) { return fract(sin_big(n) * 43758.547); }
float iq_mix(float n, float sx, float sy) {
float a = iq_hash(n), b = iq_hash(n + 1.0), c = iq_hash(n + 57.0), d = iq_hash(n + 58.0);
return mix(mix(a, b, sx), mix(c, d, sx), sy);
}
float iq_noise(float x, float y) { // FUN_00871630
float ix = floor(x), iy = floor(y);
float fx = x - ix, fy = y - iy;
return iq_mix(iy * 57.0 + ix, (3.0 - 2.0 * fx) * fx * fx, (3.0 - 2.0 * fy) * fy * fy);
}
float iq_smooth_par(float f, int i) { return (i % 2 == 0) ? (3.0 - 2.0 * f) * f * f : f; }
float iq_noise2(float x, float y) { // FUN_00871850: the interpolation weight depends on the parity of the cell
float ix = floor(x), iy = floor(y);
float fx = x - ix, fy = y - iy;
int jx = int(ix), jy = int(iy);
float sx0 = iq_smooth_par(fx, jx), sx1 = iq_smooth_par(fx, jx + 1);
float sy0 = iq_smooth_par(fy, jy), sy1 = iq_smooth_par(fy, jy + 1);
return iq_mix(iy * 57.0 + ix, (sx1 - sx0) * fx + sx0, (sy1 - sy0) * fy + sy0);
}
float noise_t(int t, float x, float y) { // FUN_00873c00 (types 0-4 are not used by any biome of the game)
if (t == 5) return simplex70(x, y);
if (t == 6) return perlin2(x, y);
if (t == 7) return value2(x, y);
if (t == 8) return simplex2(x, y);
return 0.0;
}
float fbm_t(int t, float x, float y) { // FUN_00873cc0
const vec4 M = vec4(0.84147, 0.5403, 0.5403, -0.84147);
const float K[3] = float[3](2.02, 2.33, 2.01);
float sum = noise_t(t, x, y) * 0.5, wgt = 0.25;
for (int o = 0; o < 3; ++o) {
float nx = (M.x * x + M.z * y) * K[o], ny = (M.y * x + M.w * y) * K[o];
x = nx; y = ny;
sum += noise_t(t, x, y) * wgt;
wgt *= 0.5;
}
return sum / 0.9375;
}
float inside_perlin(uint rec, float X, float Y) { // FUN_0087e7a0
uint fl = atab[rec];
float x = X * 0.05243442 * tf(rec, 1) + 0.1 + tf(rec, 5);
float y = Y * 0.05243442 * tf(rec, 2) + 0.1 + tf(rec, 6);
if ((fl & 64u) != 0u) { float sd = float(atab[5]); x += sd * 1.312e-5; y += sd * 2.331e-5; }
int it = int((fl >> 12) & 15u);
float n = (fl & 2u) != 0u ? fbm_t(it, x, y) : noise_t(it, x, y);
float v = n;
if ((fl & 4u) != 0u) v = n * n;
float mn = tf(rec, 3), mx = tf(rec, 4);
if ((fl & 8u) != 0u) { if (!(mx > v)) v = mx; if (!(v > mn)) v = mn; }
if ((fl & 16u) != 0u) v = (v * 0.5 + 0.5) * (mx - mn) + mn;
return v;
}
float cave_at(uint base, int cw, int ch, int x, int y) { // the cave image: both axes wrap
x = ((x % cw) + cw) % cw; y = ((y % ch) + ch) % ch;
return field[base + uint(y * cw + x)];
}
float cave_sample(uint base, int cw, int ch, float xf, float yf) { // FUN_00870e60
int ix = int(floor(xf)), iy = int(floor(yf));
float fx = xf - float(ix), fy = yf - float(iy);
float sx = (3.0 - fx * 2.0) * (fx * fx), sy = (3.0 - fy * 2.0) * (fy * fy);
float a = cave_at(base, cw, ch, ix, iy), b = cave_at(base, cw, ch, ix + 1, iy), c = cave_at(base, cw, ch, ix, iy + 1), d = cave_at(base, cw, ch, ix + 1, iy + 1);
float top = (b - a) * sx + a, bot = (d - c) * sx + c;
return (bot - top) * sy + top;
}
float proc_value(ivec2 w, uint rec) {
float X = float(w.x), Y = float(w.y);
float v = 1.0;
if (atab[rec + 18u] != 0xFFFFFFFFu) { // the cave image of the biome (see proc_biome.cpp)
uint cw = atab[rec + 19u] & 0xFFFFu, chh = atab[rec + 19u] >> 16;
float px = (X * 0.49162514 + 2560.0) * 0.1, py = (2560.0 * 6.86035959282328e-07 + Y * 0.49162514) * 0.1;
v = cave_sample(atab[rec + 18u], int(cw), int(chh), px, py);
if (1.0 > v) v = v + simplex70(px, py) * ((1.0 - v) * 0.495);
if (0.0 >= v) return 0.0;
}
v = proc_gradient(w, rec) * v;
float s8 = 0.0, s18 = 0.0, s1c = 0.0;
float mulp = tf(rec, 9);
uint fl = atab[rec];
if (v > 0.51 && mulp != 0.0) { s18 = (v - 0.5) * 2.0; s8 = inside_perlin(rec, X, Y); }
if (v > 0.85) {
s1c = tf(rec, 7);
float nz;
if (((fl >> 8) & 15u) == 3u) { // SIN_CAPPED_SIMPLEX
float amp = iq_noise(X * 0.02, Y * 0.021) * 0.05 + 0.05;
float t1 = sin(Y * 0.0009765625) * 1024.0 * 0.5 * 0.06 * amp, t2 = cos(X * 0.0009765625) * 1024.0 * 0.5 * 0.06 * amp;
nz = float(simplex70(t2, t1)) * v;
} else { // the default noise type
float amp = iq_noise2(X * 0.025, Y * 0.025) * 0.05 + 0.05;
nz = simplex2(X * 0.01 * amp, Y * 0.01 * amp) * v;
}
v = v + (nz - v) * ((v - 0.85) / 0.1);
}
if ((fl & 32u) != 0u) s8 = ((v * s8) - s8) * (Y * 0.001) + s8;
return mulp * s18 * s8 + tf(rec, 8) * v + s1c;
}
// Probe mode (shift_mode 2, docs: the teleport destination test): the world cells listed in probe[] get their cell word: from the live window when inside it
// (flag 4, flag 2 when the cell is falling: ext.x > 0), else generated as the window would generate them (flag 1 = an area is loaded there).
layout(std430, set = 0, binding = 53) buffer Probe { uvec4 phead; ivec4 probe[]; };
bool g_unloaded = false;
uint gen_cell(ivec2 w) {
uint hh = hash3(uint(w.x), uint(w.y), 77u);
uint var = (hh >> 8) & 0xFFu;
// LoadPixelScene images override the terrain (the scenes of every loaded area: a scene may reach into a neighbour)
bool forceSolid = false;
int whiteTile = -1; // the biome-map tile whose biome the white pixels of the scene use (the scene's origin)
for (int a = 0; a < int(atab[0]); ++a) {
int si = scene_index(a, w);
if (si < 0) continue;
uint sv = scene[si];
if (sv >= 0xE000u) { forceSolid = true; whiteTile = sv == 0xFFFFu ? -1 : int(sv - 0xE000u); break; }
if (sv != 0u) { return (sv == 1u ? 0u : ((sv - 1u) | (var << 16))); }
}
const int ai = area_of_cell(w);
if (ai < 0) { g_unloaded = true; return uint(pc.m_rock) | (var << 16); } // no area loaded here
if (forceSolid) { // a white scene pixel: solid, the material from the <Materials> of the biome with the wang value 1
float xw = float(w.x), yw = float(w.y);
float n3 = value2(xw * 0.035, yw * 0.07) * 15.5;
float s = simplex2((xw + n3) * 0.0489275, (yw + n3) * 0.0489275);
const float wv = max(0.5, s * (0.0625 * 5.35 * 0.95) + 1.0);
uint fm;
const uint wrec = whiteTile >= 0 ? tile_rec(whiteTile % int(atab[2]), whiteTile / int(atab[2])) : 0xFFFFFFFFu;
if (wrec != 0xFFFFFFFFu && atab[wrec + 22u] != 0u) fm = eval_materials_at(int(atab[wrec + 21u]), int(atab[wrec + 22u]), wv, w); // the biome of the scene origin
else fm = eval_materials(ai, wv, w);
return fm == 0u ? 0u : (fm | (var << 16));
}
{ // a procedural biome (no wang template): the value function, then the <Materials> list (FUN_0087d0e0, type 0)
const uint trec = topo_rec(w);
if (trec != 0xFFFFFFFFu && (atab[trec] & 1u) != 0u) {
const uint pm = eval_materials(ai, proc_value(w, trec), w);
return pm == 0u ? 0u : (pm | (var << 16));
}
}
gpw = arec(ai, 2); gph = arec(ai, 3); gfo = arec(ai, 4); gmo = arec(ai, 5);
ivec2 o = w - ivec2(arec(ai, 0), arec(ai, 1)); // cell relative to the centre of wang pixel (0,0)
float px = (float(o.x) + 0.5) * 0.1, py = (float(o.y) + 0.5) * 0.1;
if (py < -0.5) { return 0u; } // open sky above the area
// the last wang pixel is 5 cells short of the right / bottom edge of the area's tiles: those cells continue the edge pixel (a 13 cell wall of rock stood there)
px = clamp(px, -0.5, float(gpw) - 0.5001); py = clamp(py, -0.5, float(gph) - 0.5001);
float warp = 1.0, thr = 0.5;
int m0 = material_pixel(px, py);
if (m0 > 0) { vec2 mp = mparams[m0]; warp = mp.x; thr = mp.y; }
float qx = px, qy = py;
if (warp > 0.0) {
float xf = float(o.x), yf = float(o.y);
float n1 = simplex2(xf * 0.13715, yf * 0.13717);
float a = n1 * 0.45 + 0.1;
float xs = xf * 0.11111111, ys = yf * 0.11111111;
float n2 = perlin2(ys, xs);
float t = ((1.0 - a) * 0.33 + 0.111) * warp;
float n3 = value2(xs, ys);
qx = px + n3 * (a * warp);
qy = py + n2 * t;
}
float f = bilinear(qx, qy);
if (f < thr) { return 0u; }
int m1 = material_pixel(qx, qy);
uint mat;
if (m1 > 0) {
mat = uint(m1);
} else {
// the biome's <Materials>: FUN_0087d010 value, then the first matching component (none = air)
float xw = float(w.x), yw = float(w.y);
float n = value2(xw * 0.035, yw * 0.07);
float n3 = n * 15.5;
float h = (f - 0.5) * 0.5;
float s = simplex2((xw + n3) * 0.0489275, (yw + n3) * 0.0489275);
float v = max(0.5, s * (h * h * 5.35 * 0.95) + f);
mat = eval_materials(ai, v, w);
}
return mat == 0u ? 0u : (mat | (var << 16));
}
// The world rules of the cells that enter the window: the header in the tables before the 4 colour-grading words (main.cpp kWorldRules): [0] EVERYTHING_TO_GOLD,
// [1] gold, [2] gold_static; the cuts in binding 55: [0] count, then from word 4, 12 words per GameCutThroughWorldVertical (x, y_min, y_max, radius, edge, the engine's
// loaded region at the call: x0, y0, x1, y1, seed), every cut of the run. A generated cell (one that never was in the window) inside a cut's region takes the cut (007392d0: off = floor(3 sin(0.02 y)
// +- 0.5), air for left < x <= right, the static stainable cells within edge darkened by (1 - sqrt(d / edge)) x (0.4 + 0.3 rand), as the burn level of bits 28-30);
// with EVERYTHING_TO_GOLD every non-air cell is gold (the static ones gold_static), as the engine's redefined materials (00704f70)
const uint kWorldRules = 4u;
layout(std430, set = 0, binding = 55) readonly buffer CutRules { uint cuts[]; };
uint cut_hash(uint a, uint b) { uint h = a * 0x9E3779B1u ^ (b + 0x7F4A7C15u); h ^= h >> 15; h *= 0x2C1B3C6Du; h ^= h >> 12; h *= 0x297A2D39u; h ^= h >> 15; return h; }
uint rules_word(uint c, ivec2 w, bool fresh) {
const uint wb = uint(perm.length()) - 4u - kWorldRules;
if (fresh) {
const uint n = min(cuts[0], (uint(cuts.length()) - 4u) / 12u);
for (uint k = 0u; k < n; ++k) {
const uint b = 4u + 12u * k;
const int x = int(cuts[b]), y0 = int(cuts[b + 1u]), y1 = int(cuts[b + 2u]), r = int(cuts[b + 3u]), edge = int(cuts[b + 4u]);
if (w.y < y0 || w.y >= y1 || w.x < x - r - edge || w.x >= x + r + edge) continue;
if (w.x < int(cuts[b + 5u]) || w.y < int(cuts[b + 6u]) || w.x >= int(cuts[b + 7u]) || w.y >= int(cuts[b + 8u])) continue;
const float v = sin(float(w.y) * 0.02) * 3.0;
const int off = int(floor(v + (v < 0.0 ? -0.5 : 0.5)));
const int left = x - r + off, right = x + r + off;
if (w.x > left && w.x <= right) { c = 0u; continue; }
const uint m = cell_mat(c);
if (edge <= 0 || m == 0u || (mflags(m) & F_STATIC) == 0u || (mflags(m) & F_NOSTAIN) != 0u || (c & BURN_BIT) != 0u) continue;
const float rnd = 0.4 + 0.3 * float(cut_hash(uint(w.y), cuts[b + 9u]) & 0xFFFFu) / 65536.0;
const float d = float(w.x <= left ? left - w.x : w.x - right);
const float s = max(0.0, (1.0 - sqrt(d / float(edge))) * rnd);
const float keep = (1.0 - float((c >> 28) & 7u) / 7.0) * (1.0 - s);
const uint L = uint(clamp(int(round((1.0 - keep) * 7.0)), 0, 7));
c = (c & ~(7u << 28)) | (L << 28);
}
}
if (perm[wb] != 0u) {
const uint m = cell_mat(c);
if (m != 0u && m != perm[wb + 1u] && m != perm[wb + 2u]) c = ((mflags(m) & F_STATIC) != 0u ? perm[wb + 2u] : perm[wb + 1u]) | (c & (0xFFu << 16));
}
return c;
}
// The spell test room (main.cpp --spelltest; cut rules words 1..3): every generated cell is rock, but the hall (x0 <= x < x1, y0 <= y < y1) is air inside 16 cells of
// temple brick
uint spell_room(uint c, ivec2 w) {
if (cuts[1] == 0u) return c;
const ivec2 a = ivec2(int(cuts[2] << 16) >> 16, int(cuts[2]) >> 16), b = ivec2(int(cuts[3] << 16) >> 16, int(cuts[3]) >> 16);
const uint var = (cut_hash(uint(w.x), uint(w.y)) >> 8) & 0xFFu;
if (w.x >= a.x && w.x < b.x && w.y >= a.y && w.y < b.y) return 0u;
if (w.x >= a.x - 16 && w.x < b.x + 16 && w.y >= a.y - 16 && w.y < b.y + 16) return (cuts[1] >> 16) | (var << 16);
return (cuts[1] & 0xFFFFu) | (var << 16);
}
void world_rules(int i, ivec2 w, bool fresh) { const uint c = rules_word(cells[i], w, fresh); if (c != cells[i]) { cells[i] = c; ext[i] = vec2(0.0); } }
void main() {
if (pc.shift_mode == 3) { // far blocks (far.glsl): job z generates its block's cells into its slot - a store-backed block keeps the cells the store had
const int j = int(gl_GlobalInvocationID.z);
if (j >= farmap[kFarJobCount]) return;
const int slot = farmap[kFarJobList + 4 * j], bx = farmap[kFarJobList + 4 * j + 1], by = farmap[kFarJobList + 4 * j + 2], kept = farmap[kFarJobList + 4 * j + 3];
const ivec2 w = ivec2(bx * 64, by * 64) + ivec2(gl_GlobalInvocationID.xy);
const int fi = slot * 4096 + (w.y & 63) * 64 + (w.x & 63);
if (kept != 0 && farcells[fi] != 0xFFFFFFFFu) return;
farcells[fi] = rules_word(spell_room(gen_cell(w), w), w, true);
return;
}
if (pc.shift_mode == 2) {
const uint q = gl_GlobalInvocationID.y * gl_NumWorkGroups.x * 8u + gl_GlobalInvocationID.x;
if (q >= phead.x) return;
const ivec2 w = probe[q].xy;
const ivec2 lp = w - ivec2(pc.view_x, pc.view_y);
if (lp.x >= 0 && lp.y >= 0 && lp.x < pc.gw && lp.y < pc.gh) {
const int li = lp.y * pc.gw + lp.x;
probe[q].z = int(cells[li]);
probe[q].w = 1 | 4 | (ext[li].x > 0.0 ? 2 : 0);
return;
}
g_unloaded = false;
probe[q].z = int(rules_word(spell_room(gen_cell(w), w), w, true));
probe[q].w = g_unloaded ? 0 : 1;
return;
}
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= pc.gw || p.y >= pc.gh) return;
int i = p.y * pc.gw + p.x;
if (pc.shift_mode == 1) { // scrolling: reuse the simulated overlap, generate only the exposed strips
ivec2 src = p + ivec2(pc.shift_x, pc.shift_y);
if (src.x >= 0 && src.y >= 0 && src.x < pc.gw && src.y < pc.gh) {
cells[i] = cells_old[src.y * pc.gw + src.x];
ext[i] = ext_old[src.y * pc.gw + src.x];
birth[i] = birth_old[src.y * pc.gw + src.x]; // a cell that stays keeps its age (0070d1e0: the age belongs to the cell)
return;
}
}
birth[i] = birth_frame();
if (ovr[i] != 0xFFFFFFFFu) { // a cell that comes back from the store (only its word is kept): a burning cell or a flame gets the fade a new cell of its material has
// (0070f450: the material's temperature_of_fire), so a fire that went out of the window goes on when it comes back instead of dying at its next update
const uint oc = ovr[i];
cells[i] = oc; ext[i] = vec2(0.0);
const uint om = cell_mat(oc);
if (om != 0u && ((oc & BURN_BIT) != 0u || (mflags(om) & F_FIRE) != 0u)) birth[i] = with_fade(birth_frame(), int((mats[3u * om + 2u].w >> 8) & 255u));
world_rules(i, p + ivec2(pc.view_x, pc.view_y), false);
return;
}
ivec2 w = p + ivec2(pc.view_x, pc.view_y); // world cell
ext[i] = vec2(0.0);
cells[i] = spell_room(gen_cell(w), w);
world_rules(i, w, true);
}
+224
View File
@@ -0,0 +1,224 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "common.glsl"
// VerletPhysicsComponent chains (the vines and ropes of the caves): one invocation per chain root. The root entity has ex.w = 777, ex.x = number of points, ex.y = the
// first part entity (the parts are consecutive entity slots, one per point; the point is the part's position, its previous position is kept in a.zw), ex.z = resting
// distance, mv = (velocity dampening, stiffness, gravity (0/1), wind (0/1)). The first point hangs on the surface above the root; the others are simulated with Verlet
// integration (the previous position gives the velocity), a distance constraint to the neighbour and the live cell grid as the collision.
struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; };
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
bool solid_at(ivec2 w) {
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u;
}
bool cell_present(ivec2 w) {
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh && cell_mat(cells[p.y * pc.gw + p.x]) != 0u;
}
bool blocking_at(ivec2 w) { // a non-null cell whose type is solid (virtual +2c = 1; CLiquidCell 0070b0b0: a static powder - the rocks are liquid_static liquid_sand cells; CSolidCell)
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && (mflags(m) & F_STATIC) != 0u && ((mflags(m) & F_POWDER) != 0u || (mflags(m) & F_LIQCELL) == 0u);
}
bool stain_here(ivec2 w) {
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && (mflags(m) & F_STAINS) != 0u;
}
bool liquid_here(ivec2 w) {
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) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && (mflags(m) & F_LIQCELL) != 0u && (mflags(m) & F_POWDER) == 0u;
}
bool in_window(vec2 w, float margin) {
vec2 p = w - vec2(pc.view_x, pc.view_y);
return p.x > margin && p.y > margin && p.x < float(pc.gw) - margin && p.y < float(pc.gh) - margin;
}
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= uint(ctrl.inp.w)) return;
Ent root = ents[id];
if (root.ex.w != 777.0) return;
const int n = int(root.ex.x), first = int(root.ex.y);
if ((root.st.w & 8) != 0) { // the chain was removed (its area is gone): so are its points
for (int k = 0; k < n; ++k) ents[uint(first + k)].st.w |= 8;
return;
}
const float rest = root.ex.z, damp = root.mv.x, stiff = root.mv.y;
if (!in_window(root.a.xy, 24.0)) return;
// a new chain waits 15 frames in the window before its surface search and its joints' first look at the cells: after a teleport the pixel scenes of the new area
// reach the cells a few frames after the terrain (the holy mountain's brick over the generated rock), and the ropes searched the bare rock, or remembered it and broke
if (root.hp.y == 0.0 && root.hp.z < 15.0) { ents[id].hp.z = root.hp.z + 1.0; return; }
vec2 anchor = root.a.xy;
const int jtype = int(root.ex4.x); // MoveToSurfaceOnCreateComponent: 0 the surface above, 1 VERLET_ROPE_ONE_JOINT, 2 VERLET_ROPE_TWO_JOINTS
if (root.hp.y == 0.0) {
if (jtype == 0 || jtype == 3) { // MoveToSurfaceOnCreate (3: the hair of a limb boss, its first point on the root entity the host moves): the first point goes to the surface above the spawn point (the cell below the first solid one)
ivec2 c = ivec2(floor(anchor));
if (jtype == 0 && !solid_at(c)) {
for (int k = 1; k <= 24; ++k) if (solid_at(c - ivec2(0, k))) { anchor = vec2(anchor.x, float(c.y - k + 1)); break; }
}
ents[id].a.xy = anchor;
for (int k = 0; k < n; ++k) { // hang straight down from the anchor
vec2 p = anchor + vec2(0.0, float(k) * rest);
ents[uint(first + k)].a = vec4(p, p);
}
} else if (jtype == 3) { // VerletWorldJointComponent (load_verlet_rope_with_*_joint*): the joints are given (ex2, ex7.z), nothing is searched
} else {
// 00bc3490 / 00bc2fa0 (called with first-hit and the upward fan): each try casts ray_count rays of length lookup_radius from the spawn point at random angles within
// +-1.2566371 (0x3fa0d97c) of straight up, the first ray that hits gives the joint: the hit cell itself (offset_from_surface only moves the type-0 entity).
// TWO_JOINTS: both joints are searched again until the second is at least verlet_min_joint_distance from the first; after 11 tries the joints are dropped (the rope is free).
const float R = root.ex4.y, mind = root.ex4.w;
const int rays = max(int(root.ex6.x), 1);
vec2 A = vec2(0.0), B = vec2(0.0);
bool gotA = false, gotB = false;
for (int attempt = 0; attempt < 11 && !(jtype == 1 ? gotA : gotB); ++attempt) {
gotA = false; gotB = false;
for (int which = 0; which < 2; ++which) {
if (which == 1 && (jtype != 2 || !gotA)) continue;
vec2 pt = vec2(0.0);
bool hit = false;
for (int r = 0; r < rays && !hit; ++r) {
const float ang = float(hash3(id, uint(attempt * 8 + which * 4 + r), 4242u) & 0x7FFFu) / 32767.0 * 2.5132742 - 1.2566371;
const vec2 dir = vec2(sin(ang), -cos(ang));
for (float t = 1.0; t <= R; t += 1.0) {
const ivec2 c = ivec2(floor(root.a.xy + dir * t));
if (solid_at(c)) { pt = vec2(c); hit = true; break; }
}
}
if (!hit) break;
if (which == 0) { A = pt; gotA = true; }
else if (distance(pt, A) >= mind) { B = pt; gotB = true; }
}
}
if (jtype == 2 && !gotB) gotA = false; // (no pair: both joints go)
if (!gotA) { // 00bc3490: after 11 tries without its joint(s) the components go and the entity is killed (0044df60): no rope falls from the ceiling
ents[id].st.w |= 8;
for (int k = 0; k < n; ++k) ents[uint(first + k)].st.w |= 8;
return;
}
float flags0 = 0.0;
if (gotA) flags0 += 1.0;
if (gotB) flags0 += 2.0;
ents[id].ex2 = vec4(A, B);
ents[id].ex7.z = flags0;
}
if (jtype == 1 || jtype == 2) {
const int fl = int(ents[id].ex7.z);
const bool gotA = (fl & 1) != 0, gotB = (fl & 2) != 0;
const vec2 A = ents[id].ex2.xy, B = ents[id].ex2.zw;
const vec2 P0 = gotA ? A : (gotB ? B - vec2(0.0, float(n - 1) * rest) : root.a.xy);
ents[id].a.xy = P0;
const float L = float(n - 1) * rest;
for (int k = 0; k < n; ++k) { // 00c39fd0: the points start on a horizontal line from the entity position, resting_distance apart; the joints then move their points (first, last)
vec2 p = root.a.xy + vec2(float(k) * rest, 0.0);
if (k == 0 && gotA) p = A;
if (gotB) { // two joints: started where the rope settles (along the chord with the sag of its length) so that no point begins inside the rock beside the spawn point
const float d = max(distance(A, B), 1.0);
const float sag = L > d ? sqrt(3.0 * d * (L - d) / 8.0) : 0.0;
const float u = float(k) / float(max(n - 1, 1));
p = mix(A, B, u) + vec2(0.0, 4.0 * sag * u * (1.0 - u));
}
ents[uint(first + k)].a = vec4(p, p);
}
}
ents[id].hp.y = 1.0;
}
// the pins: ex7.z = which joints exist (1 first point, 2 last point; -1 none left); jtype 0 and a type 3 without data (a limb boss's hair) pin the first point
int pinFlags = ents[id].ex7.z < 0.0 ? 0 : (jtype == 0 || jtype == 3) && ents[id].ex7.z == 0.0 ? 1 : int(ents[id].ex7.z);
if (jtype == 1 || jtype == 2 || (jtype == 3 && ents[id].ex7.z > 0.0 && (pinFlags & 1) != 0)) anchor = ents[id].ex2.xy;
{ // VerletWorldJointSystem (00c40b90): a joint remembers the cell at its world position on its first update (none there: it never breaks); when another cell or none is
// there later, it breaks: the component goes and the rope's damping becomes 0.97 (and a "joint_break" sound, not played here). ex6.z / ex6.w: the remembered material
// + 1 of the first / last point's joint, -1 for air, 0 not yet seen
bool broke = false;
for (int j = 0; j < 2; ++j) {
if ((pinFlags & (1 << j)) == 0) continue;
const vec2 jw = j == 0 ? anchor : ents[id].ex2.zw;
const ivec2 q = ivec2(floor(jw)) - 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 m = cell_mat(cells[q.y * pc.gw + q.x]);
const float rec = j == 0 ? ents[id].ex6.z : ents[id].ex6.w;
if (rec == 0.0) { if (j == 0) ents[id].ex6.z = m == 0u ? -1.0 : float(m + 1u); else ents[id].ex6.w = m == 0u ? -1.0 : float(m + 1u); }
else if (rec > 0.0 && float(m + 1u) != rec) { pinFlags &= ~(1 << j); broke = true; }
}
if (broke) { ents[id].ex7.z = pinFlags == 0 ? -1.0 : float(pinFlags); ents[id].mv.x = 0.97; }
}
const bool pinA = (pinFlags & 1) != 0, pinB = (pinFlags & 2) != 0;
vec2 pts[40], prv[40];
for (int k = 0; k < n && k < 40; ++k) { vec4 a = ents[uint(first + k)].a; pts[k] = a.xy; prv[k] = a.zw; }
const float t = float(ctrl.inp2.x);
float massOf[40]; // the random mass of every point between mass_min and mass_max (00c39fd0); it weights the relaxation, the forces are proportional to it
for (int k = 0; k < n && k < 40; ++k) massOf[k] = root.ex5.y + (root.ex5.z - root.ex5.y) * float(hash3(id, uint(k), 77u) & 0xFFFFu) / 65536.0;
const float T = root.ex5.x * t / 60.0; // wind_change_speed x time in seconds
// wind (docs/EXPLOSION_KNOCKBACK_AND_VINES.md section 3): WorldStateComponent.wind (wind_speed x dt) times the sky exposure of the first point (00746620: the 2048 x 2048 map of
// 64-pixel cells, 1 in the open and 0 a few cells under the biomes' ground; the earlier "x num_points" was the return of that lookup misread), set by the host in ex6.y
const float W = root.ex6.y;
const float Aw = sin(25.0 * T), Bw = sin(0.4 * T), Pw = 0.5 * (sin(T) + 1.0) + 0.5, Gw = 5.0 * abs(W) + 1.0;
for (int k = 0; k < n && k < 40; ++k) {
// 00c37d30: new = cur + (cur - prev) * damping + 0.5 F / m dt^2 (dt = 1/60); a point whose stored liquid material is set damps by 0.85
const bool wet = ents[uint(first + k)].hp.x > 0.5; // the point carries a stored liquid material
vec2 v = (pts[k] - prv[k]) * (wet ? 0.85 : damp);
vec2 F = vec2(0.0); // force per unit mass
if (root.mv.z > 0.5) F.y += 600.0; // 00c379e0: mass x 600
if (root.mv.w > 0.5) { // 00c37b10
F.x += 360.0 * W * Pw + 100.0 * sin(5.0 * pts[k].x) * Aw;
F.y += 20.0 * sin(5.0 * pts[k].y) * Aw + 60.0 * sin(0.005 * pts[k].y) * Bw * Gw;
}
const vec2 delta = v + F * 0.5 / 3600.0;
vec2 cand = pts[k] + delta;
// the terrain (00c37d30): probe (truncated x, ceil y); a free cell takes the candidate (a liquid one only liquid_damping of the move); a blocking one halves the move once; if the midpoint still
// blocks, the cell above it decides: free = one pixel up, otherwise the point stays and its velocity is gone
vec2 np = cand;
if (blocking_at(ivec2(int(cand.x), int(ceil(cand.y))))) {
const vec2 mid = (pts[k] + cand) * 0.5;
if (!blocking_at(ivec2(int(mid.x), int(ceil(mid.y))))) np = mid;
else if (!blocking_at(ivec2(int(mid.x), int(ceil(mid.y)) - 1)) && cell_present(ivec2(int(mid.x), int(ceil(mid.y)) - 1))) { np = pts[k] + vec2(0.0, -1.0); prv[k] = np; pts[k] = np; continue; }
else { prv[k] = pts[k]; continue; }
} else if (liquid_here(ivec2(int(cand.x), int(ceil(cand.y))))) {
np = pts[k] + root.ex5.w * delta;
// 00c37d30: the liquid's material is sampled into the point on 6 of 10 points, else with a 1 in 30 draw (only liquids that stain)
if (stain_here(ivec2(int(cand.x), int(ceil(cand.y)))) && (k % 10 < 6 || hash3(id, uint(k), uint(pc.frame)) % 30u == 0u)) ents[uint(first + k)].hp.x = 1.0;
}
prv[k] = pts[k];
pts[k] = np;
}
for (int it = 0; it < 2; ++it) // 00c38310 runs twice over the links: the chain is soft, it stretches under its own weight
for (int k = 0; k + 1 < n && k + 1 < 40; ++k) { // distance constraints; the joints' points move like the others here and are put back afterwards (00c3a610)
vec2 d = pts[k + 1] - pts[k];
float l = length(d);
if (l < 1e-5) continue;
float diff = (l - rest) / max(l, 0.01) * stiff * 0.5; // 00c3a610 passes stiffness x 0.5 (0x3f000000) to 00c38310; links shorter than 0.01 count as 0.01
const float wa = 1.0 / massOf[k], wb = 1.0 / massOf[k + 1], sa = wa / (wa + wb); // the share of the correction by the inverse masses
pts[k] += d * diff * sa; pts[k + 1] -= d * diff * (1.0 - sa);
}
if (pinA) pts[0] = anchor; // the world joints put their points back after the relaxation (00c3a610)
if (pinB) pts[n - 1] = ents[id].ex2.zw;
// the animation (00c385a0, after the relaxation when animation_target_offset is not 0 - the tentacler's tentacles, offset (0, 1)): every link pulls its far point toward
// near point + offset + the offset's direction turned by 90 degrees x sin((link + time) x animation_speed) x animation_amount, by animation_energy shared by the inverse masses
// (root ex17 = offset, amount, speed; ex18.x = energy; the time in seconds)
if (root.ex17.x != 0.0 || root.ex17.y != 0.0) {
const vec2 D = root.ex17.xy, u = normalize(D), perp = vec2(-u.y, u.x);
for (int k = 0; k + 1 < n && k + 1 < 40; ++k) {
const float w = sin((float(k) + t / 60.0) * root.ex17.w) * root.ex17.z;
const vec2 delta = pts[k] + D + perp * w - pts[k + 1];
const float wa = 1.0 / massOf[k], wb = 1.0 / massOf[k + 1];
pts[k + 1] += delta * (root.ex18.x * wb / (wa + wb));
}
}
for (int k = 0; k < n && k < 40; ++k) {
ents[uint(first + k)].a = vec4(pts[k], prv[k]);
const vec2 d = k + 1 < n ? pts[k + 1] - pts[k] : pts[k] - pts[max(k - 1, 0)];
ents[uint(first + k)].ex7.x = atan(d.y, d.x); // the horizontal part sprite is turned along the chain
}
}