// Smooth shaders: vertex and fragment programs for the rounded world look. const SM_VS = `#version 300 es void main() { vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); }`; const SM_FS = `#version 300 es precision highp float; precision highp int; uniform sampler2D tCol, tColL, tBack, tTree, tField, tLight, tGlow; uniform vec2 uSize, uOrigin, uLightSize, uGlowSize; uniform vec4 uView, uLightMap; uniform float uGlowK, uTime, uSurf, uRain; out vec4 o; float h21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } float vn(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(h21(i), h21(i + vec2(1, 0)), f.x), mix(h21(i + vec2(0, 1)), h21(i + vec2(1, 1)), f.x), f.y); } float fbm(vec2 p) { float a = 0.5, s = 0.0; for (int k = 0; k < 4; k++) { s += a * vn(p); p = p * 2.03 + 17.1; a *= 0.5; } return s / 0.9375; } vec4 lin(sampler2D t, vec2 q) { return textureLod(t, q / uSize, 0.0); } // cubic B-spline from four bilinear taps: rounds a field of 0s and 1s into smooth shapes vec4 bsp(sampler2D t, vec2 q) { vec2 tc = q - 0.5, i = floor(tc), f = tc - i, f2 = f * f, f3 = f2 * f; vec2 w0 = (1.0 - 3.0 * f + 3.0 * f2 - f3) / 6.0, w1 = (4.0 - 6.0 * f2 + 3.0 * f3) / 6.0; vec2 w2 = (1.0 + 3.0 * f + 3.0 * f2 - 3.0 * f3) / 6.0, w3 = f3 / 6.0; vec2 g0 = w0 + w1, g1 = w2 + w3; vec2 h0 = (i - 0.5 + w1 / g0) / uSize, h1 = (i + 1.5 + w3 / g1) / uSize; return g0.y * (g0.x * textureLod(t, h0, 0.0) + g1.x * textureLod(t, vec2(h1.x, h0.y), 0.0)) + g1.y * (g0.x * textureLod(t, vec2(h0.x, h1.y), 0.0) + g1.x * textureLod(t, h1, 0.0)); } vec4 cellAt(sampler2D t, ivec2 c) { return texelFetch(t, clamp(c, ivec2(0), ivec2(uSize) - 1), 0); } int cls(vec4 c) { return int(c.a * 255.0 + 0.5); } float isT(int n) { return (n == 1 || n == 4 || n == 6 || (n >= 7 && n <= 12)) ? 1.0 : 0.0; } float isM(int n) { return n == 2 ? 1.0 : isT(n); } // matter: ground, built pieces or loose grains float isF(int n) { return (n == 13 || n == 14) ? 1.0 : 0.0; } float isBelt(int n) { return (n == 7 || n == 8) ? 1.0 : 0.0; } float isN(int n, int k) { return n == k ? 1.0 : 0.0; } // signed distance (in grains) to the outline of one block of belt: a band 3.7 grains tall; where // the line of belts ends, the end is square with rounded corners. Negative inside. float beltSD(vec2 lb, bool endL, bool endR) { float x0 = endL ? 0.15 : -2.0, x1 = endR ? 3.85 : 6.0, r = 0.75; vec2 c = vec2((x0 + x1) * 0.5, 2.0), hs = vec2((x1 - x0) * 0.5, 1.85); vec2 q = abs(lb - c) - hs + r; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; } // The outline of the belt or filter in cell c. Belts and filters side by side are one belt, so // the line only rounds off where the whole run ends. float beltLineSD(ivec2 c, vec2 lb) { int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0))); return beltSD(lb, isBelt(nl) + isF(nl) < 0.5, isBelt(nr) + isF(nr) < 0.5); } // The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and // back along the bottom. float bandRidge(vec2 w, vec2 lb, float dir) { float run = lb.y < 2.0 ? 1.0 : -1.0; return step(0.72, fract((w.x - dir * run * uTime * 30.0) * 0.5)); } vec3 bandColor(vec2 lb, float rg) { return mix(vec3(0.2, 0.21, 0.24), vec3(0.34, 0.35, 0.39), rg) * (lb.y < 2.0 ? 1.0 : 0.72); } void main() { float zp = uView.z, px = 1.0 / zp; vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y); vec2 q = sp * px + uView.xy; // position in grains, in the window of grains uploaded vec2 w = q + uOrigin; // position in the world vec2 cam = uOrigin + uView.xy; // world position of the screen's top-left corner vec2 scr = q - uView.xy; // grains from the screen's top-left corner float shc = uView.w * px; // screen height in grains ivec2 c = ivec2(floor(q)); vec2 qq = q - 0.5; ivec2 c0 = ivec2(floor(qq)); vec2 t = qq - floor(qq); vec4 k0 = cellAt(tCol, c0), k1 = cellAt(tCol, c0 + ivec2(1, 0)), k2 = cellAt(tCol, c0 + ivec2(0, 1)), k3 = cellAt(tCol, c0 + ivec2(1, 1)); int n0 = cls(k0), n1 = cls(k1), n2 = cls(k2), n3 = cls(k3); vec4 own = cellAt(tCol, c); int nc = cls(own); vec2 s = smoothstep(0.1, 0.9, t); vec4 wq = vec4((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y); // fields, and everything that needs screen derivatives (worked out before any branching) vec4 Fb = bsp(tField, q); vec4 F = mix(lin(tField, q), Fb, 0.62); // loose-grain occupancy from the four grains around (bilinear): ground and grains together make "matter" vec4 wl = vec4((1.0 - t.x) * (1.0 - t.y), t.x * (1.0 - t.y), (1.0 - t.x) * t.y, t.x * t.y); float pw = dot(wl, vec4(isN(n0, 2), isN(n1, 2), isN(n2, 2), isN(n3, 2))); float M = clamp(F.r + pw, 0.0, 1.0); float lf = clamp(mix(F.g, Fb.g, 0.6) + F.r + pw, 0.0, 1.0); float fwT = max(fwidth(F.r), 1e-4), fwM = max(fwidth(M), 1e-4), fwA = max(fwidth(F.a), 1e-4), fwL = max(fwidth(lf), 1e-4); vec2 gT = vec2(dFdx(mix(Fb.r, M, 0.5)), dFdy(mix(Fb.r, M, 0.5))), gL = vec2(dFdx(F.g), dFdy(F.g)); // ---- behind everything: sky with clouds and far hills, or the cave's back wall vec4 back = lin(tBack, q); vec3 C = back.rgb; float sky = back.a; if (sky > 0.01) { vec3 S = C; vec2 P0 = scr + cam * 0.08; // clouds; while it rains they thicken and turn grey float cl = smoothstep(0.56 - 0.3 * uRain, 0.78 - 0.2 * uRain, fbm(vec2(P0.x * 0.016 + uTime * 0.006, P0.y * 0.05))); float hi = clamp((uSurf - 40.0 - w.y) / 50.0, 0.0, 1.0); S = mix(S, mix(vec3(0.96, 0.95, 0.93), vec3(0.5, 0.52, 0.56), uRain), cl * (0.6 + 0.3 * uRain) * hi); float k1 = 0.16; vec2 P1 = scr + cam * k1; float base1 = shc * 0.5 + 8.0 + (uSurf - shc * 0.5) * k1; float top1 = base1 - 36.0 * (0.25 + 0.9 * fbm(vec2(P1.x * 0.009, 1.7))); float m1 = smoothstep(top1 - px * 1.5, top1 + px * 1.5, P1.y); vec3 far = mix(vec3(0.50, 0.60, 0.72), S, 0.35) * (0.94 + 0.08 * fbm(P1 * 0.05)); S = mix(S, far, m1); float k2 = 0.38; vec2 P2 = scr + cam * k2; float base2 = shc * 0.5 + 14.0 + (uSurf - shc * 0.5) * k2; float top2 = base2 - 20.0 * (0.2 + 0.9 * fbm(vec2(P2.x * 0.016, 7.3))); float m2 = smoothstep(top2 - px * 1.5, top2 + px * 1.5, P2.y); vec3 near = vec3(0.36, 0.5, 0.47) * (0.92 + 0.14 * fbm(P2 * 0.08)); near = mix(near, S, 0.42 - 0.2 * smoothstep(top2, top2 + 30.0, P2.y)); S = mix(S, near, m2); C = mix(C, S, sky); } // the back wall: rough rock, and darker close to the tunnel walls (ambient occlusion) float notSky = 1.0 - sky; C *= mix(1.0, 1.08 + 0.4 * fbm(w * 0.21), notSky); float near = textureLod(tField, q / uSize, 2.2).r; C *= 1.0 - 0.34 * smoothstep(0.12, 0.85, near) * (1.0 - 0.75 * sky); // ---- trees (behind the ground and everything on it) if (F.a > 0.2) { vec4 a0 = cellAt(tTree, c0), a1 = cellAt(tTree, c0 + ivec2(1, 0)), a2 = cellAt(tTree, c0 + ivec2(0, 1)), a3 = cellAt(tTree, c0 + ivec2(1, 1)); vec4 m = vec4(step(0.4, a0.a), step(0.4, a1.a), step(0.4, a2.a), step(0.4, a3.a)) * wq; float sm = m.x + m.y + m.z + m.w; if (sm > 1e-4) { vec3 tc = (a0.rgb * m.x + a1.rgb * m.y + a2.rgb * m.z + a3.rgb * m.w) / sm; float leaf = (a0.a * m.x + a1.a * m.y + a2.a * m.z + a3.a * m.w) / sm; float lv = smoothstep(0.6, 0.9, leaf); tc *= mix(0.86 + 0.22 * vn(vec2(w.x * 1.4, w.y * 0.25)), 0.72 + 0.5 * fbm(w * 0.42), lv); float d = (F.a - 0.36) / fwA; tc *= mix(0.55, 1.0, smoothstep(0.4, 1.6, d)); C = mix(C, tc, smoothstep(-0.7, 0.7, d)); } } // ---- smoke, steam and flame: soft, see-through puffs if (Fb.b > 0.02) { vec4 m = vec4(isN(n0, 5), isN(n1, 5), isN(n2, 5), isN(n3, 5)) * wq; float sm = m.x + m.y + m.z + m.w; vec3 gc = sm > 1e-4 ? (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm : own.rgb; if (sm > 1e-4 || nc == 5) C = mix(C, gc, smoothstep(0.03, 0.55, Fb.b) * 0.92); } // ---- liquids: smooth surfaces that fill right up against the ground, with a glossy top line float anyL = isN(n0, 3) + isN(n1, 3) + isN(n2, 3) + isN(n3, 3); if (anyL > 0.0 && lf > 0.3) { vec4 m = vec4(isN(n0, 3), isN(n1, 3), isN(n2, 3), isN(n3, 3)) * (wq + 0.02); float sm = m.x + m.y + m.z + m.w; vec3 lc = (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm; lc *= 0.94 + 0.1 * vn(w * 0.55 + vec2(uTime * 0.7, uTime * 0.25)); float gl = length(gL); float topSide = gl > 1e-5 ? clamp(gL.y / gl, 0.0, 1.0) : 0.0; float band = 1.0 - smoothstep(0.5, 0.75, F.g); lc += vec3(0.16, 0.18, 0.2) * topSide * band; C = mix(C, lc, smoothstep(0.5 - fwL, 0.5 + fwL, lf)); } // ---- the ground: rounded, outlined, with a lit top edge and a shaded underside if (F.r > 0.3) { vec4 m = vec4(isT(n0), isT(n1), isT(n2), isT(n3)) * wq; float sm = m.x + m.y + m.z + m.w; vec3 blur = bsp(tColL, q).rgb, tc = own.rgb; float bd = 1e9; if (m.x > 0.0) { vec3 e = k0.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k0.rgb; } } if (m.y > 0.0) { vec3 e = k1.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k1.rgb; } } if (m.z > 0.0) { vec3 e = k2.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k2.rgb; } } if (m.w > 0.0) { vec3 e = k3.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k3.rgb; } } if (sm < 1e-4) tc = own.rgb; tc *= 0.84 + 0.3 * fbm(w * 0.28); tc *= 0.95 + 0.1 * vn(w * 1.6); float gl = length(gT); vec2 nrm = gl > 1e-6 ? -gT / gl : vec2(0.0); float band = 1.0 - smoothstep(0.5, 0.9, M); float lit = dot(nrm, normalize(vec2(-0.5, -1.0))); tc *= 1.0 + 0.42 * lit * band; // Keep the air outline; a softer, screen-width seam also separates rock from loose grains. float dm = (M - 0.5) / fwM; tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm)); float dt = (F.r - 0.5) / fwT; if (nc == 1 && pw > 0.0) tc *= mix(0.65, 1.0, smoothstep(0.0, 1.5, dt)); C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT)); } // ---- loose grains: little round pebbles; a pocket closed in by four grains is filled int np = int(isN(n0, 2) + isN(n1, 2) + isN(n2, 2) + isN(n3, 2)); bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5; if (np > 0) { float best = 1e9; vec3 gc = vec3(0.0); vec2 gd = vec2(0.0); float gr = 0.6; for (int k = 0; k < 4; k++) { ivec2 o4 = ivec2(k & 1, k >> 1); int nk = k == 0 ? n0 : k == 1 ? n1 : k == 2 ? n2 : n3; if (nk != 2) continue; vec4 ck = k == 0 ? k0 : k == 1 ? k1 : k == 2 ? k2 : k3; vec2 ce = vec2(c0 + o4) + 0.5; float r = 0.6 + 0.1 * (h21(ce + uOrigin) - 0.5); vec2 dv = (q - ce) / r; float dd = length(dv); if (dd < best) { best = dd; gc = ck.rgb; gd = dv; gr = r; } } float cover = np == 4 ? 1.0 : 1.0 - smoothstep(1.0 - px / gr, 1.0 + px / gr, best); if (closed && F.r < 0.5) cover = 1.0; // Pebbles stop at the rock face so they cannot paint over its thin seam. cover *= 1.0 - smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT); if (cover > 0.0) { float r2 = min(1.0, dot(gd, gd)); vec3 n3 = normalize(vec3(gd * 0.85, sqrt(1.0 - r2) + 0.25)); float sh = 0.98 + 0.18 * max(0.0, dot(n3, normalize(vec3(-0.45, -0.8, 0.6)))); sh *= mix(1.0, 0.94, smoothstep(0.7 - px * 0.2, 1.15 + px * 0.2, best)); vec3 pc = (np == 4 ? (k0.rgb * wq.x + k1.rgb * wq.y + k2.rgb * wq.z + k3.rgb * wq.w) : gc) * sh; if (!closed) pc *= mix(1.0, 0.72, smoothstep(0.7, 1.0, best)); C = mix(C, pc, cover); } } // ---- built pieces: crisp, and wall blocks drawn as bevelled tiles with mortar lines if (nc == 4) C = own.rgb; else if (nc == 7 || nc == 8) { // belt: a rubber band round two copper rollers per block; the top run moves the load float dir = nc == 8 ? 1.0 : -1.0; vec2 lb = fract(w * 0.25) * 4.0; // outline: square ends with rounded corners where the line of belts (and filters) ends float sd = beltLineSD(c, lb); float inside = 1.0 - smoothstep(-px, px, sd); if (inside > 0.0) { float edge = -sd; vec3 bc; if (edge < 0.8) { bc = bandColor(lb, bandRidge(w, lb, dir)); bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge)); } else { vec2 rc = vec2(lb.x < 2.0 ? 1.0 : 3.0, 2.0), dv = lb - rc; float rd = length(dv); bc = vec3(0.15, 0.16, 0.18); if (rd < 0.95) { float ang = atan(dv.y, dv.x) - dir * uTime * 30.0 / 0.95; float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0)); bc = mix(vec3(0.5, 0.36, 0.25), vec3(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd)); bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd)); bc *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd)); } } C = mix(C, bc, inside); } } else if ((nc >= 9 && nc <= 12) || (nc >= 19 && nc <= 22)) { // screw tube: a copper pipe; what is inside shows through, and the screw's flights turn bool vert = nc == 9 || nc == 11; float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0; float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x; if (nc >= 19) { vec2 d = nc == 19 ? vec2(1.0, -1.0) : nc == 20 ? vec2(1.0, 1.0) : nc == 21 ? vec2(-1.0, 1.0) : vec2(-1.0, -1.0); a = fract(dot(w, vec2(-d.y, d.x)) * 0.25) * 4.0; along = dot(w, d); sp2 = 1.0; } float wall = 0.62; vec3 tc; if (a < wall || a > 4.0 - wall) { float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore float cyl = sin(u2 * 1.5708); tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl); if (a < wall) tc *= 1.08; float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0); tc *= 1.0 - 0.3 * ring; tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a))); } else { bool full = own.r + own.g + own.b > 0.0; float fl = fract((along + a * 0.9 - sp2 * uTime * 14.0) * 0.5); float flight = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl)); vec3 bore = vec3(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4)); tc = full ? own.rgb * (0.9 + 0.1 * vn(w * 1.3)) : bore; tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8)); tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0)); } C = tc; } else if (nc == 6 || (nc >= 15 && nc <= 18)) { vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb; float e = min(min(lb.x, lb.y), min(rb.x, rb.y)); vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55)); float hl = max(1.0 - smoothstep(0.0, 0.8, lb.x), 1.0 - smoothstep(0.0, 0.8, lb.y)); float shd = max(1.0 - smoothstep(0.0, 0.8, rb.x), 1.0 - smoothstep(0.0, 0.8, rb.y)); tc *= 1.0 + 0.2 * hl - 0.26 * shd; tc *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e)); if (nc >= 15) { vec2 sl = lb; if (nc == 16 || nc == 18) sl.x = 4.0 - sl.x; if (nc >= 17) sl.y = 4.0 - sl.y; float d = (sl.y - sl.x) * 0.70710678; tc *= 1.0 + 0.2 * (1.0 - smoothstep(0.0, 0.6, d)); C = mix(C, tc, smoothstep(-px * 0.5, px * 0.5, d)); } else C = tc; } else if (nc == 13 || nc == 14) { // filter: a belt whose middle is a see-through steel mesh. It has the same band as a belt, so a // row of belts and filters reads as one belt, rounded only where the whole line ends. The // band is perforated; its colour tag sits on the top run for what rides along, on the // bottom run for what drops through. float dir = nc == 14 ? 1.0 : -1.0; vec2 lb = fract(w * 0.25) * 4.0; vec4 tag = cellAt(tTree, c); bool carry = tag.a > 0.16, tinted = tag.r + tag.g + tag.b > 0.0; float sd = beltLineSD(c, lb); float inside = 1.0 - smoothstep(-px, px, sd); if (inside > 0.0) { float edge = -sd; vec3 steel = vec3(0.42, 0.44, 0.5); // through the holes: whatever is behind (water, sky, rock), or what is passing through vec3 behind = own.r + own.g + own.b > 0.0 ? own.rgb : C * 0.8; vec2 cg = abs(fract(w) - 0.5); float bw2 = max(px * 0.9, 0.09); float hole = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y)); vec3 fc; if (edge < 0.8) { float rg = bandRidge(w, lb, dir); vec3 band = bandColor(lb, rg); if (tinted && (carry == (lb.y < 2.0))) band = mix(band, tag.rgb * (lb.y < 2.0 ? 1.0 : 0.8), 0.75 * rg); fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75); fc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge)); } else { fc = mix(steel * 0.78, behind, hole); fc *= mix(0.7, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge - 0.8)); } C = mix(C, fc, inside); } } // ---- light, then the glow of hot things on top if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb; if (uGlowK > 0.0) { vec2 gu = q / (2.0 * uGlowSize), d = 1.0 / uGlowSize; vec3 g = textureLod(tGlow, gu, 0.0).rgb * 0.25; g += (textureLod(tGlow, gu + vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu - vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu + vec2(0, d.y), 0.0).rgb + textureLod(tGlow, gu - vec2(0, d.y), 0.0).rgb) * 0.125; g += (textureLod(tGlow, gu + d, 0.0).rgb + textureLod(tGlow, gu - d, 0.0).rgb + textureLod(tGlow, gu + vec2(d.x, -d.y), 0.0).rgb + textureLod(tGlow, gu + vec2(-d.x, d.y), 0.0).rgb) * 0.0625; C += g * uGlowK; } o = vec4(C, 1.0); }`;