Also fixed: * flying wands not pathing properly. * Lantern particles offset * Wand fire offset * Water spray from flask
266 lines
16 KiB
Plaintext
266 lines
16 KiB
Plaintext
#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))) { release_part(id); 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) { release_part(id); 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 bool spray = (p.c.w & 0x200000u) != 0u;
|
|
const float g = spray ? PART_GRAV : 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 && !(spray && kind == 1u)) { dead = true; break; }
|
|
if ((f & F_LIQUID) != 0u || (spray && (f & F_STATIC) != 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;
|
|
} else if (spray) kind = 2u;
|
|
} else if (spray) kind = 2u; // 00712390: leaving material ends the mode-1 penetration.
|
|
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 && !(spray && kind == 1u)) dead = true;
|
|
else if ((f & F_LIQUID) != 0u || (spray && (f & F_STATIC) != 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;
|
|
}
|
|
} else if (spray) kind = 2u;
|
|
} else if (spray) kind = 2u;
|
|
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))) { release_part(id); 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 & 0x300000u) | ((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 || p.b.x >= 2.9e38) return; // Allocation may still be initializing this record.
|
|
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);
|
|
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].c = uvec4(cw, 3u, 0u, 0u);
|
|
fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0);
|
|
} else release_part(id);
|
|
return;
|
|
}
|
|
if (p.b.x <= 0.0) release_part(id);
|
|
else fparts[id].b.x = p.b.x;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|