import { glsl } from './common/tags.ts'; /** Falling sand shaders using block cellular automata with Margolus offsets. * Based on "Probabilistic Cellular Automata for Granular Media in Video Games" (https://arxiv.org/abs/2008.06341) * Code adapted from https://github.com/GelamiSalami/GPU-Falling-Sand-CA */ const CONSTANTS = glsl` #define AIR 0.0 #define SMOKE 1.0 #define WATER 2.0 #define LAVA 3.0 #define SAND 4.0 #define PLANT 5.0 #define STONE 6.0 #define WALL 7.0 #define COLLISION 99.0 const vec3 bgColor = pow(vec3(31, 34, 36) / 255.0, vec3(2)); `; const UTILS = glsl` const float EPSILON = 1e-4; const float PI = acos(-1.); const float TAU = PI * 2.0; vec3 saturate(vec3 x) { return clamp(x, vec3(0), vec3(1)); } // https://iquilezles.org/articles/palettes/ vec3 palette(float t) { return .5 + .5 * cos(TAU * (vec3(1, 1, 1) * t + vec3(0, .33, .67))); } // Hash without Sine // https://www.shadertoy.com/view/4djSRW float hash12(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * .1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); } float hash13(vec3 p3) { p3 = fract(p3 * .1031); p3 += dot(p3, p3.zyx + 31.32); return fract((p3.x + p3.y) * p3.z); } vec3 hash33(vec3 p3) { p3 = fract(p3 * vec3(.1031, .1030, .0973)); p3 += dot(p3, p3.yxz+33.33); return fract((p3.xxy + p3.yxx)*p3.zyx); } vec4 hash43(vec3 p) { vec4 p4 = fract(vec4(p.xyzx) * vec4(.1031, .1030, .0973, .1099)); p4 += dot(p4, p4.wzxy+33.33); return fract((p4.xxyz+p4.yzzw)*p4.zywx); } // https://www.chilliant.com/rgb2hsv.html vec3 RGBtoHCV(in vec3 RGB) { // Based on work by Sam Hocevar and Emil Persson vec4 P = (RGB.g < RGB.b) ? vec4(RGB.bg, -1.0, 2.0/3.0) : vec4(RGB.gb, 0.0, -1.0/3.0); vec4 Q = (RGB.r < P.x) ? vec4(P.xyw, RGB.r) : vec4(RGB.r, P.yzx); float C = Q.x - min(Q.w, Q.y); float H = abs((Q.w - Q.y) / (6.0 * C + EPSILON) + Q.z); return vec3(H, C, Q.x); } vec3 RGBtoHSL(in vec3 RGB) { vec3 HCV = RGBtoHCV(RGB); float L = HCV.z - HCV.y * 0.5; float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + EPSILON); return vec3(HCV.x, S, L); } vec3 HUEtoRGB(in float H) { float R = abs(H * 6.0 - 3.0) - 1.0; float G = 2.0 - abs(H * 6.0 - 2.0); float B = 2.0 - abs(H * 6.0 - 4.0); return saturate(vec3(R,G,B)); } vec3 HSLtoRGB(in vec3 HSL) { vec3 RGB = HUEtoRGB(HSL.x); float C = (1.0 - abs(2.0 * HSL.z - 1.0)) * HSL.y; return (RGB - 0.5) * C + HSL.z; } vec3 linearTosRGB(vec3 col) { return mix(1.055 * pow(col, vec3(1.0 / 2.4)) - 0.055, col * 12.92, lessThan(col, vec3(0.0031308))); } `; /** Vertex shader for rendering quads */ export const vertexShader = glsl`#version 300 es in vec4 aPosition; in vec2 aUv; out vec2 outUv; void main() { gl_Position = aPosition; outUv = aUv; } `; export const simulationShader = glsl`#version 300 es precision mediump float; uniform vec2 resolution; uniform float time; uniform int frame; uniform vec4 mouse; uniform int materialType; uniform float brushRadius; uniform sampler2D tex; uniform sampler2D u_collisionTex; in vec2 outUv; out vec4 fragColor; ${CONSTANTS} ${UTILS} // https://iquilezles.org/articles/distfunctions2d/ float sdSegment(vec2 p, vec2 a, vec2 b) { vec2 pa = p-a, ba = b-a; float h = clamp( dot(pa,ba) / dot(ba,ba), 0.0, 1.0 ); return length( pa - ba*h ); } ivec2 getOffset(int frame) { int i = frame % 4; if (i == 0) return ivec2(0, 0); else if (i == 1) return ivec2(1, 1); else if (i == 2) return ivec2(0, 1); return ivec2(1, 0); } vec4 getData(ivec2 p) { // Check boundaries first if (p.x < 0 || p.y < 0 || p.x >= int(resolution.x) || p.y >= int(resolution.y)) { return vec4(vec3(0.02), WALL); } // Calculate UV coordinates for the collision texture vec2 collisionUv = (vec2(p) + 0.5) / resolution; float collisionValue = texture(u_collisionTex, collisionUv).r; // If there's a collision at this position, always return COLLISION type if (collisionValue > 0.5) { return vec4(bgColor, COLLISION); } // If no collision, get the data from the simulation texture vec4 data = texelFetch(tex, p, 0); if (data.xyz == vec3(0)) { data.xyz = bgColor; } return data; } void swap(inout vec4 a, inout vec4 b) { vec4 tmp = a; a = b; b = tmp; } vec4 createParticle(float id) { if (id == AIR) { return vec4(bgColor, AIR); } else if (id == SMOKE) { return vec4(mix(bgColor, vec3(0.15), 0.5), SMOKE); } else if (id == WATER) { return vec4(mix(bgColor, vec3(0.15, 0.45, 0.9), 0.7), WATER); } else if (id == LAVA) { vec3 r = hash33(vec3(gl_FragCoord.xy, frame)); vec3 color = vec3(255, 40, 20) / 255.0; vec3 hsl = RGBtoHSL(color); hsl.x += (r.z - 0.5) * 12.0 / 255.0; hsl.y += (r.x - 0.5) * 16.0 / 255.0; hsl.z *= (r.y * 80.0 / 255.0 + (255.0 - 80.0) / 255.0); return vec4(HSLtoRGB(hsl), LAVA); } else if (id == SAND) { vec3 r = hash33(vec3(gl_FragCoord.xy, frame)); vec3 color = vec3(220, 158, 70) / 255.0; vec3 hsl = RGBtoHSL(color); hsl.x += (r.z - 0.5) * 12.0 / 255.0; hsl.y += (r.x - 0.5) * 16.0 / 255.0; hsl.z += (r.y - 0.5) * 40.0 / 255.0; return vec4(HSLtoRGB(hsl), SAND); } else if (id == PLANT) { vec3 r = hash33(vec3(gl_FragCoord.xy, frame)); vec3 color = vec3(34, 139, 34) / 255.0; vec3 hsl = RGBtoHSL(color); hsl.x += (r.z - 0.5) * 0.1; hsl.y += (r.x - 0.5) * 0.2; hsl.z *= (r.y * 0.4 + 0.6); return vec4(HSLtoRGB(hsl), PLANT); } else if (id == STONE) { float r = hash13(vec3(gl_FragCoord.xy, frame)); return vec4(vec3(0.08, 0.1, 0.12) * (r * 0.5 + 0.5), STONE); } else if (id == WALL) { float r = hash13(vec3(gl_FragCoord.xy, frame)); return vec4(bgColor * 0.5 * (r * 0.4 + 0.6), WALL); } return vec4(bgColor, AIR); } void main() { vec2 uv = gl_FragCoord.xy / resolution; if (frame == 0) { float r = hash12(gl_FragCoord.xy); float id = AIR; if (r < 0.15) { id = SAND; } else if (r < 0.25) { id = SMOKE; } fragColor = createParticle(id); return; } if (mouse.x > 0.0) { float d = sdSegment(gl_FragCoord.xy, mouse.xy, mouse.zw); if (d < brushRadius) { fragColor = createParticle(float(materialType)); return; } } ivec2 offset = getOffset(frame); ivec2 fc = ivec2(gl_FragCoord.xy) + offset; ivec2 p = (fc / 2) * 2 - offset; ivec2 xy = fc % 2; int i = xy.x + xy.y * 2; vec4 t00 = getData(p); // top-left vec4 t10 = getData(p + ivec2(1, 0)); // top-right vec4 t01 = getData(p + ivec2(0, 1)); // bottom-left vec4 t11 = getData(p + ivec2(1, 1)); // bottom-right vec4 tn00 = getData(p + ivec2(0, -1)); vec4 tn10 = getData(p + ivec2(1, -1)); if (t00.a == t10.a && t01.a == t11.a && t00.a == t01.a) { fragColor = i == 0 ? t00 : i == 1 ? t10 : i == 2 ? t01 : t11; return; } vec4 r = hash43(vec3(p, frame)); if ((t01.a == SMOKE && t11.a < SMOKE || t01.a < SMOKE && t11.a == SMOKE) && r.x < 0.25) { swap(t01, t11); } if (t00.a == SMOKE) { if (t01.a < t00.a && r.y < 0.25) { swap(t00, t01); } else if (r.z < 0.003) { t00 = vec4(bgColor, AIR); } } if (t10.a == SMOKE) { if (t11.a < t10.a && r.y < 0.25) { swap(t10, t11); } else if (r.z < 0.003) { t10 = vec4(bgColor, AIR); } } if (((t01.a == SAND) && t11.a < SAND || t01.a < SAND && (t11.a == SAND)) && t00.a < SAND && t10.a < SAND && r.x < 0.4) { swap(t01, t11); } if (t01.a == SAND || t01.a == STONE) { if (t00.a < SAND && t00.a != WATER && t00.a != LAVA) { if (r.y < 0.9) swap(t01, t00); } else if (t00.a == WATER) { if (r.y < 0.3) swap(t01, t00); } else if (t00.a == LAVA) { float fallProb = t01.a == SAND ? 0.15 : 0.25; if (r.y < fallProb) swap(t01, t00); } else if (t11.a < SAND && t10.a < SAND) { swap(t01, t10); } } if (t11.a == SAND || t11.a == STONE) { if (t10.a < SAND && t10.a != WATER && t10.a != LAVA) { if (r.y < 0.9) swap(t11, t10); } else if (t10.a == WATER) { if (r.y < 0.3) swap(t11, t10); } else if (t10.a == LAVA) { float fallProb = t11.a == SAND ? 0.15 : 0.25; if (r.y < fallProb) swap(t11, t10); } else if (t01.a < SAND && t00.a < SAND) { swap(t11, t00); } } bool drop = false; if (t01.a == WATER) { if (t00.a < t01.a && r.y < 0.95) { swap(t01, t00); drop = true; } else if (t11.a < t01.a && t10.a < t01.a && r.z < 0.3) { swap(t01, t10); drop = true; } } if (t11.a == WATER) { if (t10.a < t11.a && r.y < 0.95) { swap(t11, t10); drop = true; } else if (t01.a < t11.a && t00.a < t11.a && r.z < 0.3) { swap(t11, t00); drop = true; } } if (!drop) { if ((t01.a == WATER && t11.a < WATER || t01.a < WATER && t11.a == WATER) && (t00.a >= WATER && t10.a >= WATER || r.w < 0.8)) { swap(t01, t11); } if ((t00.a == WATER && t10.a < WATER || t00.a < WATER && t10.a == WATER) && (tn00.a >= WATER && tn10.a >= WATER || r.w < 0.8)) { swap(t00, t10); } } if (t01.a == LAVA) { if (t00.a < t01.a && r.y < 0.8) { swap(t01, t00); } else if (t11.a < t01.a && t10.a < t01.a && r.z < 0.2) { swap(t01, t10); } } if (t11.a == LAVA) { if (t10.a < t11.a && r.y < 0.8) { swap(t11, t10); } else if (t01.a < t11.a && t00.a < t11.a && r.z < 0.2) { swap(t11, t00); } } if (t00.a == LAVA) { if (t01.a == WATER) { t00 = createParticle(STONE); t01 = createParticle(SMOKE); } else if (t10.a == WATER) { t00 = createParticle(STONE); t10 = createParticle(SMOKE); } else if (t01.a == PLANT && r.x < 0.03) // left { t01 = createParticle(SMOKE); if (r.y < 0.04) { t00 = createParticle(STONE); } } else if (t10.a == PLANT && r.x < 0.03) // right { t10 = createParticle(SMOKE); if (r.y < 0.04) { t10 = createParticle(STONE); } } } if (t10.a == LAVA) { if (t11.a == WATER) { t10 = createParticle(STONE); t11 = createParticle(SMOKE); } else if (t00.a == WATER) { t10 = createParticle(STONE); t00 = createParticle(SMOKE); } else if (t11.a == PLANT && r.x < 0.03) { t11 = createParticle(SMOKE); if (r.y < 0.04) { t10 = createParticle(STONE); } } else if (t00.a == PLANT && r.x < 0.03) { t00 = createParticle(SMOKE); if (r.y < 0.04) { t10 = createParticle(STONE); } } } if (t01.a == LAVA) { if (t00.a == PLANT && r.x < 0.03) { t00 = createParticle(SMOKE); if (r.y < 0.04) { t01 = createParticle(STONE); } } else if (t11.a == PLANT && r.x < 0.03) { t11 = createParticle(SMOKE); if (r.y < 0.04) { t01 = createParticle(STONE); } } } if (t11.a == LAVA) { if (t10.a == PLANT && r.x < 0.03) { t10 = createParticle(SMOKE); if (r.y < 0.04) { t11 = createParticle(STONE); } } else if (t01.a == PLANT && r.x < 0.03) { t01 = createParticle(SMOKE); if (r.y < 0.04) { t11 = createParticle(STONE); } } } if ((t01.a == LAVA && t11.a < LAVA || t01.a < LAVA && t11.a == LAVA) && r.x < 0.6) { swap(t01, t11); } fragColor = i == 0 ? t00 : i == 1 ? t10 : i == 2 ? t01 : t11; if (fragColor.a == COLLISION) { vec2 collisionUv = gl_FragCoord.xy / resolution; float collisionValue = texture(u_collisionTex, collisionUv).r; if (collisionValue <= 0.5) { fragColor = vec4(bgColor, AIR); } } } `; export const distanceFieldInitShader = glsl`#version 300 es precision highp float; uniform vec2 resolution; uniform sampler2D dataTex; ${CONSTANTS} layout(location = 0) out vec4 fragColorR; layout(location = 1) out vec4 fragColorG; layout(location = 2) out vec4 fragColorB; void main() { vec2 uv = gl_FragCoord.xy / resolution; vec4 data = texture(dataTex, uv); fragColorR = vec4(-1, -1, 0, 0); fragColorG = vec4(-1, -1, 0, 0); fragColorB = vec4(-1, -1, 0, 0); if (data.a <= LAVA) { fragColorR.xy = gl_FragCoord.xy; fragColorG.xy = gl_FragCoord.xy; fragColorB.xy = gl_FragCoord.xy; } if (data.a == SMOKE) { fragColorR.w = 6.0; fragColorG.w = 6.0; fragColorB.w = 6.0; } else if (data.a == WATER) { fragColorR.w = 9.0; fragColorG.w = 6.0; fragColorB.w = 4.0; } else if (data.a == LAVA) { fragColorR.w = 0.0; fragColorG.w = 11.0; fragColorB.w = 14.0; } else if (data.a == PLANT) { fragColorR.w = 4.0 - data.r * 4.0; fragColorG.w = 4.0 - data.g * 4.0; fragColorB.w = 4.0 - data.b * 4.0; } } `; export const distanceFieldPropagationShader = glsl`#version 300 es precision highp float; uniform float stepSize; uniform vec2 resolution; uniform sampler2D texR; uniform sampler2D texG; uniform sampler2D texB; uniform int passCount; uniform int passIndex; layout(location = 0) out vec4 fragColorR; layout(location = 1) out vec4 fragColorG; layout(location = 2) out vec4 fragColorB; void main() { vec2 fc = gl_FragCoord.xy; vec4 bestR = vec4(0,0,1e3,0); vec4 bestG = vec4(0,0,1e3,0); vec4 bestB = vec4(0,0,1e3,0); for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { vec2 p = fc + vec2(x, y) * stepSize; vec4 dataR = texture(texR, p / resolution); vec4 dataG = texture(texG, p / resolution); vec4 dataB = texture(texB, p / resolution); if (dataR.xy != vec2(-1) && dataR.xy == clamp(dataR.xy, vec2(0.5), resolution-0.5)) { float dist = distance(fc, dataR.xy) + dataR.w; if (dist < bestR.z) { bestR = dataR; bestR.z = dist; } } if (dataG.xy != vec2(-1) && dataG.xy == clamp(dataG.xy, vec2(0.5), resolution-0.5)) { float dist = distance(fc, dataG.xy) + dataG.w; if (dist < bestG.z) { bestG = dataG; bestG.z = dist; } } if (dataB.xy != vec2(-1) && dataB.xy == clamp(dataB.xy, vec2(0.5), resolution-0.5)) { float dist = distance(fc, dataB.xy) + dataB.w; if (dist < bestB.z) { bestB = dataB; bestB.z = dist; } } } } fragColorR = vec4(bestR.xy, bestR.z != 1e3 ? bestR.z : 1e3, bestR.w); fragColorG = vec4(bestG.xy, bestG.z != 1e3 ? bestG.z : 1e3, bestG.w); fragColorB = vec4(bestB.xy, bestB.z != 1e3 ? bestB.z : 1e3, bestB.w); if (passIndex == passCount - 1) { if (bestR.xy == vec2(-1)) fragColorR.z = 1e3; if (bestG.xy == vec2(-1)) fragColorG.z = 1e3; if (bestB.xy == vec2(-1)) fragColorB.z = 1e3; } } `; export const visualizationShader = glsl`#version 300 es precision highp float; uniform vec2 texResolution; uniform float texScale; uniform vec2 resolution; uniform sampler2D tex; uniform sampler2D shadowTexR; uniform sampler2D shadowTexG; uniform sampler2D shadowTexB; uniform sampler2D u_collisionTex; uniform float scale; ${CONSTANTS} out vec4 fragColor; vec2 getCoordsAA(vec2 uv) { float w = 1.5; // 1.5 vec2 fl = floor(uv + 0.5); vec2 fr = fract(uv + 0.5); vec2 aa = fwidth(uv) * w * 0.5; fr = smoothstep(0.5 - aa, 0.5 + aa, fr); return fl + fr - 0.5; } vec3 linearTosRGB(vec3 col) { return mix(1.055 * pow(col, vec3(1.0 / 2.4)) - 0.055, col * 12.92, lessThan(col, vec3(0.0031308))); } void main() { vec2 uv = gl_FragCoord.xy / (texResolution * texScale); uv -= 0.5; uv *= scale; uv += 0.5; vec2 fc = uv * texResolution; vec4 data = texture(tex, getCoordsAA(fc) / texResolution); vec4 dataUp = texture(tex, getCoordsAA(fc + vec2(0, 1)) / texResolution); vec4 dataDown = texture(tex, getCoordsAA(fc - vec2(0, 1)) / texResolution); float hig = float(data.a > dataUp.a); float dropSha = 1.0 - float(data.a > dataDown.a); vec3 color = data.rgb == vec3(0) ? bgColor : data.rgb; vec4 shaDataR = texture(shadowTexR, uv); vec4 shaDataG = texture(shadowTexG, uv); vec4 shaDataB = texture(shadowTexB, uv); float shaR = shaDataR.xy != vec2(-1) ? shaDataR.z : 16.0; float shaG = shaDataG.xy != vec2(-1) ? shaDataG.z : 16.0; float shaB = shaDataB.xy != vec2(-1) ? shaDataB.z : 16.0; vec3 sha = clamp(1.0 - vec3(shaR, shaG, shaB) / 16.0, vec3(0.0), vec3(1.0)); sha *= sha; color *= 0.5 * max(hig, dropSha) + 0.5; color *= sha * 1.0 + 0.2; color += color * 0.4 * hig; fragColor = vec4(linearTosRGB(color), 1); } `; export const collisionVertexShader = glsl`#version 300 es precision highp float; layout(location = 0) in vec2 aPosition; void main() { gl_Position = vec4(aPosition, 0.0, 1.0); }`; export const collisionFragmentShader = glsl`#version 300 es precision highp float; out vec4 fragColor; void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0); // red represents solid }`;