radiance: 3D particle sim — curl-noise currents, ribbon density, depth scatter on stir, rolling respawn; ~100k particles
This commit is contained in:
@ -61,48 +61,86 @@ vec3 rotX(vec3 p, float a) {
|
|||||||
return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
|
return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where particle idx rests, in units of the orb radius (origin at the orb centre, y up).
|
const float TAU = 6.2831853;
|
||||||
|
|
||||||
|
vec3 sphDir(float a, float b) {
|
||||||
|
float y = a * 2.0 - 1.0;
|
||||||
|
float s = sqrt(1.0 - y * y);
|
||||||
|
float ph = b * TAU;
|
||||||
|
return vec3(cos(ph) * s, y, sin(ph) * s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A divergence-free current: the curl of three offset value-noise fields.
|
||||||
|
vec3 curlNoise(vec3 p) {
|
||||||
|
const float e = 0.1;
|
||||||
|
const vec3 o1 = vec3(0.0);
|
||||||
|
const vec3 o2 = vec3(31.4, 17.1, 5.3);
|
||||||
|
const vec3 o3 = vec3(9.2, 47.7, 23.9);
|
||||||
|
vec3 dx = vec3(e, 0.0, 0.0), dy = vec3(0.0, e, 0.0), dz = vec3(0.0, 0.0, e);
|
||||||
|
float x_dy = vnoise(p + o1 + dy) - vnoise(p + o1 - dy);
|
||||||
|
float x_dz = vnoise(p + o1 + dz) - vnoise(p + o1 - dz);
|
||||||
|
float y_dx = vnoise(p + o2 + dx) - vnoise(p + o2 - dx);
|
||||||
|
float y_dz = vnoise(p + o2 + dz) - vnoise(p + o2 - dz);
|
||||||
|
float z_dx = vnoise(p + o3 + dx) - vnoise(p + o3 - dx);
|
||||||
|
float z_dy = vnoise(p + o3 + dy) - vnoise(p + o3 - dy);
|
||||||
|
return vec3(z_dy - y_dz, x_dz - z_dx, y_dx - x_dy) / (2.0 * e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- inner particles: each lives in a 3D volume and is carried by currents.
|
||||||
|
// Every particle is respawned now and then (fully faded at the seam) so the flow's sinks never swallow the density.
|
||||||
|
float lifeLen(uint idx) { return mix(9.0, 30.0, h01(idx * 8u + 1u)); }
|
||||||
|
float lifeT(uint idx, float t) { return t / lifeLen(idx) + h01(idx * 8u + 2u); }
|
||||||
|
float lifeAlpha(float x) {
|
||||||
|
float ph = fract(x);
|
||||||
|
return smoothstep(0.0, 0.08, ph) * (1.0 - smoothstep(0.92, 1.0, ph));
|
||||||
|
}
|
||||||
|
// most particles sit in the glassy skin, the rest hang in the volume behind it
|
||||||
|
float layerR(uint idx) {
|
||||||
|
float k = h01(idx * 8u + 5u);
|
||||||
|
float u = h01(idx * 8u + 4u);
|
||||||
|
return k < 0.5 ? mix(0.94, 1.0, u) : mix(0.4, 0.94, u);
|
||||||
|
}
|
||||||
|
// A drifting ridge field: density peaks along thin curved sheets, which cut the sphere as ribbons.
|
||||||
|
float density(vec3 dir, float t) {
|
||||||
|
vec3 w = dir * 1.6 + vec3(0.0, t * 0.05, t * 0.035);
|
||||||
|
float n = fbm(w + 1.6 * fbm(w * 0.8 + 3.1));
|
||||||
|
return clamp(1.0 - abs(n - 0.48) * 4.2, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
// where a particle is reborn: best of four candidates against the density field
|
||||||
|
vec3 spawnDir(uint idx, float epoch, float t) {
|
||||||
|
uint base = pcg(idx * 2246822519u + uint(epoch) * 3266489917u + 17u);
|
||||||
|
vec3 best = vec3(0.0, 1.0, 0.0);
|
||||||
|
float bn = -1.0;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
uint s = base + uint(i) * 374761393u;
|
||||||
|
vec3 d = sphDir(h01(s), h01(s + 1u));
|
||||||
|
float n = density(d, t);
|
||||||
|
if (n > bn) { bn = n; best = d; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
// the spawn point of a particle's current life; the flow carries it away, a soft pull brings it back
|
||||||
|
vec3 homeOf(uint idx, float epoch) {
|
||||||
|
float tSpawn = (epoch - h01(idx * 8u + 2u)) * lifeLen(idx);
|
||||||
|
return spawnDir(idx, epoch, tSpawn) * layerR(idx);
|
||||||
|
}
|
||||||
|
vec3 bodyToView(vec3 p) { return rotX(rotY(p, uTime * 0.045 + 2.4 + uTilt.x), uTilt.y); }
|
||||||
|
vec3 viewToBody(vec3 v) { return rotY(rotX(v, -uTilt.y), -(uTime * 0.045 + 2.4 + uTilt.x)); }
|
||||||
|
|
||||||
|
// ---- corona: sparks thrown off the limb, laid out in screen space; strongest over the top
|
||||||
struct P {
|
struct P {
|
||||||
vec2 xy;
|
vec2 xy;
|
||||||
float z;
|
|
||||||
float live;
|
|
||||||
float kind; // 0 inner, 1 corona
|
|
||||||
float n;
|
float n;
|
||||||
float spark;
|
|
||||||
float reach;
|
float reach;
|
||||||
float fade;
|
float fade;
|
||||||
vec4 r;
|
vec4 r;
|
||||||
};
|
};
|
||||||
|
|
||||||
P rest(uint idx) {
|
P corona(uint idx) {
|
||||||
P o;
|
P o;
|
||||||
o.r = vec4(h01(idx * 8u), h01(idx * 8u + 1u), h01(idx * 8u + 2u), h01(idx * 8u + 3u));
|
o.r = vec4(h01(idx * 8u), h01(idx * 8u + 1u), h01(idx * 8u + 2u), h01(idx * 8u + 3u));
|
||||||
vec4 q = vec4(h01(idx * 8u + 4u), h01(idx * 8u + 5u), h01(idx * 8u + 6u), h01(idx * 8u + 7u));
|
vec4 q = vec4(h01(idx * 8u + 4u), h01(idx * 8u + 5u), h01(idx * 8u + 6u), h01(idx * 8u + 7u));
|
||||||
o.live = 1.0; o.spark = 0.0; o.n = 0.0; o.reach = 0.0; o.fade = 0.0; o.z = 0.0;
|
|
||||||
float t = uTime;
|
float t = uTime;
|
||||||
|
|
||||||
if (idx < uInner) {
|
|
||||||
// inner particles: density follows a slowly drifting, domain-warped noise field
|
|
||||||
o.kind = 0.0;
|
|
||||||
float y = q.x * 2.0 - 1.0;
|
|
||||||
float phi = q.y * 6.2831853;
|
|
||||||
float s = sqrt(1.0 - y * y);
|
|
||||||
vec3 d = vec3(cos(phi) * s, y, sin(phi) * s);
|
|
||||||
float rad = 0.955 + o.r.x * 0.04;
|
|
||||||
vec3 w = d * 1.45 + vec3(0.0, t * 0.03, t * 0.02);
|
|
||||||
float n = fbm(w + 1.6 * fbm(w * 0.8 + 3.1));
|
|
||||||
n = clamp((n - 0.46) * 2.3 + 0.5, 0.0, 1.0);
|
|
||||||
float keep = smoothstep(0.05, 0.7, n) - o.r.y * 0.7;
|
|
||||||
o.spark = step(0.92, o.r.z);
|
|
||||||
o.n = n;
|
|
||||||
if (keep < 0.0 && o.spark < 0.5) o.live = 0.0;
|
|
||||||
vec3 p = rotX(rotY(d, t * 0.045 + 2.4 + uTilt.x), uTilt.y) * rad;
|
|
||||||
float persp = 1.0 / (1.0 - p.z * 0.12);
|
|
||||||
o.xy = p.xy * persp;
|
|
||||||
o.z = p.z;
|
|
||||||
} else {
|
|
||||||
// corona: sparks thrown off the limb, laid out in screen space; strongest over the top
|
|
||||||
o.kind = 1.0;
|
|
||||||
float a = q.x * 6.2831853;
|
float a = q.x * 6.2831853;
|
||||||
vec2 cd = vec2(cos(a), sin(a));
|
vec2 cd = vec2(cos(a), sin(a));
|
||||||
float n = fbm(vec3(cd * 4.2, t * 0.10));
|
float n = fbm(vec3(cd * 4.2, t * 0.10));
|
||||||
@ -115,7 +153,6 @@ P rest(uint idx) {
|
|||||||
o.reach = reach;
|
o.reach = reach;
|
||||||
o.fade = 1.0 - smoothstep(0.0, 0.42, reach);
|
o.fade = 1.0 - smoothstep(0.0, 0.42, reach);
|
||||||
o.xy = cd * (1.0 + 0.004 + reach);
|
o.xy = cd * (1.0 + 0.004 + reach);
|
||||||
}
|
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -261,7 +298,8 @@ void main() { o = uValue * texture(uTex, vUv); }
|
|||||||
|
|
||||||
const FRAG_STATE = `${HEAD}
|
const FRAG_STATE = `${HEAD}
|
||||||
${COMMON}
|
${COMMON}
|
||||||
uniform sampler2D uState;
|
uniform sampler2D uPos; // inner: body-space xyz (orb radii), w = stir. corona: xy displacement
|
||||||
|
uniform sampler2D uVel; // velocity, same layout
|
||||||
uniform sampler2D uFlow;
|
uniform sampler2D uFlow;
|
||||||
uniform int uT;
|
uniform int uT;
|
||||||
uniform uint uTotal;
|
uniform uint uTotal;
|
||||||
@ -271,43 +309,95 @@ uniform float uR; // css px
|
|||||||
uniform vec2 uSim; // flow texture size in texels
|
uniform vec2 uSim; // flow texture size in texels
|
||||||
uniform float uDt;
|
uniform float uDt;
|
||||||
uniform float uGain;
|
uniform float uGain;
|
||||||
out vec4 o;
|
uniform float uAmb; // 0 freezes the ambient currents (reduced motion)
|
||||||
|
uniform int uInit;
|
||||||
|
layout(location = 0) out vec4 oPos;
|
||||||
|
layout(location = 1) out vec4 oVel;
|
||||||
|
|
||||||
|
// fluid velocity (texels/s) under a view-space point (orb radii, y up) -> orb radii per second
|
||||||
|
vec2 flowAt(vec2 sp) {
|
||||||
|
vec2 px = uCenter + vec2(sp.x, -sp.y) * uR;
|
||||||
|
vec2 uv = vec2(px.x / uView.x, 1.0 - px.y / uView.y);
|
||||||
|
vec2 f = texture(uFlow, uv).xy;
|
||||||
|
return vec2(f.x / uSim.x * uView.x, f.y / uSim.y * uView.y) / uR;
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
ivec2 ip = ivec2(gl_FragCoord.xy);
|
ivec2 ip = ivec2(gl_FragCoord.xy);
|
||||||
uint idx = uint(ip.y * uT + ip.x);
|
uint idx = uint(ip.y * uT + ip.x);
|
||||||
if (idx >= uTotal) { o = vec4(0.0); return; }
|
if (idx >= uTotal) { oPos = vec4(0.0); oVel = vec4(0.0); return; }
|
||||||
P p = rest(idx);
|
vec4 s0 = texelFetch(uPos, ip, 0);
|
||||||
if (p.live < 0.5) { o = vec4(0.0); return; }
|
vec4 s1 = texelFetch(uVel, ip, 0);
|
||||||
|
float t = uTime;
|
||||||
|
|
||||||
vec4 st = texelFetch(uState, ip, 0);
|
if (idx >= uInner) {
|
||||||
vec2 disp = st.xy;
|
// corona: a spring pulls each spark home, the flow and a light wind shake it
|
||||||
vec2 vel = st.zw;
|
P p = corona(idx);
|
||||||
|
vec2 disp = s0.xy;
|
||||||
// sample the flow where the particle currently is
|
vec2 vel = s1.xy;
|
||||||
vec2 pos = p.xy + disp;
|
vec2 flow = flowAt(p.xy + disp);
|
||||||
vec2 px = uCenter + vec2(pos.x, -pos.y) * uR;
|
vec3 np = vec3(p.xy * 2.4 + vec2(0.0, t * 0.05), t * 0.16);
|
||||||
vec2 uv = vec2(px.x / uView.x, 1.0 - px.y / uView.y);
|
|
||||||
vec2 f = texture(uFlow, uv).xy; // texels per second
|
|
||||||
vec2 flow = vec2(f.x / uSim.x * uView.x, f.y / uSim.y * uView.y) / uR; // radii per second
|
|
||||||
|
|
||||||
// ambient shimmer: a slow curl-noise wind keeps the particles streaming
|
|
||||||
vec3 np = vec3(p.xy * 2.4 + vec2(0.0, uTime * 0.05), uTime * 0.16);
|
|
||||||
float e = 0.03;
|
float e = 0.03;
|
||||||
float nx = vnoise(np + vec3(e, 0.0, 0.0)) - vnoise(np - vec3(e, 0.0, 0.0));
|
float nx = vnoise(np + vec3(e, 0.0, 0.0)) - vnoise(np - vec3(e, 0.0, 0.0));
|
||||||
float ny = vnoise(np + vec3(0.0, e, 0.0)) - vnoise(np - vec3(0.0, e, 0.0));
|
float ny = vnoise(np + vec3(0.0, e, 0.0)) - vnoise(np - vec3(0.0, e, 0.0));
|
||||||
vec2 wind = vec2(ny, -nx) / (2.0 * e);
|
vec2 wind = vec2(ny, -nx) / (2.0 * e);
|
||||||
|
float k = 10.5 * (0.65 + 0.7 * p.r.w);
|
||||||
float light = p.kind; // corona sparks are lighter
|
float c = 2.0 * sqrt(k) * 0.45;
|
||||||
float k = (8.0 + 2.5 * light) * (0.65 + 0.7 * p.r.w); // spring back home
|
vec2 acc = -k * disp - c * vel + 15.0 * (flow * uGain * 1.8 - vel) + wind * 0.84 * uAmb;
|
||||||
float c = 2.0 * sqrt(k) * 0.45; // under-damped: a little bounce on the way home
|
|
||||||
float follow = 9.0 + 6.0 * light; // how hard the flow drags it
|
|
||||||
vec2 acc = -k * disp - c * vel + follow * (flow * uGain * (1.0 + 0.8 * light) - vel) + wind * (0.14 + 0.7 * light);
|
|
||||||
vel += acc * uDt;
|
vel += acc * uDt;
|
||||||
disp += vel * uDt;
|
disp += vel * uDt;
|
||||||
float m = length(disp);
|
float m = length(disp);
|
||||||
if (m > 0.9) disp *= 0.9 / m;
|
if (m > 0.9) disp *= 0.9 / m;
|
||||||
o = vec4(disp, vel);
|
oPos = vec4(disp, 0.0, 0.0);
|
||||||
|
oVel = vec4(vel, 0.0, 0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// inner particle
|
||||||
|
float rt = layerR(idx);
|
||||||
|
float lt = lifeT(idx, t);
|
||||||
|
vec3 pos = s0.xyz;
|
||||||
|
vec3 vel = s1.xyz;
|
||||||
|
float stir = s0.w;
|
||||||
|
vec3 home = homeOf(idx, floor(lt));
|
||||||
|
if (uInit == 1 || floor(lt) != floor(lifeT(idx, t - uDt))) {
|
||||||
|
pos = home;
|
||||||
|
vel = vec3(0.0);
|
||||||
|
stir = 0.0;
|
||||||
|
}
|
||||||
|
float r = length(pos);
|
||||||
|
vec3 n = pos / max(r, 1e-4);
|
||||||
|
float surf = step(0.9, rt);
|
||||||
|
|
||||||
|
// the cursor: the 2D fluid under the particle's screen position, pushed into 3D
|
||||||
|
vec3 bp = bodyToView(pos);
|
||||||
|
vec2 flow = flowAt(bp.xy / (1.0 - bp.z * 0.12));
|
||||||
|
float fm = length(flow);
|
||||||
|
float front = mix(0.35, 1.0, smoothstep(-0.5, 0.5, bp.z));
|
||||||
|
vec3 push = viewToBody(vec3(flow, 0.0)) * uGain * front;
|
||||||
|
// hard stirring also tumbles particles through depth, so the volume churns instead of sliding sideways
|
||||||
|
vec3 tumble = curlNoise(pos * 4.0 + vec3(0.0, 0.0, t * 0.9)) * fm * 0.7 * front;
|
||||||
|
vec3 depth = n * (h01(idx * 8u + 7u) - 0.5) * fm * 0.9 * front;
|
||||||
|
|
||||||
|
// idle: slow broad currents plus finer eddies, kept mostly along the skin so streaks pile up and shear
|
||||||
|
vec3 amb = curlNoise(pos * 1.35 + vec3(0.0, 0.0, t * 0.35))
|
||||||
|
+ 0.6 * curlNoise(pos * 3.2 + vec3(t * 0.15, 0.0, t * 0.7));
|
||||||
|
amb -= n * dot(amb, n) * mix(0.35, 0.8, surf);
|
||||||
|
amb *= 0.3 * uAmb;
|
||||||
|
|
||||||
|
vec3 target = amb + push + tumble + depth + (home - pos) * 0.3;
|
||||||
|
vel += (target - vel) * (1.0 - exp(-uDt * 3.0));
|
||||||
|
pos += vel * uDt;
|
||||||
|
|
||||||
|
// soft restoring pull to its layer
|
||||||
|
r = length(pos);
|
||||||
|
n = pos / max(r, 1e-4);
|
||||||
|
r = mix(r, rt, 1.0 - exp(-uDt * 1.6));
|
||||||
|
pos = n * r;
|
||||||
|
|
||||||
|
stir = max(stir * exp(-uDt * 1.2), clamp(fm * front * 1.5, 0.0, 1.5));
|
||||||
|
oPos = vec4(pos, stir);
|
||||||
|
oVel = vec4(vel, 0.0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -315,8 +405,10 @@ void main() {
|
|||||||
|
|
||||||
const VERT_PARTICLES = `${HEAD}
|
const VERT_PARTICLES = `${HEAD}
|
||||||
${COMMON}
|
${COMMON}
|
||||||
uniform sampler2D uState;
|
uniform sampler2D uPos;
|
||||||
|
uniform sampler2D uVel;
|
||||||
uniform int uT;
|
uniform int uT;
|
||||||
|
uniform int uStatic; // no float render targets: particles sit at their spawn points
|
||||||
uniform vec2 uView; // css px
|
uniform vec2 uView; // css px
|
||||||
uniform vec2 uCenter; // css px
|
uniform vec2 uCenter; // css px
|
||||||
uniform float uR; // css px
|
uniform float uR; // css px
|
||||||
@ -326,38 +418,53 @@ out float vGlow;
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
uint idx = uint(gl_VertexID);
|
uint idx = uint(gl_VertexID);
|
||||||
P p = rest(idx);
|
ivec2 tc = ivec2(int(idx) % uT, int(idx) / uT);
|
||||||
if (p.live < 0.5) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); gl_PointSize = 0.0; vColor = vec4(0.0); vGlow = 0.0; return; }
|
vec4 s0 = texelFetch(uPos, tc, 0);
|
||||||
|
vec4 s1 = texelFetch(uVel, tc, 0);
|
||||||
vec4 st = texelFetch(uState, ivec2(int(idx) % uT, int(idx) / uT), 0);
|
float t = uTime;
|
||||||
vec2 disp = st.xy;
|
|
||||||
float speed = length(st.zw);
|
|
||||||
float stir = clamp(length(disp) * 1.6 + speed * 0.35, 0.0, 1.5); // how disturbed this particle is
|
|
||||||
|
|
||||||
vec2 px = uCenter + vec2(p.xy.x + disp.x, -(p.xy.y + disp.y)) * uR;
|
|
||||||
float sc = clamp(uR / 315.0, 0.8, 1.7);
|
float sc = clamp(uR / 315.0, 0.8, 1.7);
|
||||||
|
vec2 vp;
|
||||||
vec4 col;
|
vec4 col;
|
||||||
float size;
|
float size;
|
||||||
float persp = 1.0;
|
float persp = 1.0;
|
||||||
|
|
||||||
if (p.kind < 0.5) {
|
if (idx < uInner) {
|
||||||
vec3 warm = mix(vec3(0.94, 0.27, 0.20), vec3(1.0, 0.52, 0.32), p.r.w);
|
vec4 r = vec4(h01(idx * 8u), h01(idx * 8u + 1u), h01(idx * 8u + 2u), h01(idx * 8u + 3u));
|
||||||
warm = mix(warm, vec3(1.0, 0.72, 0.42), smoothstep(0.1, 0.45, 1.0 - p.n) * 0.8);
|
float spark = step(0.88, h01(idx * 8u + 6u));
|
||||||
col = vec4(mix(warm, vec3(1.0, 0.97, 0.84), p.spark), mix(0.85, 0.95, p.spark));
|
float rt = layerR(idx);
|
||||||
size = mix(1.25 + p.r.y * 1.3 + p.n * 0.9, 2.6 + p.r.w * 2.4, p.spark);
|
float lt = lifeT(idx, t);
|
||||||
float tw = 0.5 + 0.5 * sin(uTime * (1.2 + p.r.x * 2.5) + p.r.w * 40.0);
|
vec3 pos = s0.xyz;
|
||||||
col.a *= mix(1.0, tw, p.spark);
|
float stir = clamp(s0.w, 0.0, 1.5);
|
||||||
vGlow = p.spark;
|
if (uStatic == 1) { pos = homeOf(idx, floor(lt)); stir = 0.0; }
|
||||||
// far side of the sphere reads dimmer through the glass
|
|
||||||
col.a *= mix(0.25, 1.0, smoothstep(-0.4, 0.4, p.z));
|
vec3 bp = bodyToView(pos);
|
||||||
persp = 1.0 / (1.0 - p.z * 0.12);
|
persp = 1.0 / (1.0 - bp.z * 0.12);
|
||||||
|
vp = bp.xy * persp;
|
||||||
|
|
||||||
|
float n = density(normalize(pos + vec3(1e-5)), t);
|
||||||
|
vec3 warm = mix(vec3(0.94, 0.27, 0.20), vec3(1.0, 0.52, 0.32), r.w);
|
||||||
|
warm = mix(warm, vec3(1.0, 0.72, 0.42), smoothstep(0.1, 0.45, 1.0 - n) * 0.8);
|
||||||
|
col = vec4(mix(warm, vec3(1.0, 0.97, 0.84), spark), mix(0.95, 0.95, spark));
|
||||||
|
size = mix(1.15 + r.y * 1.2 + n * 0.8, 2.0 + r.w * 2.0, spark);
|
||||||
|
float tw = 0.5 + 0.5 * sin(t * (1.2 + r.x * 2.5) + r.w * 40.0);
|
||||||
|
col.a *= mix(1.0, tw, spark);
|
||||||
|
vGlow = spark;
|
||||||
|
// the far side reads dimmer through the glass, and so do particles hanging deep in the volume
|
||||||
|
// (it also thins the crowd that edge-on particles would form along the limb)
|
||||||
|
col.a *= mix(0.2, 1.0, smoothstep(-0.2, 0.6, bp.z));
|
||||||
|
col.a *= mix(0.65, 1.0, step(0.9, rt));
|
||||||
|
col.a *= lifeAlpha(lt);
|
||||||
// stirred particles heat up: brighter, hotter, a little bigger
|
// stirred particles heat up: brighter, hotter, a little bigger
|
||||||
col.rgb = mix(col.rgb, vec3(1.0, 0.8, 0.55), clamp(stir * 0.3, 0.0, 0.4));
|
col.rgb = mix(col.rgb, vec3(1.0, 0.8, 0.55), clamp(stir * 0.3, 0.0, 0.4));
|
||||||
col.a = min(1.0, col.a * (1.0 + stir * 0.5));
|
col.a = min(1.0, col.a * (1.0 + stir * 0.5));
|
||||||
size *= 1.0 + stir * 0.8;
|
size *= 1.0 + stir * 0.8;
|
||||||
vGlow = max(vGlow, clamp(stir * 0.8, 0.0, 1.0));
|
vGlow = max(vGlow, clamp(stir * 0.8, 0.0, 1.0));
|
||||||
} else {
|
} else {
|
||||||
float tw = 0.5 + 0.5 * sin(uTime * (1.0 + p.r.y * 3.0) + p.r.z * 60.0);
|
P p = corona(idx);
|
||||||
|
vec2 disp = s0.xy;
|
||||||
|
float stir = clamp(length(disp) * 1.6 + length(s1.xy) * 0.35, 0.0, 1.5);
|
||||||
|
vp = p.xy + disp;
|
||||||
|
float tw = 0.5 + 0.5 * sin(t * (1.0 + p.r.y * 3.0) + p.r.z * 60.0);
|
||||||
col = vec4(1.0, 0.97, 0.86, (0.3 + 0.7 * p.fade) * (0.35 + 0.65 * smoothstep(0.2, 0.75, p.n)));
|
col = vec4(1.0, 0.97, 0.86, (0.3 + 0.7 * p.fade) * (0.35 + 0.65 * smoothstep(0.2, 0.75, p.n)));
|
||||||
col.a *= mix(0.5, 1.0, tw);
|
col.a *= mix(0.5, 1.0, tw);
|
||||||
size = 1.0 + p.r.y * p.r.y * 2.6 * (0.3 + p.fade);
|
size = 1.0 + p.r.y * p.r.y * 2.6 * (0.3 + p.fade);
|
||||||
@ -369,6 +476,7 @@ void main() {
|
|||||||
size *= 1.0 + stir * 0.9;
|
size *= 1.0 + stir * 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vec2 px = uCenter + vec2(vp.x, -vp.y) * uR;
|
||||||
gl_Position = vec4(px.x / uView.x * 2.0 - 1.0, 1.0 - px.y / uView.y * 2.0, 0.0, 1.0);
|
gl_Position = vec4(px.x / uView.x * 2.0 - 1.0, 1.0 - px.y / uView.y * 2.0, 0.0, 1.0);
|
||||||
gl_PointSize = size * sc * uDpr * persp;
|
gl_PointSize = size * sc * uDpr * persp;
|
||||||
vColor = col;
|
vColor = col;
|
||||||
@ -514,7 +622,7 @@ function program(gl, vs, fs) {
|
|||||||
return { p, u: uniforms };
|
return { p, u: uniforms };
|
||||||
}
|
}
|
||||||
|
|
||||||
function target(gl, w, h, internal, type, filter) {
|
function texture(gl, w, h, internal, type, filter) {
|
||||||
const tex = gl.createTexture();
|
const tex = gl.createTexture();
|
||||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, internal, w, h, 0, gl.RGBA, type, null);
|
gl.texImage2D(gl.TEXTURE_2D, 0, internal, w, h, 0, gl.RGBA, type, null);
|
||||||
@ -522,22 +630,34 @@ function target(gl, w, h, internal, type, filter) {
|
|||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dual: a second colour attachment (tex2) is rendered alongside the first
|
||||||
|
function target(gl, w, h, internal, type, filter, dual = false) {
|
||||||
|
const tex = texture(gl, w, h, internal, type, filter);
|
||||||
|
const tex2 = dual ? texture(gl, w, h, internal, type, filter) : null;
|
||||||
const fbo = gl.createFramebuffer();
|
const fbo = gl.createFramebuffer();
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
||||||
|
if (dual) {
|
||||||
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, tex2, 0);
|
||||||
|
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
|
||||||
|
}
|
||||||
gl.viewport(0, 0, w, h);
|
gl.viewport(0, 0, w, h);
|
||||||
gl.clearColor(0, 0, 0, 0);
|
gl.clearColor(0, 0, 0, 0);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
return { tex, fbo, w, h };
|
return { tex, tex2, fbo, w, h };
|
||||||
}
|
}
|
||||||
|
|
||||||
function freeTarget(gl, t) {
|
function freeTarget(gl, t) {
|
||||||
gl.deleteTexture(t.tex);
|
gl.deleteTexture(t.tex);
|
||||||
|
if (t.tex2) gl.deleteTexture(t.tex2);
|
||||||
gl.deleteFramebuffer(t.fbo);
|
gl.deleteFramebuffer(t.fbo);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pingPong(gl, w, h, internal, type, filter) {
|
function pingPong(gl, w, h, internal, type, filter, dual = false) {
|
||||||
const pair = [target(gl, w, h, internal, type, filter), target(gl, w, h, internal, type, filter)];
|
const pair = [target(gl, w, h, internal, type, filter, dual), target(gl, w, h, internal, type, filter, dual)];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
return {
|
return {
|
||||||
get read() { return pair[i]; },
|
get read() { return pair[i]; },
|
||||||
@ -564,9 +684,9 @@ export function startOrb(canvas, { onReady } = {}) {
|
|||||||
const coarse = matchMedia('(pointer: coarse)').matches;
|
const coarse = matchMedia('(pointer: coarse)').matches;
|
||||||
const small = Math.min(innerWidth, innerHeight) < 600;
|
const small = Math.min(innerWidth, innerHeight) < 600;
|
||||||
|
|
||||||
const T = small ? 192 : 256;
|
const T = small ? 192 : 320;
|
||||||
const INNER = small ? 26000 : 52000;
|
const INNER = small ? 26000 : 82000;
|
||||||
const CORONA = small ? 8000 : 13000;
|
const CORONA = small ? 8000 : 18000;
|
||||||
const TOTAL = INNER + CORONA;
|
const TOTAL = INNER + CORONA;
|
||||||
|
|
||||||
let progs;
|
let progs;
|
||||||
@ -596,8 +716,9 @@ export function startOrb(canvas, { onReady } = {}) {
|
|||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
// particle state: displacement.xy + velocity.xy per particle
|
// particle state, two textures: position (xyz + stir) and velocity (xyz) per particle
|
||||||
const state = interactive ? pingPong(gl, T, T, stateFmt.internal, stateFmt.type, gl.NEAREST) : null;
|
const state = interactive ? pingPong(gl, T, T, stateFmt.internal, stateFmt.type, gl.NEAREST, true) : null;
|
||||||
|
let needInit = true;
|
||||||
|
|
||||||
// fluid fields; rebuilt when the viewport shape changes so the grid stays square-celled
|
// fluid fields; rebuilt when the viewport shape changes so the grid stays square-celled
|
||||||
let velocity, dye, pressure, divergence, curl, simW = 1, simH = 1;
|
let velocity, dye, pressure, divergence, curl, simW = 1, simH = 1;
|
||||||
@ -738,8 +859,8 @@ export function startOrb(canvas, { onReady } = {}) {
|
|||||||
into(dye.write); tri(); dye.swap();
|
into(dye.write); tri(); dye.swap();
|
||||||
}
|
}
|
||||||
|
|
||||||
const t0 = performance.now();
|
let simT = reduced ? 8 : 0;
|
||||||
let last = t0;
|
let last = performance.now();
|
||||||
let ready = false;
|
let ready = false;
|
||||||
|
|
||||||
function commonUniforms(prog, t) {
|
function commonUniforms(prog, t) {
|
||||||
@ -751,7 +872,9 @@ export function startOrb(canvas, { onReady } = {}) {
|
|||||||
function frame(now) {
|
function frame(now) {
|
||||||
const dt = Math.min((now - last) / 1000, 1 / 30) || 1 / 60;
|
const dt = Math.min((now - last) / 1000, 1 / 30) || 1 / 60;
|
||||||
last = now;
|
last = now;
|
||||||
const t = reduced ? 8 : (now - t0) / 1000;
|
// simulation clock: advances by clamped steps, so a backgrounded tab resumes without a jump
|
||||||
|
if (!reduced) simT += dt;
|
||||||
|
const t = simT;
|
||||||
par.cx += (par.x - par.cx) * 0.04;
|
par.cx += (par.x - par.cx) * 0.04;
|
||||||
par.cy += (par.y - par.cy) * 0.04;
|
par.cy += (par.y - par.cy) * 0.04;
|
||||||
const ox = par.cx * 10, oy = par.cy * 6;
|
const ox = par.cx * 10, oy = par.cy * 6;
|
||||||
@ -763,8 +886,12 @@ export function startOrb(canvas, { onReady } = {}) {
|
|||||||
const s = progs.state;
|
const s = progs.state;
|
||||||
gl.useProgram(s.p);
|
gl.useProgram(s.p);
|
||||||
commonUniforms(s, t);
|
commonUniforms(s, t);
|
||||||
gl.uniform1i(s.u.uState, bind(0, state.read.tex));
|
gl.uniform1i(s.u.uPos, bind(0, state.read.tex));
|
||||||
gl.uniform1i(s.u.uFlow, bind(1, velocity.read.tex));
|
gl.uniform1i(s.u.uVel, bind(1, state.read.tex2));
|
||||||
|
gl.uniform1i(s.u.uFlow, bind(2, velocity.read.tex));
|
||||||
|
gl.uniform1f(s.u.uAmb, reduced ? 0 : 1);
|
||||||
|
gl.uniform1i(s.u.uInit, needInit ? 1 : 0);
|
||||||
|
needInit = false;
|
||||||
gl.uniform1i(s.u.uT, T);
|
gl.uniform1i(s.u.uT, T);
|
||||||
gl.uniform1ui(s.u.uTotal, TOTAL);
|
gl.uniform1ui(s.u.uTotal, TOTAL);
|
||||||
gl.uniform2f(s.u.uCenter, cx + ox, cy + oy);
|
gl.uniform2f(s.u.uCenter, cx + ox, cy + oy);
|
||||||
@ -792,12 +919,15 @@ export function startOrb(canvas, { onReady } = {}) {
|
|||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
const stateTex = interactive ? state.read.tex : zeroTex;
|
const posTex = interactive ? state.read.tex : zeroTex;
|
||||||
|
const velTex = interactive ? state.read.tex2 : zeroTex;
|
||||||
const useParticles = () => {
|
const useParticles = () => {
|
||||||
const p = progs.particles;
|
const p = progs.particles;
|
||||||
gl.useProgram(p.p);
|
gl.useProgram(p.p);
|
||||||
commonUniforms(p, t);
|
commonUniforms(p, t);
|
||||||
gl.uniform1i(p.u.uState, bind(0, stateTex));
|
gl.uniform1i(p.u.uPos, bind(0, posTex));
|
||||||
|
gl.uniform1i(p.u.uVel, bind(1, velTex));
|
||||||
|
gl.uniform1i(p.u.uStatic, interactive ? 0 : 1);
|
||||||
gl.uniform1i(p.u.uT, T);
|
gl.uniform1i(p.u.uT, T);
|
||||||
gl.uniform2f(p.u.uView, W, H);
|
gl.uniform2f(p.u.uView, W, H);
|
||||||
gl.uniform2f(p.u.uCenter, cx + ox, cy + oy);
|
gl.uniform2f(p.u.uCenter, cx + ox, cy + oy);
|
||||||
|
|||||||
Reference in New Issue
Block a user