@dopaminefx/effect-lightning 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/lightning-logic.d.ts +60 -0
- package/dist/lightning-logic.d.ts.map +1 -0
- package/dist/lightning-logic.js +200 -0
- package/dist/lightning-logic.js.map +1 -0
- package/dist/lightning-shader.d.ts +35 -0
- package/dist/lightning-shader.d.ts.map +1 -0
- package/dist/lightning-shader.js +209 -0
- package/dist/lightning-shader.js.map +1 -0
- package/dist/lightning.dope.json +495 -0
- package/package.json +46 -0
- package/src/index.ts +80 -0
- package/src/lightning-logic.ts +221 -0
- package/src/lightning-shader.ts +220 -0
- package/src/lightning.dope.json +495 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightning bolt geometry precompute — the SINGLE cross-platform source.
|
|
3
|
+
*
|
|
4
|
+
* The bolt polyline is fragment-INDEPENDENT, so it is computed ONCE per frame
|
|
5
|
+
* here (a faithful port of the shared fbm/hash + the original shader
|
|
6
|
+
* `boltPoint`) and fed to the shader as the `uVerts` / `uBoltMeta` arrays. The
|
|
7
|
+
* shader keeps the exact original inverse-distance plasma glow; only the cost
|
|
8
|
+
* moved off the per-pixel path.
|
|
9
|
+
*
|
|
10
|
+
* THIS FILE IS TRANSPILED to Swift (`LightningRenderer.swift`) and Kotlin
|
|
11
|
+
* (`LightningRenderer.kt`) by `tools/dopamine/src/logic.mjs` (declared by the
|
|
12
|
+
* `.dope` `x-build.logic` block) — the per-frame-geometry analog of the scoped
|
|
13
|
+
* GLSL→MSL shader transpiler. Keep it inside the supported subset:
|
|
14
|
+
*
|
|
15
|
+
* • no imports — the module is self-contained (pure math, no DOM/GL);
|
|
16
|
+
* • function declarations with `number` / `Float32Array` / interface-typed
|
|
17
|
+
* params; `const`/`let`; `if`/`else`; canonical `for (let i = A; i < B; i++)`
|
|
18
|
+
* loops; `break`/`continue`/`return`; ternaries; arithmetic + comparisons;
|
|
19
|
+
* • Math.{floor,min,max,abs,sqrt,exp,hypot,sin,cos,pow,round} and Math.PI;
|
|
20
|
+
* • `new Float32Array(n)`, element writes, `{ x, y }` vector literals, and a
|
|
21
|
+
* `{ verts, meta }` bundle return (declared via an exported interface).
|
|
22
|
+
*
|
|
23
|
+
* The transpiler THROWS on anything outside that subset. Numeric semantics are
|
|
24
|
+
* JS's: every number is a double (loop counters transpile to ints), `/` is
|
|
25
|
+
* always double division, and array writes narrow to float32 exactly like the
|
|
26
|
+
* Float32Array stores here do.
|
|
27
|
+
*
|
|
28
|
+
* Output (gl_FragCoord space — device px, y-UP, to match the shader):
|
|
29
|
+
* verts: Float32Array(MAX_BOLTS * VERTS_PER_BOLT * 2) — vertex i of bolt b at
|
|
30
|
+
* [(b*VPB + i) * 2].
|
|
31
|
+
* meta: Float32Array(MAX_BOLTS * 4) — per bolt (segCount, radFrac, fadeMul, isMain).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Max secondary forks — shared by the shader + the `.dope` clamp. */
|
|
35
|
+
export const MAX_FORKS = 7;
|
|
36
|
+
/** Polyline segment count of the main bolt (and forks). More = jaggier arc. */
|
|
37
|
+
export const BOLT_SEGS = 14;
|
|
38
|
+
/** Main trunk + forks. */
|
|
39
|
+
export const MAX_BOLTS = MAX_FORKS + 1;
|
|
40
|
+
/** Vertices stored per bolt (BOLT_SEGS + 1). */
|
|
41
|
+
export const VERTS_PER_BOLT = BOLT_SEGS + 1;
|
|
42
|
+
/** Window (ms) over which the bolt cracks in to the strike point. Hard + fast. */
|
|
43
|
+
export const STRIKE_MS = 130;
|
|
44
|
+
|
|
45
|
+
interface Vec2 {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** verts: MAX_BOLTS*VPB*2 ; meta: MAX_BOLTS*4 = (segCount, radFrac, fadeMul, isMain). */
|
|
51
|
+
export interface LightningArrays {
|
|
52
|
+
verts: Float32Array;
|
|
53
|
+
meta: Float32Array;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clamp(x: number, lo: number, hi: number): number {
|
|
57
|
+
return x < lo ? lo : x > hi ? hi : x;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clamp01(x: number): number {
|
|
61
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function smoothstep(e0: number, e1: number, x: number): number {
|
|
65
|
+
const t = clamp01((x - e0) / (e1 - e0));
|
|
66
|
+
return t * t * (3 - 2 * t);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fract(x: number): number {
|
|
70
|
+
return x - Math.floor(x);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mix(a: number, b: number, t: number): number {
|
|
74
|
+
return a + (b - a) * t;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Bolt strike progress (0..1) over elapsed ms — the jagged arc racing from the
|
|
79
|
+
* source to the action point. Ease-out quint: a near-instant crack-in that
|
|
80
|
+
* settles abruptly, so the bolt reads as a strike, not a slow draw.
|
|
81
|
+
*/
|
|
82
|
+
export function strikeProgress(elapsedMs: number): number {
|
|
83
|
+
const x = clamp01(elapsedMs / STRIKE_MS);
|
|
84
|
+
return 1 - Math.pow(1 - x, 5);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Faithful port of the shared look/glsl hash + value-noise fbm -----------
|
|
88
|
+
|
|
89
|
+
function hash11(p: number): number {
|
|
90
|
+
p = fract(p * 0.1031);
|
|
91
|
+
p *= p + 33.33;
|
|
92
|
+
p *= p + p;
|
|
93
|
+
return fract(p);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Just the .x channel of the shared hash21 (used for the start jog). */
|
|
97
|
+
function hash21x(p: number): number {
|
|
98
|
+
let x = fract(p * 0.1031), y = fract(p * 0.103), z = fract(p * 0.0973);
|
|
99
|
+
const d = x * (y + 33.33) + y * (z + 33.33) + z * (x + 33.33);
|
|
100
|
+
x += d; y += d; z += d;
|
|
101
|
+
return fract((x + y) * z);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hash21(p: number): Vec2 {
|
|
105
|
+
let x = fract(p * 0.1031), y = fract(p * 0.103), z = fract(p * 0.0973);
|
|
106
|
+
const d = x * (y + 33.33) + y * (z + 33.33) + z * (x + 33.33);
|
|
107
|
+
x += d; y += d; z += d;
|
|
108
|
+
return { x: fract((x + y) * z), y: fract((x + z) * y) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function vnoise(x: number, y: number): number {
|
|
112
|
+
const ix = Math.floor(x), iy = Math.floor(y);
|
|
113
|
+
const fx = x - ix, fy = y - iy;
|
|
114
|
+
const ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
|
|
115
|
+
const a = hash11(ix * 1 + iy * 57);
|
|
116
|
+
const b = hash11((ix + 1) * 1 + iy * 57);
|
|
117
|
+
const c = hash11(ix * 1 + (iy + 1) * 57);
|
|
118
|
+
const d = hash11((ix + 1) * 1 + (iy + 1) * 57);
|
|
119
|
+
return mix(mix(a, b, ux), mix(c, d, ux), uy);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function fbm(x: number, y: number): number {
|
|
123
|
+
let s = 0, a = 0.5;
|
|
124
|
+
for (let i = 0; i < 4; i++) {
|
|
125
|
+
s += a * vnoise(x, y);
|
|
126
|
+
const nx = (0.8 * x + 0.6 * y) * 2.03;
|
|
127
|
+
const ny = (-0.6 * x + 0.8 * y) * 2.03;
|
|
128
|
+
x = nx; y = ny; a *= 0.5;
|
|
129
|
+
}
|
|
130
|
+
return s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Port of the shader boltPoint: a jagged vertex at t along A→B. */
|
|
134
|
+
function boltPoint(A: Vec2, B: Vec2, t: number, seedOff: number, seed: number, jagged: number, beat: number): Vec2 {
|
|
135
|
+
const dx = B.x - A.x, dy = B.y - A.y;
|
|
136
|
+
const len = Math.max(Math.hypot(dx, dy), 1);
|
|
137
|
+
const dirx = dx / len, diry = dy / len;
|
|
138
|
+
const nrmx = -diry, nrmy = dirx;
|
|
139
|
+
const n = fbm(t * 6 + seedOff + seed, beat * 0.5) - 0.5;
|
|
140
|
+
const fine = fbm(t * 22 + seedOff * 3.1 + seed, beat) - 0.5;
|
|
141
|
+
const taper = Math.sin(t * Math.PI);
|
|
142
|
+
const off = (n * 1.6 + fine * 0.5) * jagged * len * 0.16 * taper;
|
|
143
|
+
return { x: A.x + dirx * (t * len) + nrmx * off, y: A.y + diry * (t * len) + nrmy * off };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Write up to BOLT_SEGS+1 vertices of the drawn (0..drawn) polyline A→B into
|
|
147
|
+
* `verts` at bolt slot `b`; returns the segment count (points-1). */
|
|
148
|
+
function writeBolt(
|
|
149
|
+
verts: Float32Array, b: number, A: Vec2, B: Vec2, drawn: number,
|
|
150
|
+
seedOff: number, seed: number, jagged: number, beat: number,
|
|
151
|
+
): number {
|
|
152
|
+
const base = b * VERTS_PER_BOLT;
|
|
153
|
+
let last = 0;
|
|
154
|
+
const v0 = boltPoint(A, B, 0, seedOff, seed, jagged, beat);
|
|
155
|
+
verts[(base + 0) * 2] = v0.x;
|
|
156
|
+
verts[(base + 0) * 2 + 1] = v0.y;
|
|
157
|
+
for (let i = 1; i <= BOLT_SEGS; i++) {
|
|
158
|
+
const t = i / BOLT_SEGS;
|
|
159
|
+
if (t - 1 / BOLT_SEGS > drawn) break;
|
|
160
|
+
const tc = Math.min(t, drawn);
|
|
161
|
+
const v = boltPoint(A, B, tc, seedOff, seed, jagged, beat);
|
|
162
|
+
verts[(base + i) * 2] = v.x;
|
|
163
|
+
verts[(base + i) * 2 + 1] = v.y;
|
|
164
|
+
last = i;
|
|
165
|
+
}
|
|
166
|
+
return last;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compute the bolt polyline (trunk + forks) for this frame, in gl_FragCoord
|
|
171
|
+
* space (device px, y-up). `originX`/`originY` is the strike point (gl coords);
|
|
172
|
+
* `width`/`height` the canvas device px; `elapsedMs`/`life` the timing.
|
|
173
|
+
*/
|
|
174
|
+
export function computeLightningArrays(
|
|
175
|
+
style: number, thickness: number, jagged: number, branches: number, boltSeed: number,
|
|
176
|
+
width: number, height: number, originX: number, originY: number,
|
|
177
|
+
elapsedMs: number, life: number,
|
|
178
|
+
): LightningArrays {
|
|
179
|
+
const verts = new Float32Array(MAX_BOLTS * VERTS_PER_BOLT * 2);
|
|
180
|
+
const meta = new Float32Array(MAX_BOLTS * 4);
|
|
181
|
+
const strike = strikeProgress(elapsedMs);
|
|
182
|
+
if (strike <= 0) return { verts, meta };
|
|
183
|
+
|
|
184
|
+
const seed = boltSeed;
|
|
185
|
+
const beat = Math.floor((elapsedMs / 1000) * 12) * style;
|
|
186
|
+
|
|
187
|
+
// Strike geometry: from near the top edge (biased toward the strike x) down to
|
|
188
|
+
// the strike point. gl coords y-up: top edge is y ≈ height.
|
|
189
|
+
const jx = (hash21x(seed * 1.7) - 0.5) * width * 0.5;
|
|
190
|
+
const A: Vec2 = { x: clamp(originX + jx, width * 0.12, width * 0.88), y: height * 1.02 };
|
|
191
|
+
const B: Vec2 = { x: originX, y: originY };
|
|
192
|
+
|
|
193
|
+
// MAIN BOLT (slot 0).
|
|
194
|
+
const mainSegs = writeBolt(verts, 0, A, B, strike, 0, seed, jagged, beat);
|
|
195
|
+
meta[0] = mainSegs; meta[1] = thickness; meta[2] = 1.0; meta[3] = 1.0;
|
|
196
|
+
|
|
197
|
+
// FORKS (slots 1..).
|
|
198
|
+
const forks = Math.max(0, Math.min(MAX_FORKS, Math.round(branches)));
|
|
199
|
+
const dlenRaw = Math.hypot(B.x - A.x, B.y - A.y);
|
|
200
|
+
const dlen = dlenRaw === 0 ? 1 : dlenRaw;
|
|
201
|
+
const dirx = (B.x - A.x) / dlen, diry = (B.y - A.y) / dlen;
|
|
202
|
+
const nrmx = -diry, nrmy = dirx;
|
|
203
|
+
const forkFade = 0.6 + 0.4 * (1 - smoothstep(0.5, 1.0, life));
|
|
204
|
+
for (let i = 0; i < forks; i++) {
|
|
205
|
+
const b = 1 + i;
|
|
206
|
+
const hh = hash21(i * 9.7 + seed + 3);
|
|
207
|
+
const launchT = 0.18 + hh.x * 0.62;
|
|
208
|
+
if (strike < launchT) { meta[b * 4] = 0; continue; }
|
|
209
|
+
const forkA = boltPoint(A, B, launchT, 0, seed, jagged, beat);
|
|
210
|
+
const ang = (hh.y - 0.5) * 2.2;
|
|
211
|
+
const reach = (0.18 + hh.x * 0.22) * dlen;
|
|
212
|
+
const ex = dirx * (0.5 + hh.y) + nrmx * ang;
|
|
213
|
+
const ey = diry * (0.5 + hh.y) + nrmy * ang;
|
|
214
|
+
const forkB: Vec2 = { x: forkA.x + ex * reach, y: forkA.y + ey * reach };
|
|
215
|
+
const forkDrawn = clamp((strike - launchT) / Math.max(1 - launchT, 0.05), 0, 1);
|
|
216
|
+
const segs = writeBolt(verts, b, forkA, forkB, forkDrawn, i * 17 + 5, seed, jagged, beat);
|
|
217
|
+
meta[b * 4] = segs; meta[b * 4 + 1] = thickness * 0.6; meta[b * 4 + 2] = forkFade; meta[b * 4 + 3] = 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { verts, meta };
|
|
221
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLSL ES 3.00 source for Lightning (web, PRECOMPUTED-VERTEX glow).
|
|
3
|
+
*
|
|
4
|
+
* This keeps the ORIGINAL look — a per-pixel inverse-distance (`1/d`) plasma glow
|
|
5
|
+
* with a hot white core, branching forks, impact burst, strobe flash and cel
|
|
6
|
+
* comic mode — but removes the cost that made it crawl under software/ANGLE
|
|
7
|
+
* WebGL: the old shader re-derived every bolt vertex with TWO 4-octave `fbm`
|
|
8
|
+
* calls per segment AT EVERY PIXEL (~220 fbm/pixel). The bolt polyline is
|
|
9
|
+
* fragment-INDEPENDENT, so it's now computed ONCE per frame on the CPU (see
|
|
10
|
+
* lightning-logic.ts — a faithful JS port of the same fbm/boltPoint) and fed
|
|
11
|
+
* in as the `uVerts` / `uBoltMeta` uniform arrays. The fragment shader just walks
|
|
12
|
+
* those segments with cheap `sdSeg` + the same glow accumulation, so the look is
|
|
13
|
+
* unchanged while the per-pixel cost drops from ~220 fbm to a single fbm (the
|
|
14
|
+
* halo's living variation).
|
|
15
|
+
*
|
|
16
|
+
* All three platforms use this precomputed-vertex approach, and THIS GLSL is
|
|
17
|
+
* the single shader source (`x-build.shader`): the MSL `Lightning.metal` and
|
|
18
|
+
* the Kotlin `LightningShader.kt` are GENERATED from it by the toolchain. The
|
|
19
|
+
* `uVerts`/`uBoltMeta` uniform arrays ride the `.dope` `binding.arrays`
|
|
20
|
+
* contract — web and Android bind them by name through the runners'
|
|
21
|
+
* `frameArrays` seam; the GLSL→MSL transpiler turns each declared uniform
|
|
22
|
+
* array into a `constant floatN *` fragment buffer at its declared index, and
|
|
23
|
+
* the generated native factories feed them from the transpiled
|
|
24
|
+
* `LightningRenderer` (the same lightning-logic.ts source as here).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
GLSL_CONSTANTS,
|
|
29
|
+
GLSL_DITHER,
|
|
30
|
+
GLSL_FBM,
|
|
31
|
+
GLSL_HASH,
|
|
32
|
+
GLSL_SD_SEG,
|
|
33
|
+
GLSL_TONEMAP_ACES,
|
|
34
|
+
} from "@dopaminefx/core";
|
|
35
|
+
// The bolt-geometry constants live with the CPU precompute (the single
|
|
36
|
+
// transpiled source, lightning-logic.ts); re-exported here for consumers.
|
|
37
|
+
import { MAX_FORKS, BOLT_SEGS, MAX_BOLTS, VERTS_PER_BOLT } from "./lightning-logic.js";
|
|
38
|
+
|
|
39
|
+
export { MAX_FORKS, BOLT_SEGS, MAX_BOLTS, VERTS_PER_BOLT };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Total uVerts entries (exported so the toolchain's Android emitter can resolve
|
|
43
|
+
* the `${TOTAL_VERTS}` interpolation — it resolves single identifiers only).
|
|
44
|
+
*/
|
|
45
|
+
export const TOTAL_VERTS = MAX_BOLTS * VERTS_PER_BOLT;
|
|
46
|
+
|
|
47
|
+
export const LIGHTNING_VERTEX_SRC = /* glsl */ `#version 300 es
|
|
48
|
+
void main() {
|
|
49
|
+
vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
50
|
+
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
|
|
51
|
+
}`;
|
|
52
|
+
|
|
53
|
+
export const LIGHTNING_FRAGMENT_SRC = /* glsl */ `#version 300 es
|
|
54
|
+
precision highp float;
|
|
55
|
+
out vec4 fragColor;
|
|
56
|
+
|
|
57
|
+
uniform vec2 uResolution; // device pixels
|
|
58
|
+
uniform vec2 uOrigin; // strike point (gl coords, y-up)
|
|
59
|
+
uniform float uStrike; // bolt strike progress 0..1 (impact timing)
|
|
60
|
+
uniform float uFlash; // strobe/flash amplitude
|
|
61
|
+
uniform float uLife; // whole-effect progress 0..1
|
|
62
|
+
uniform float uTimeS; // elapsed seconds
|
|
63
|
+
uniform float uAmp; // impact envelope amplitude (peaks > 1)
|
|
64
|
+
uniform float uThickness; // bolt half-width as fraction of min dim (impact sizing)
|
|
65
|
+
uniform float uFlashBright; // peak flash brightness multiplier
|
|
66
|
+
uniform float uExposure; // overall light gain
|
|
67
|
+
uniform float uSeed; // per-fire hash offset (halo variation)
|
|
68
|
+
uniform float uStyle; // 0..1 photoreal plasma -> cel comic bolt (whimsy)
|
|
69
|
+
uniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)
|
|
70
|
+
uniform vec2 uShadowOffset; // device-px offset of the cast silhouette
|
|
71
|
+
uniform float uShadowSoft; // penumbra softness in device px
|
|
72
|
+
uniform float uShadowStrength;// 0..1 max darkening of the multiply layer
|
|
73
|
+
uniform vec3 uC0; // electric core hue
|
|
74
|
+
// CPU-precomputed bolt polyline: uVerts[b*VPB + i] is vertex i of bolt b
|
|
75
|
+
// (device px, gl coords); uBoltMeta[b] = (segCount, radFrac, fadeMul, isMain).
|
|
76
|
+
uniform vec2 uVerts[${TOTAL_VERTS}];
|
|
77
|
+
uniform vec4 uBoltMeta[${MAX_BOLTS}];
|
|
78
|
+
|
|
79
|
+
#define MAX_FORKS ${MAX_FORKS}
|
|
80
|
+
#define BOLT_SEGS ${BOLT_SEGS}
|
|
81
|
+
#define MAX_BOLTS ${MAX_BOLTS}
|
|
82
|
+
#define VPB ${VERTS_PER_BOLT}
|
|
83
|
+
${GLSL_CONSTANTS}
|
|
84
|
+
${GLSL_HASH}
|
|
85
|
+
${GLSL_FBM}
|
|
86
|
+
${GLSL_TONEMAP_ACES}
|
|
87
|
+
${GLSL_DITHER}
|
|
88
|
+
${GLSL_SD_SEG}
|
|
89
|
+
|
|
90
|
+
// Electric channel colour ramp: a tight blue/violet -> hot white anchored on uC0
|
|
91
|
+
// (so the bolt stays monochromatic electric, not the roaming golden-angle palette).
|
|
92
|
+
vec3 elecRamp(float t){
|
|
93
|
+
t = clamp(t, 0.0, 1.0);
|
|
94
|
+
vec3 rim = mix(uC0, vec3(0.45, 0.6, 1.0), 0.35);
|
|
95
|
+
vec3 mid = mix(uC0, vec3(0.8, 0.85, 1.0), 0.5);
|
|
96
|
+
vec3 hot = vec3(1.0);
|
|
97
|
+
return t < 0.5 ? mix(rim, mid, t * 2.0) : mix(mid, hot, (t - 0.5) * 2.0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Glow of bolt \`b\` at frag: walk its precomputed segments, accumulate the same
|
|
101
|
+
// inverse-distance plasma glow + hot core the original boltGlow used. radFrac is
|
|
102
|
+
// the bolt half-width as a frac of minDim. Returns vec2(core, glow).
|
|
103
|
+
vec2 boltGlowV(vec2 frag, int b, int segCount, float radFrac){
|
|
104
|
+
float minDim = min(uResolution.x, uResolution.y);
|
|
105
|
+
float rad = minDim * radFrac;
|
|
106
|
+
float glow = 0.0;
|
|
107
|
+
float core = 0.0;
|
|
108
|
+
int base = b * VPB;
|
|
109
|
+
vec2 prev = uVerts[base];
|
|
110
|
+
for (int i = 1; i <= BOLT_SEGS; i++) {
|
|
111
|
+
if (i > segCount) break;
|
|
112
|
+
vec2 cur = uVerts[base + i];
|
|
113
|
+
float dist = sdSeg(frag, prev, cur);
|
|
114
|
+
glow += rad / (dist + rad * 0.35);
|
|
115
|
+
core = max(core, 1.0 - smoothstep(rad * 0.25, rad * 0.6, dist));
|
|
116
|
+
prev = cur;
|
|
117
|
+
}
|
|
118
|
+
glow = clamp(glow / float(BOLT_SEGS) * 2.2, 0.0, 1.4);
|
|
119
|
+
return vec2(core, glow);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// SHADOW: the main bolt's silhouette only (matches the original), 9-tap ring blur.
|
|
123
|
+
vec4 lightningShadowColor(vec2 frag){
|
|
124
|
+
float minDim = min(uResolution.x, uResolution.y);
|
|
125
|
+
float rad = minDim * uThickness * 1.6;
|
|
126
|
+
int segCount = int(uBoltMeta[0].x + 0.5);
|
|
127
|
+
vec2 sp = frag - uShadowOffset;
|
|
128
|
+
float soft = uShadowSoft;
|
|
129
|
+
float s2 = soft * 0.7071;
|
|
130
|
+
vec2 taps[9];
|
|
131
|
+
taps[0] = sp;
|
|
132
|
+
taps[1] = sp + vec2( soft, 0.0);
|
|
133
|
+
taps[2] = sp + vec2(-soft, 0.0);
|
|
134
|
+
taps[3] = sp + vec2(0.0, soft);
|
|
135
|
+
taps[4] = sp + vec2(0.0, -soft);
|
|
136
|
+
taps[5] = sp + vec2( s2, s2);
|
|
137
|
+
taps[6] = sp + vec2(-s2, s2);
|
|
138
|
+
taps[7] = sp + vec2( s2, -s2);
|
|
139
|
+
taps[8] = sp + vec2(-s2, -s2);
|
|
140
|
+
float occSum = 0.0;
|
|
141
|
+
for (int k = 0; k < 9; k++) {
|
|
142
|
+
float occ = 0.0;
|
|
143
|
+
vec2 prev = uVerts[0];
|
|
144
|
+
for (int i = 1; i <= BOLT_SEGS; i++) {
|
|
145
|
+
if (i > segCount) break;
|
|
146
|
+
vec2 cur = uVerts[i];
|
|
147
|
+
occ = max(occ, 1.0 - smoothstep(rad * 0.6, rad, sdSeg(taps[k], prev, cur)));
|
|
148
|
+
prev = cur;
|
|
149
|
+
}
|
|
150
|
+
occSum += clamp(occ * uAmp, 0.0, 1.0);
|
|
151
|
+
}
|
|
152
|
+
occSum /= 9.0;
|
|
153
|
+
float dark = clamp(occSum, 0.0, 1.0) * uShadowStrength;
|
|
154
|
+
vec3 tint = mix(vec3(1.0), 0.55 + 0.45 * normalize(elecRamp(0.2) + 1e-3), 0.25);
|
|
155
|
+
return vec4(mix(vec3(1.0), tint, dark), 1.0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
void main(){
|
|
159
|
+
vec2 frag = gl_FragCoord.xy;
|
|
160
|
+
float minDim = min(uResolution.x, uResolution.y);
|
|
161
|
+
|
|
162
|
+
if (uShadow > 0.5) {
|
|
163
|
+
fragColor = lightningShadowColor(frag);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
vec3 col = vec3(0.0);
|
|
168
|
+
float gain = uExposure * uAmp;
|
|
169
|
+
float boltCore = 0.0;
|
|
170
|
+
float boltGlowAcc = 0.0;
|
|
171
|
+
|
|
172
|
+
// A touch of living fbm variation on the halo — ONE fbm/pixel (was the only
|
|
173
|
+
// per-pixel noise we keep; the bolt geometry is precomputed on the CPU).
|
|
174
|
+
float haloVar = 0.1 * (fbm(frag / minDim * 4.0 + uSeed) - 0.5);
|
|
175
|
+
|
|
176
|
+
// Trunk + forks: same glow/colour accumulation as the original, reading the
|
|
177
|
+
// precomputed polyline. uBoltMeta[b] = (segCount, radFrac, fadeMul, isMain).
|
|
178
|
+
for (int b = 0; b < MAX_BOLTS; b++) {
|
|
179
|
+
vec4 meta = uBoltMeta[b];
|
|
180
|
+
int segCount = int(meta.x + 0.5);
|
|
181
|
+
if (segCount < 1) continue;
|
|
182
|
+
float fadeMul = meta.z;
|
|
183
|
+
bool isMain = meta.w > 0.5;
|
|
184
|
+
vec2 g = boltGlowV(frag, b, segCount, meta.y);
|
|
185
|
+
float core = g.x * fadeMul;
|
|
186
|
+
float glow = g.y * fadeMul;
|
|
187
|
+
float haloT = clamp(glow * 0.7 + (isMain ? haloVar : 0.15), 0.0, 1.0);
|
|
188
|
+
col += elecRamp(haloT) * glow * gain * (isMain ? 1.3 : 0.8);
|
|
189
|
+
col += vec3(1.0) * core * gain * (isMain ? 2.4 : 1.5);
|
|
190
|
+
boltCore = max(boltCore, core);
|
|
191
|
+
boltGlowAcc = max(boltGlowAcc, glow);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- IMPACT GLOW ---- bright radial burst at the strike point, easing off.
|
|
195
|
+
float landed = smoothstep(0.7, 1.0, uStrike) * (0.4 + 0.6 * (1.0 - smoothstep(0.1, 0.5, uLife)));
|
|
196
|
+
float dB = length(frag - uOrigin);
|
|
197
|
+
float impact = (minDim * uThickness * 2.0) / (dB + minDim * uThickness * 1.4);
|
|
198
|
+
impact *= impact;
|
|
199
|
+
col += elecRamp(0.7) * impact * landed * gain * 0.8;
|
|
200
|
+
|
|
201
|
+
// ---- FLASH / STROBE ---- hard near-white wash, hottest at the strike point.
|
|
202
|
+
float flashRadial = 0.28 + 0.72 * exp(-dB / (minDim * 0.5));
|
|
203
|
+
vec3 flashCol = mix(vec3(1.0), elecRamp(0.6), 0.25);
|
|
204
|
+
col += flashCol * uFlash * uFlashBright * flashRadial;
|
|
205
|
+
|
|
206
|
+
col = tonemapACES(col * 0.9);
|
|
207
|
+
|
|
208
|
+
// ---- Cel / comic-book bolt (whimsy) ---- flatten ONLY the bolt forms.
|
|
209
|
+
if (uStyle > 0.001) {
|
|
210
|
+
float coreMask = smoothstep(0.45, 0.65, boltCore);
|
|
211
|
+
float bandMask = smoothstep(0.45, 0.8, boltGlowAcc) * (1.0 - coreMask);
|
|
212
|
+
vec3 boltColor = clamp(elecRamp(0.35) * 1.5 + 0.05, 0.0, 1.3);
|
|
213
|
+
vec3 cel = vec3(1.0) * coreMask + boltColor * bandMask;
|
|
214
|
+
float boltMask = clamp(coreMask + bandMask, 0.0, 1.0);
|
|
215
|
+
col = mix(col, mix(col, cel, boltMask), uStyle);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle);
|
|
219
|
+
fragColor = vec4(max(col, 0.0), 1.0);
|
|
220
|
+
}`;
|