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

+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;
}
}
}