#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 } }