radiance: corona sparks now have a lifecycle — born on the rim, drift outward, shrink and fizzle out; cursor pushes carry them, nothing pulls them back

This commit is contained in:
dangrubbb
2026-10-24 09:41:32 -04:00
parent fcc1326e86
commit cfd081abcd

View File

@ -133,32 +133,48 @@ vec3 homeOf(uint idx, float epoch) {
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
// ---- corona: sparks born on the rim, laid out in screen space, that drift outward and fizzle out.
// Each is reborn at a fresh spot on the rim (favouring tufts over the top) when its life ends.
struct P {
vec2 xy;
float n;
float reach;
float fade;
vec2 xy; // where the spark is, before any cursor push
float n; // strength of the tuft it was born in
float life; // 0 at birth .. 1 at death
float fade; // fades in at birth, out toward death
vec4 r;
};
float coronaLen(uint idx) { return mix(3.5, 8.5, h01(idx * 8u + 1u)); }
float coronaLT(uint idx, float t) { return t / coronaLen(idx) + h01(idx * 8u + 2u); }
P corona(uint idx) {
P o;
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));
float t = uTime;
float a = q.x * 6.2831853;
vec2 cd = vec2(cos(a), sin(a));
float n = fbm(vec3(cd * 4.2, t * 0.10));
float up = smoothstep(-0.75, 0.9, cd.y);
float lt = coronaLT(idx, uTime);
float epoch = floor(lt);
o.life = fract(lt);
float tSpawn = (epoch - o.r.z) * coronaLen(idx);
// birthplace: best of three rim angles, scored by tuft strength and how far over the top it is
uint base = pcg(idx * 2654435761u + uint(epoch) * 40503u + 91u);
vec2 cd = vec2(0.0, 1.0);
float n = 0.0, up = 0.0, best = -1.0;
for (int i = 0; i < 3; i++) {
float a = h01(base + uint(i) * 374761393u) * 6.2831853;
vec2 c = vec2(cos(a), sin(a));
float nn = fbm(vec3(c * 4.2, tSpawn * 0.10));
float u = smoothstep(-0.75, 0.9, c.y);
float score = (0.25 + 0.75 * u) * (0.3 + nn);
if (score > best) { best = score; cd = c; n = nn; up = u; }
}
float spike = pow(n, 1.6) * 1.6;
float reach = pow(o.r.x, 1.3) * (0.07 + 0.5 * spike) * (0.3 + 0.7 * up);
reach *= 0.88 + 0.12 * sin(t * 0.7 + o.r.w * 20.0);
reach += 0.012 * sin(t * 0.9 + o.r.z * 30.0);
float travel = pow(o.r.x, 1.3) * (0.07 + 0.5 * spike) * (0.3 + 0.7 * up) + 0.02;
float ease = 1.0 - pow(1.0 - o.life, 1.35); // a steady drift that slows toward the end
float ang = (o.r.w - 0.5) * 0.08 * o.life; // a little sideways drift
float ca = cos(ang), sa = sin(ang);
vec2 dir = vec2(ca * cd.x - sa * cd.y, sa * cd.x + ca * cd.y);
o.n = n;
o.reach = reach;
o.fade = 1.0 - smoothstep(0.0, 0.42, reach);
o.xy = cd * (1.0 + 0.004 + reach);
o.xy = dir * (1.004 + travel * ease);
o.fade = smoothstep(0.0, 0.04, o.life) * (1.0 - smoothstep(0.12, 1.0, o.life)); // brightest at the rim, dissolving with distance
return o;
}
`;
@ -337,23 +353,22 @@ void main() {
float t = uTime;
if (idx >= uInner) {
// corona: a spring pulls each spark home, the flow and a light wind shake it
// corona: the cursor flow and a light wind carry a spark along; nothing pulls it back,
// it just lives out its life and dies (a rebirth clears the push)
P p = corona(idx);
vec2 disp = s0.xy;
vec2 vel = s1.xy;
if (uInit == 1 || floor(coronaLT(idx, t)) != floor(coronaLT(idx, t - uDt))) { disp = vec2(0.0); vel = vec2(0.0); }
vec2 flow = flowAt(p.xy + disp);
vec3 np = vec3(p.xy * 2.4 + vec2(0.0, t * 0.05), t * 0.16);
float e = 0.03;
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));
vec2 wind = vec2(ny, -nx) / (2.0 * e);
float k = 10.5 * (0.65 + 0.7 * p.r.w);
float c = 2.0 * sqrt(k) * 0.45;
vec2 acc = -k * disp - c * vel + 15.0 * (flow * uGain * 1.8 - vel) + wind * 0.84 * uAmb;
vel += acc * uDt;
vel += (flow * uGain * 1.8 + wind * 0.05 * uAmb - vel) * (1.0 - exp(-uDt * 3.0));
disp += vel * uDt;
float m = length(disp);
if (m > 0.9) disp *= 0.9 / m;
if (m > 1.2) disp *= 1.2 / m;
oPos = vec4(disp, 0.0, 0.0);
oVel = vec4(vel, 0.0, 0.0);
return;
@ -474,21 +489,18 @@ void main() {
} else {
P p = corona(idx);
vec2 disp = s0.xy;
float stir = clamp(length(disp) * 1.6 + length(s1.xy) * 0.35, 0.0, 1.5);
float stir = clamp(length(s1.xy) * 0.35, 0.0, 1.0);
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.98, 0.88, (0.45 + 0.55 * p.fade) * (0.5 + 0.5 * smoothstep(0.2, 0.75, p.n)));
col = vec4(1.0, 0.98, 0.88, 0.95 * p.fade * (0.55 + 0.45 * smoothstep(0.2, 0.75, p.n)));
col.a *= mix(0.55, 1.0, tw);
// a minority of sparks are bright, haloed points; the rest stay faint dust
float lead = step(0.7, p.r.x);
col.a *= mix(0.7, 1.0, lead);
size = (1.0 + p.r.y * p.r.y * 2.8 * (0.3 + p.fade)) * mix(0.9, 1.5, lead);
size = (1.0 + p.r.y * p.r.y * 2.8) * mix(0.9, 1.5, lead) * (1.0 - 0.5 * p.life); // shrinking as it dies
vGlow = mix(0.4, 1.0, lead);
// sparks that get knocked off the limb flare, then fade out while they are away from home
float away = smoothstep(0.03, 0.5, length(disp));
col.a *= 1.0 - 0.8 * away;
col.a = min(1.0, col.a * (1.0 + stir * 0.8));
size *= 1.0 + stir * 0.9;
size *= 1.0 + stir * 0.5;
}
vec2 px = uCenter + vec2(vp.x, -vp.y) * uR;