SOURCE / PINNED RELEASE
Made of little things.
Safe Sand · Material studies
- Release
- 9101a9cd9fb4…
- Author-recorded commit
- 75d98e1b8edb…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-7682e55a791
Archive hash verified: 6c8ddabeb31100e1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Adapted from av/safe-sand, e27bafa. See THIRD_PARTY.md for provenance.
import { HEADER, COMMON } from './glsl-common.js';
export const QUAD_VS = HEADER + `
const vec2 P[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
void main() { gl_Position = vec4(P[gl_VertexID], 0.0, 1.0); }
`;
export const SPLAT_VS = HEADER + COMMON + `
uniform sampler2D uA;
uniform sampler2D uB;
uniform int uTW;
uniform vec2 uWorld;
uniform vec2 uGrid;
out vec4 vData;
out vec2 vCenter;
out float vRk;
void main() {
int id = gl_VertexID;
ivec2 pc = ivec2(id % uTW, id / uTW);
vec4 A = texelFetch(uA, pc, 0);
vec4 B = texelFetch(uB, pc, 0);
Grain g = grain(uint(id));
if (B.w < 0.5 || B.w > 1.0001 || B.x > g.r) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); gl_PointSize = 0.0; return; }
vec2 cell = uWorld / uGrid;
vec2 gc = A.xy / cell;
float rk = max(1.0, g.r * 1.3 / cell.x);
gl_PointSize = 2.0 * ceil(rk) + 1.0;
gl_Position = vec4(gc / uGrid * 2.0 - 1.0, 0.0, 1.0);
float V = 4.18879 * g.r * g.r * g.r;
float hc = V / (0.6 * cell.x * cell.y);
vData = vec4(hc, hc * A.z, hc * A.w, hc * length(A.zw));
vCenter = gc;
vRk = rk;
}
`;
export const SPLAT_FS = HEADER + `
in vec4 vData;
in vec2 vCenter;
in float vRk;
out vec4 o;
void main() {
vec2 d = abs(gl_FragCoord.xy - vCenter) / vRk;
float w = max(0.0, 1.0 - d.x) * max(0.0, 1.0 - d.y) / (vRk * vRk);
o = vData * w;
}
`;
export const GRID_FS = HEADER + `
uniform sampler2D uS;
layout(location = 0) out vec4 o0;
layout(location = 1) out vec4 o1;
void main() {
ivec2 c = ivec2(gl_FragCoord.xy);
ivec2 mx = textureSize(uS, 0) - 1;
vec4 b = vec4(0.0);
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++) {
float w = (x == 0 ? 2.0 : 1.0) * (y == 0 ? 2.0 : 1.0);
b += w * texelFetch(uS, clamp(c + ivec2(x, y), ivec2(0), mx), 0);
}
b /= 16.0;
vec2 vel = b.yz / max(b.x, 1e-4);
o0 = vec4(b.x, b.x, 0.0, 0.0);
o1 = vec4(vel, b.w / max(b.x, 1e-4), 0.0);
}
`;
export const HBLUR_FS = HEADER + `
uniform sampler2D uT;
uniform ivec2 uDir;
out vec4 o;
void main() {
ivec2 c = ivec2(gl_FragCoord.xy);
ivec2 mx = textureSize(uT, 0) - 1;
vec4 base = texelFetch(uT, c, 0);
float s = 0.0;
float W[5] = float[5](0.2270, 0.1945, 0.1216, 0.0540, 0.0162);
for (int k = -4; k <= 4; k++) {
ivec2 q = c + uDir * k;
ivec2 e = clamp(q, ivec2(0), mx);
float v = texelFetch(uT, e, 0).y;
if (q != e) v = max(2.0 * v - texelFetch(uT, clamp(2 * e - q, ivec2(0), mx), 0).y, 0.0);
s += W[abs(k)] * v;
}
o = vec4(base.x, s, base.zw);
}
`;
export const GRAD_FS = HEADER + `
uniform sampler2D uT;
uniform vec2 uCell;
out vec4 o;
void main() {
ivec2 c = ivec2(gl_FragCoord.xy);
ivec2 mx = textureSize(uT, 0) - 1;
float l = texelFetch(uT, clamp(c - ivec2(1, 0), ivec2(0), mx), 0).y;
float r = texelFetch(uT, clamp(c + ivec2(1, 0), ivec2(0), mx), 0).y;
float d = texelFetch(uT, clamp(c - ivec2(0, 1), ivec2(0), mx), 0).y;
float u = texelFetch(uT, clamp(c + ivec2(0, 1), ivec2(0), mx), 0).y;
vec4 base = texelFetch(uT, c, 0);
vec2 k = vec2(c.x > 0 && c.x < mx.x ? 2.0 : 1.0, c.y > 0 && c.y < mx.y ? 2.0 : 1.0);
o = vec4(base.x, (r - l) / (k.x * uCell.x), (u - d) / (k.y * uCell.y), base.y);
}
`;
export const STEP_FS = HEADER + COMMON + `
uniform sampler2D uA;
uniform sampler2D uB;
uniform sampler2D uG0;
uniform sampler2D uG1;
uniform int uTW;
uniform vec2 uWorld;
uniform float uDt;
uniform vec4 uGrav;
uniform vec4 uFA[5];
uniform vec4 uFB[5];
uniform uint uFrame;
uniform float uMagnet;
uniform float uTool;
uniform vec4 uSpawn;
uniform vec4 uSpawnConfig;
layout(location = 0) out vec4 oA;
layout(location = 1) out vec4 oB;
vec2 rdir(uint i, uint s) {
float a = 6.2831853 * rnd3(i, s, uFrame);
return vec2(cos(a), sin(a));
}
void main() {
ivec2 pc = ivec2(gl_FragCoord.xy);
uint idx = uint(pc.y * uTW + pc.x);
vec4 A = texelFetch(uA, pc, 0);
vec4 B = texelFetch(uB, pc, 0);
oA = A; oB = B;
float slot = mod(float(idx) - uSpawn.x + uSpawnConfig.w, uSpawnConfig.w);
if (slot < uSpawn.y) {
float angle = rnd3(idx, 40u, uFrame) * 6.2831853;
float radius = sqrt(rnd3(idx, 41u, uFrame)) * uSpawnConfig.x;
vec2 pos = clamp(uSpawn.zw + vec2(cos(angle), sin(angle)) * radius, vec2(0.5), uWorld - 0.5);
oA = vec4(pos, rdir(idx, 42u) * 4.0);
oB = vec4(uSpawnConfig.y, -45.0, rnd3(idx, 43u, uFrame), 1.0);
return;
}
if (B.w < 0.5) return;
if (B.w > 1.0) { oB.w = max(1.0, B.w - uDt); return; }
Grain g = grain(idx);
vec2 p = A.xy;
vec2 v = A.zw;
float zAir = B.x;
float vz = B.y;
float layer = B.z;
float N = uGrav.z;
vec2 gt = uGrav.xy;
vec2 uv = p / uWorld;
vec4 G0 = texture(uG0, uv);
vec4 G1 = texture(uG1, uv);
float h = G0.x;
vec2 grad = G0.yz;
vec2 vg = G1.xy;
float dt = uDt;
vec2 cs = uWorld / vec2(textureSize(uG0, 0));
float rk = max(1.0, g.r * 1.3 / cs.x);
float selfH = 4.18879 * g.r * g.r * g.r / (0.6 * cs.x * cs.y * rk * rk) * (0.172 + 0.85 * (rk - 1.0) / (rk + 0.9));
float contact = smoothstep(0.2, 0.8, max(h - selfH, 0.0) / max(2.0 * uSize.x, 0.05));
bool peb = g.kind == 2.0;
float glue = g.kind == 0.0 ? min(pow(0.1 / g.r, 3.0), 6.0) * (1.0 - smoothstep(0.12, 0.17, g.r)) * (rnd(idx, 22u) < 0.45 ? 1.0 : 1.0 - contact) : 0.0;
float adh = 9810.0 * glue;
float shake = smoothstep(1.5, 8.0, uGrav.w);
bool hang = zAir <= 0.0 && N + adh < -600.0;
if (hang && (shake > 0.85 || rnd3(idx, 26u, uFrame) < 1.0 - exp(-dt * mix(18.0, 5.0, contact)))) { zAir = 1e-3; vz = 0.0; hang = false; }
bool gb = zAir <= 0.0;
if (!gb) {
vz += -N * dt;
zAir += vz * dt;
v *= exp(-dt * 0.4);
v += gt * dt;
p += v * dt;
if (zAir <= 0.0) {
float impact = max(-vz, 0.0);
zAir = 0.0;
vz = 0.0;
v = v * mix(mix(0.35, 0.75, rnd3(idx, 12u, uFrame)), 0.15, contact) + rdir(idx, 13u) * impact * (peb ? 0.04 : mix(0.1, 0.015, contact)) * rnd3(idx, 14u, uFrame);
layer = 0.75 + 0.25 * rnd(idx, 15u);
}
if (zAir > 120.0 && vz > 0.0) { oB = vec4(zAir, vz, layer, 0.0); oA = vec4(p, v); return; }
} else {
float Ns = max(N, 5900.0);
float sp0 = length(v);
float mu = mix(g.mu, uFric.z * 0.85, contact * (peb ? 0.2 : 1.0));
if (sp0 < 0.8) mu *= 1.18;
vec2 gr = grad * (peb ? 1.4 : 1.0);
float gl = length(gr);
float marg = (uFric.z * 0.38 + uMaterial.y * 0.5) * contact;
float se = max(gl - marg * (1.0 - shake), 0.0);
vec2 drive = gt - (gl > 1e-5 ? gr / gl : vec2(0.0)) * Ns * se * contact;
float pic = 1.0 - exp(-dt * 70.0 * contact * (peb ? 0.25 : 1.0) * (1.0 - 0.8 * shake));
v = mix(v, vg, pic);
v *= exp(-dt * uMaterial.w * (0.2 + contact));
v += drive * dt + rdir(idx, 28u) * uGrav.w * 1800.0 * shake * contact * rnd3(idx, 29u, uFrame) * dt;
v *= exp(-dt * 45.0 * contact * min(max(gl - marg, 0.0), 8.0));
float sp = length(v);
float Nf = max(N, contact * 0.6 * length(gt) * (1.0 - smoothstep(2900.0, 6900.0, N)));
float fr = mu * max(Nf + adh, 0.0) * dt * (1.0 + uMaterial.y * contact);
if (sp <= fr) v = vec2(0.0);
else v -= v / sp * fr;
sp = length(v);
if (sp > 1.0) {
v += rdir(idx, 16u) * sp * (0.05 + 0.08 * contact) * rnd3(idx, 17u, uFrame);
float target = peb ? 1.0 : 0.55 + 0.45 * rnd3(idx, 18u, uFrame);
layer = mix(layer, target, clamp(sp * dt * 0.8, 0.0, 0.25));
}
float vmax = 2500.0;
sp = length(v);
if (sp > vmax) v *= vmax / sp;
if (hang) v = vec2(0.0);
}
bool ignoreFinger = (g.kind == 0.0 && rnd(idx, 19u) < 0.012 && g.r < 0.22);
vec2 q = p + (gb ? v * dt : vec2(0.0));
if (zAir < 3.0) {
float zg = clamp(max(h, G0.w), 0.0, 4.0) * (0.35 + 0.65 * layer);
for (int f = 0; f < 5; f++) {
vec4 fb = uFB[f];
if (fb.w < 0.5) continue;
vec4 fa = uFA[f];
vec2 a = fa.xy, b = fa.zw;
vec2 ab = b - a;
vec2 U = fb.xy;
if (uTool == 3.0) {
vec2 segment = b - a;
vec2 closest = a + segment * clamp(dot(p - a, segment) / max(dot(segment, segment), 1e-6), 0.0, 1.0);
if (distance(p, closest) < fb.z) { oB.w = 0.0; return; }
continue;
}
if (uMagnet > 0.5 && uMaterial.x == 2.0) {
vec2 toward = b - p;
float distanceToMagnet = length(toward);
float reach = fb.z * 8.0;
if (distanceToMagnet < reach && distanceToMagnet > 0.1) {
vec2 direction = toward / distanceToMagnet;
float force = 44000.0 * pow(1.0 - distanceToMagnet / reach, 2.0);
v += direction * force * dt * (uTool == 5.0 ? -0.65 : 1.0);
v *= exp(-dt * 14.0);
q = p + v * dt;
// Needles orient along the local magnetic field for rendering.
layer = clamp(layer, 0.0, 1.0);
}
continue;
}
float us = length(U);
float R = min(sqrt(fb.z * fb.z + 18.0 * zg), fb.z + 4.5) + g.r * 0.6;
if (uTool == 2.0) R = fb.z + g.r * 0.4;
vec2 c = a + ab * clamp(dot(q - a, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);
float dist = length(q - c);
if (gb && us < 8.0 && dist < R + 0.3) {
v = U;
q = p + U * dt;
c = a + ab * clamp(dot(q - a, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);
dist = length(q - c);
}
if (dist < R - (us < 8.0 ? 0.6 : 0.0) && !ignoreFinger) {
vec2 n = dist > 1e-4 ? (q - c) / dist : rdir(idx, 20u);
float pen = R - dist;
q = c + n * (R + (pen > 0.15 ? 1.6 * rnd3(idx, 23u, uFrame) : 0.0));
float vn = dot(v - U, n);
if (vn < 0.0) v -= vn * (1.0 + 0.6 * smoothstep(250.0, 2500.0, us)) * n;
vec2 tng = vec2(-n.y, n.x);
v -= dot(v - U, tng) * tng * 0.25;
v += n * us * 0.08 * rnd3(idx, 21u, uFrame);
if (us >= 8.0) layer = mix(layer, 1.0, 0.2);
if (us > 250.0 && pen > 0.05 && !peb) {
float k = rnd3(idx, 27u, uFrame);
zAir = 0.02;
vz = us * 0.2 * k * k;
} else if (zAir > 0.0) { zAir = 0.0; vz = 0.0; }
}
}
}
p = q;
float r = g.r;
float rest = g.kind == 2.0 ? 0.35 : uMaterial.z;
if (p.x < r) { p.x = r; if (v.x < 0.0) v.x = -v.x * rest; }
if (p.x > uWorld.x - r) { p.x = uWorld.x - r; if (v.x > 0.0) v.x = -v.x * rest; }
if (p.y < r) { p.y = r; if (v.y < 0.0) v.y = -v.y * rest; }
if (p.y > uWorld.y - r) { p.y = uWorld.y - r; if (v.y > 0.0) v.y = -v.y * rest; }
oA = vec4(p, v);
oB = vec4(zAir, vz, clamp(layer, 0.0, 1.0), 1.0);
}
`;
export const STATS_FS = HEADER + `
uniform sampler2D uS;
out vec4 o;
void main() {
ivec2 ts = textureSize(uS, 0);
ivec2 blk = (ts + 15) / 16;
ivec2 base = ivec2(gl_FragCoord.xy) * blk;
vec4 acc = vec4(0.0);
for (int y = 0; y < 64; y++) {
if (y >= blk.y) break;
for (int x = 0; x < 64; x++) {
if (x >= blk.x) break;
ivec2 c = base + ivec2(x, y);
if (c.x >= ts.x || c.y >= ts.y) continue;
vec4 s = texelFetch(uS, c, 0);
acc += vec4(s.x, s.w, step(0.05, s.x), 0.0);
}
}
o = acc;
}
`;
