@wave3d/core 0.7.0 → 0.8.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/config/model.d.ts +30 -2
- package/dist/config/model.js +4 -1
- package/dist/config/model.js.map +1 -1
- package/dist/presets.js +35 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +4 -2
- package/dist/renderer/WaveRenderer.js +11 -5
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/particleField.d.ts +35 -4
- package/dist/renderer/particleField.js +150 -13
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/shaders.js +178 -86
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +2321 -2248
- package/package.json +1 -1
- package/skills/wave3d/SKILL.md +1 -1
package/dist/renderer/shaders.js
CHANGED
|
@@ -258,6 +258,102 @@ WaveShape waveShape(vec3 position, vec2 uv, float t, vec2 loopOff){
|
|
|
258
258
|
return s;
|
|
259
259
|
}
|
|
260
260
|
`;
|
|
261
|
+
const pointerFieldChunk = `
|
|
262
|
+
uniform vec2 uPointer; // smoothed pointer, NDC (-1..1)
|
|
263
|
+
uniform float uPointerActive; // presence ramp 0..1 × per-wave influence
|
|
264
|
+
uniform float uPointerRadius; // falloff radius in NDC-y units (config radius × 2)
|
|
265
|
+
uniform float uPointerAspect; // drawing-buffer dw/dh (circular screen falloff)
|
|
266
|
+
uniform float uPointerAgitate;
|
|
267
|
+
uniform float uPointerPush; // signed membrane dome at the cursor (+ repel / − attract)
|
|
268
|
+
uniform float uPointerWake; // drag-wake trough amplitude (behind the moving cursor)
|
|
269
|
+
uniform vec2 uPointerVel; // smoothed pointer velocity, NDC/s (drag-wake direction)
|
|
270
|
+
// Ribbon flow: stretch the falloff along the strip's length axis so the field reaches ALONG the
|
|
271
|
+
// ribbon rather than as a screen disc. 0 = the plain circular smoothstep (byte-identical when off).
|
|
272
|
+
uniform float uShapeFlow;
|
|
273
|
+
#ifdef POINTER_RIPPLES
|
|
274
|
+
uniform vec2 uRippleOrigin[4]; // NDC
|
|
275
|
+
uniform float uRippleAge[4]; // seconds since spawn (CPU-computed)
|
|
276
|
+
uniform float uRippleAmp[4]; // shared 0..1 decay envelope per slot (CPU-computed; 0 = slot free)
|
|
277
|
+
uniform float uPointerRipple; // THIS wave's ripple amplitude (scales the shared envelope)
|
|
278
|
+
const float RIPPLE_WAVE_SPEED = 0.85; // NDC/s the ring crest travels outward
|
|
279
|
+
const float RIPPLE_SIGMA = 0.14; // gaussian half-width of the travelling packet (NDC)
|
|
280
|
+
const float RIPPLE_FREQ = 11.0; // oscillation within the packet (one crest + faint troughs)
|
|
281
|
+
const float RIPPLE_MAX_R = 1.2; // reach where the crest has fully left the frame
|
|
282
|
+
#endif
|
|
283
|
+
|
|
284
|
+
// fall = screen falloff × presence (the wave's vPointerFall, which both fragment themes consume);
|
|
285
|
+
// disp = the signed displacement along the surface's own up-axis, which the CALLER applies (the wave
|
|
286
|
+
// in its local space, the dust through the wave's world matrix).
|
|
287
|
+
struct PointerHit { float fall; float disp; };
|
|
288
|
+
|
|
289
|
+
// Sample the field for ONE point. ndc is that point's screen position; mvp the clip transform of the
|
|
290
|
+
// space rotA/rotB/rotC and churnPos live in (the owning wave's local space); t / loopOff the caller's
|
|
291
|
+
// linear / orbit time — only the one selected by LOOP_MOTION is read.
|
|
292
|
+
PointerHit pointerField(vec2 ndc, mat4 mvp, mat4 rotA, mat4 rotB, mat4 rotC, vec3 churnPos,
|
|
293
|
+
float t, vec2 loopOff){
|
|
294
|
+
// Screen-space offset from the cursor (aspect-corrected → round in pixels). The DEFAULT metric.
|
|
295
|
+
vec2 dp = (ndc - uPointer) * vec2(uPointerAspect, 1.0);
|
|
296
|
+
// Ribbon flow: stretch the metric along the strip's own LENGTH axis so the field reaches ALONG the
|
|
297
|
+
// ribbon and stays tight across it — the "flows with the material" feel, per-vertex (so it follows
|
|
298
|
+
// the strip's curve) with no CPU surface pick. The length axis is local +X (uv.x runs with x)
|
|
299
|
+
// carried through the SAME twist as the surface. The camera is orthographic (affine, w=1), so the
|
|
300
|
+
// axis's screen image is the linear map of the DIRECTION (w=0): one mat·dir, no second
|
|
301
|
+
// point-projection and no perspective divide. (A true per-pixel uv would need GPU picking — the
|
|
302
|
+
// visible surface is shader-displaced, so a CPU raycast of the base geometry misses.)
|
|
303
|
+
if (uShapeFlow > 0.0) {
|
|
304
|
+
vec3 tangentLocal = (((vec4(1.0, 0.0, 0.0, 0.0) * rotA) * rotB) * rotC).xyz;
|
|
305
|
+
vec2 tang = (mvp * vec4(tangentLocal, 0.0)).xy * vec2(uPointerAspect, 1.0);
|
|
306
|
+
float tl = length(tang);
|
|
307
|
+
if (tl > 1.0e-6) {
|
|
308
|
+
tang /= tl;
|
|
309
|
+
vec2 nrm = vec2(-tang.y, tang.x);
|
|
310
|
+
dp = vec2(dot(dp, tang) / (1.0 + uShapeFlow * 2.5), dot(dp, nrm)); // up to 3.5× reach along length
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
float fall = smoothstep(uPointerRadius, 0.0, length(dp)) * uPointerActive;
|
|
314
|
+
// Agitation: a fast churn octave near the cursor (additive — never rewrites base noise t, which
|
|
315
|
+
// would force restructuring the shared path). Loop-safe under both time variants.
|
|
316
|
+
#ifdef LOOP_MOTION
|
|
317
|
+
float disp = uPointerAgitate * fall
|
|
318
|
+
* simplexNoise(vec2(churnPos.x * uDispFreqX * 3.0, churnPos.z * uDispFreqZ * 3.0) + loopOff * 4.0);
|
|
319
|
+
#else
|
|
320
|
+
float disp = uPointerAgitate * fall
|
|
321
|
+
* simplexNoise(vec2(churnPos.x * uDispFreqX * 3.0 + t * 4.0, churnPos.z * uDispFreqZ * 3.0));
|
|
322
|
+
#endif
|
|
323
|
+
// Membrane push/pull: a smooth dome (fall is the falloff) that swells toward you (+ repel) or dents
|
|
324
|
+
// away (− attract) at the cursor, riding along with the sprung field.
|
|
325
|
+
disp += uPointerPush * fall;
|
|
326
|
+
// Drag-wake: pull the surface just BEHIND the moving cursor into a trailing trough. dp points
|
|
327
|
+
// from cursor to vertex; "behind" is how far the vertex sits opposite the velocity (0 ahead → 1 a
|
|
328
|
+
// radius behind), gated by speed so it only forms while dragging and heals when the cursor stops.
|
|
329
|
+
vec2 velC = uPointerVel * vec2(uPointerAspect, 1.0);
|
|
330
|
+
float wakeSpeed = length(velC);
|
|
331
|
+
if (uPointerWake != 0.0 && wakeSpeed > 1.0e-4) {
|
|
332
|
+
float behind = clamp(dot(-dp, velC) / (wakeSpeed * uPointerRadius), 0.0, 1.0);
|
|
333
|
+
disp -= uPointerWake * fall * behind * smoothstep(0.05, 0.6, wakeSpeed);
|
|
334
|
+
}
|
|
335
|
+
#ifdef POINTER_RIPPLES
|
|
336
|
+
for (int i = 0; i < 4; i++) {
|
|
337
|
+
if (uRippleAmp[i] > 0.0) {
|
|
338
|
+
float rd = length((ndc - uRippleOrigin[i]) * vec2(uPointerAspect, 1.0));
|
|
339
|
+
// A wave PACKET whose crest travels outward at RIPPLE_WAVE_SPEED: a gaussian window centred on
|
|
340
|
+
// the moving front carrying a short oscillation (a raised ring with faint trailing troughs),
|
|
341
|
+
// so the energy radiates instead of throbbing at the click point. The shared uRippleAmp
|
|
342
|
+
// envelope fades the whole packet over its lifetime; reach fades it as the crest leaves frame.
|
|
343
|
+
float front = uRippleAge[i] * RIPPLE_WAVE_SPEED;
|
|
344
|
+
float band = rd - front;
|
|
345
|
+
float packet = exp(-band * band / (2.0 * RIPPLE_SIGMA * RIPPLE_SIGMA)) * cos(band * RIPPLE_FREQ);
|
|
346
|
+
float reach = 1.0 - smoothstep(RIPPLE_MAX_R * 0.7, RIPPLE_MAX_R, front);
|
|
347
|
+
disp += uPointerRipple * uRippleAmp[i] * packet * reach;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
#endif
|
|
351
|
+
PointerHit hit;
|
|
352
|
+
hit.fall = fall;
|
|
353
|
+
hit.disp = disp;
|
|
354
|
+
return hit;
|
|
355
|
+
}
|
|
356
|
+
`;
|
|
261
357
|
const vertexShader = `
|
|
262
358
|
${simplex2d}
|
|
263
359
|
|
|
@@ -291,32 +387,12 @@ varying vec3 vWorldPos;
|
|
|
291
387
|
varying vec3 vViewDir;
|
|
292
388
|
varying vec4 vClipPosition; // = gl_Position, for the wireframe theme's depth fade
|
|
293
389
|
|
|
294
|
-
// Pointer field (optional, additive)
|
|
295
|
-
//
|
|
296
|
-
//
|
|
390
|
+
// Pointer field (optional, additive) — the shared chunk, gated so a wave with no interaction config
|
|
391
|
+
// compiles the exact same program. The particle emitter interpolates the SAME chunk, so dust reacts
|
|
392
|
+
// through one implementation of the footprint / falloff / displacement.
|
|
297
393
|
#ifdef POINTER_FX
|
|
298
|
-
|
|
299
|
-
uniform float uPointerActive; // presence ramp 0..1 × per-wave influence
|
|
300
|
-
uniform float uPointerRadius; // falloff radius in NDC-y units (config radius × 2)
|
|
301
|
-
uniform float uPointerAspect; // drawing-buffer dw/dh (circular screen falloff)
|
|
302
|
-
uniform float uPointerAgitate;
|
|
303
|
-
uniform float uPointerPush; // signed membrane dome at the cursor (+ repel / − attract)
|
|
304
|
-
uniform float uPointerWake; // drag-wake trough amplitude (behind the moving cursor)
|
|
305
|
-
uniform vec2 uPointerVel; // smoothed pointer velocity, NDC/s (drag-wake direction)
|
|
306
|
-
// Ribbon flow: stretch the falloff along the strip's length axis so the field reaches ALONG the
|
|
307
|
-
// ribbon rather than as a screen disc. 0 = the plain circular smoothstep (byte-identical when off).
|
|
308
|
-
uniform float uShapeFlow;
|
|
394
|
+
${pointerFieldChunk}
|
|
309
395
|
varying float vPointerFall; // falloff × presence — consumed by both fragment themes
|
|
310
|
-
#ifdef POINTER_RIPPLES
|
|
311
|
-
uniform vec2 uRippleOrigin[4]; // NDC
|
|
312
|
-
uniform float uRippleAge[4]; // seconds since spawn (CPU-computed)
|
|
313
|
-
uniform float uRippleAmp[4]; // shared 0..1 decay envelope per slot (CPU-computed; 0 = slot free)
|
|
314
|
-
uniform float uPointerRipple; // THIS wave's ripple amplitude (scales the shared envelope)
|
|
315
|
-
const float RIPPLE_WAVE_SPEED = 0.85; // NDC/s the ring crest travels outward
|
|
316
|
-
const float RIPPLE_SIGMA = 0.14; // gaussian half-width of the travelling packet (NDC)
|
|
317
|
-
const float RIPPLE_FREQ = 11.0; // oscillation within the packet (one crest + faint troughs)
|
|
318
|
-
const float RIPPLE_MAX_R = 1.2; // reach where the crest has fully left the frame
|
|
319
|
-
#endif
|
|
320
396
|
#endif
|
|
321
397
|
|
|
322
398
|
${waveShapeChunk}
|
|
@@ -351,75 +427,20 @@ void main(){
|
|
|
351
427
|
// Pointer field: displace along the wave's own (post-twist) up-axis, weighted by a screen-space
|
|
352
428
|
// falloff around the smoothed cursor — a circle at uShapeFlow 0, stretched along the ribbon as it
|
|
353
429
|
// rises. Everything here is ADDITIVE and fenced, so the shared path above/below is untouched and
|
|
354
|
-
// byte-identical when POINTER_FX is off.
|
|
430
|
+
// byte-identical when POINTER_FX is off. The field itself lives in pointerFieldChunk, which the
|
|
431
|
+
// particle emitter also calls — so dust reacts through this exact footprint and falloff.
|
|
355
432
|
// Shared clip-space transform, computed once and reused for the cursor metric and the ribbon
|
|
356
433
|
// tangent (the compiler is not guaranteed to CSE the triple product otherwise). Associativity is
|
|
357
434
|
// unchanged, so preClip is bit-for-bit what the plain P*V*M*v product produced.
|
|
358
435
|
mat4 mvp = projectionMatrix * viewMatrix * modelMatrix;
|
|
359
436
|
vec4 preClip = mvp * vec4(pos, 1.0);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// Ribbon flow: stretch the metric along the strip's own LENGTH axis so the field reaches ALONG the
|
|
364
|
-
// ribbon and stays tight across it — the "flows with the material" feel, per-vertex (so it follows
|
|
365
|
-
// the strip's curve) with no CPU surface pick. The length axis is local +X (uv.x runs with x)
|
|
366
|
-
// carried through the SAME twist as the surface. The camera is orthographic (affine, w=1), so the
|
|
367
|
-
// axis's screen image is the linear map of the DIRECTION (w=0): one mat·dir, no second
|
|
368
|
-
// point-projection and no perspective divide. (A true per-pixel uv would need GPU picking — the
|
|
369
|
-
// visible surface is shader-displaced, so a CPU raycast of the base geometry misses.)
|
|
370
|
-
if (uShapeFlow > 0.0) {
|
|
371
|
-
vec3 tangentLocal = (((vec4(1.0, 0.0, 0.0, 0.0) * ws.rotA) * ws.rotB) * ws.rotC).xyz;
|
|
372
|
-
vec2 tang = (mvp * vec4(tangentLocal, 0.0)).xy * vec2(uPointerAspect, 1.0);
|
|
373
|
-
float tl = length(tang);
|
|
374
|
-
if (tl > 1.0e-6) {
|
|
375
|
-
tang /= tl;
|
|
376
|
-
vec2 nrm = vec2(-tang.y, tang.x);
|
|
377
|
-
dp = vec2(dot(dp, tang) / (1.0 + uShapeFlow * 2.5), dot(dp, nrm)); // up to 3.5× reach along length
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
float fall = smoothstep(uPointerRadius, 0.0, length(dp));
|
|
381
|
-
vPointerFall = fall * uPointerActive;
|
|
437
|
+
PointerHit hit = pointerField(preClip.xy / max(preClip.w, 1.0e-6), mvp,
|
|
438
|
+
ws.rotA, ws.rotB, ws.rotC, pos, t, loopOff);
|
|
439
|
+
vPointerFall = hit.fall;
|
|
382
440
|
// Displacement axis = local +Y carried through the SAME three twist rotations as pos (row-vector
|
|
383
441
|
// convention). Rotations are linear, so post-twist axis displacement equals pre-twist Y displacement.
|
|
384
442
|
vec3 dispAxis = (((vec4(0.0, 1.0, 0.0, 0.0) * ws.rotA) * ws.rotB) * ws.rotC).xyz;
|
|
385
|
-
|
|
386
|
-
// would force restructuring the shared path). Loop-safe under both time variants.
|
|
387
|
-
#ifdef LOOP_MOTION
|
|
388
|
-
float disp = uPointerAgitate * vPointerFall
|
|
389
|
-
* simplexNoise(vec2(pos.x * uDispFreqX * 3.0, pos.z * uDispFreqZ * 3.0) + loopOff * 4.0);
|
|
390
|
-
#else
|
|
391
|
-
float disp = uPointerAgitate * vPointerFall
|
|
392
|
-
* simplexNoise(vec2(pos.x * uDispFreqX * 3.0 + t * 4.0, pos.z * uDispFreqZ * 3.0));
|
|
393
|
-
#endif
|
|
394
|
-
// Membrane push/pull: a smooth dome (vPointerFall is the falloff) that swells toward you (+ repel)
|
|
395
|
-
// or dents away (− attract) at the cursor, riding along with the sprung field.
|
|
396
|
-
disp += uPointerPush * vPointerFall;
|
|
397
|
-
// Drag-wake: pull the surface just BEHIND the moving cursor into a trailing trough. dp points
|
|
398
|
-
// from cursor to vertex; "behind" is how far the vertex sits opposite the velocity (0 ahead → 1 a
|
|
399
|
-
// radius behind), gated by speed so it only forms while dragging and heals when the cursor stops.
|
|
400
|
-
vec2 velC = uPointerVel * vec2(uPointerAspect, 1.0);
|
|
401
|
-
float wakeSpeed = length(velC);
|
|
402
|
-
if (uPointerWake != 0.0 && wakeSpeed > 1.0e-4) {
|
|
403
|
-
float behind = clamp(dot(-dp, velC) / (wakeSpeed * uPointerRadius), 0.0, 1.0);
|
|
404
|
-
disp -= uPointerWake * vPointerFall * behind * smoothstep(0.05, 0.6, wakeSpeed);
|
|
405
|
-
}
|
|
406
|
-
#ifdef POINTER_RIPPLES
|
|
407
|
-
for (int i = 0; i < 4; i++) {
|
|
408
|
-
if (uRippleAmp[i] > 0.0) {
|
|
409
|
-
float rd = length((preClip.xy / max(preClip.w, 1.0e-6) - uRippleOrigin[i]) * vec2(uPointerAspect, 1.0));
|
|
410
|
-
// A wave PACKET whose crest travels outward at RIPPLE_WAVE_SPEED: a gaussian window centred on
|
|
411
|
-
// the moving front carrying a short oscillation (a raised ring with faint trailing troughs),
|
|
412
|
-
// so the energy radiates instead of throbbing at the click point. The shared uRippleAmp
|
|
413
|
-
// envelope fades the whole packet over its lifetime; reach fades it as the crest leaves frame.
|
|
414
|
-
float front = uRippleAge[i] * RIPPLE_WAVE_SPEED;
|
|
415
|
-
float band = rd - front;
|
|
416
|
-
float packet = exp(-band * band / (2.0 * RIPPLE_SIGMA * RIPPLE_SIGMA)) * cos(band * RIPPLE_FREQ);
|
|
417
|
-
float reach = 1.0 - smoothstep(RIPPLE_MAX_R * 0.7, RIPPLE_MAX_R, front);
|
|
418
|
-
disp += uPointerRipple * uRippleAmp[i] * packet * reach;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
#endif
|
|
422
|
-
pos += dispAxis * disp;
|
|
443
|
+
pos += dispAxis * hit.disp;
|
|
423
444
|
#endif
|
|
424
445
|
|
|
425
446
|
// The scale / rotation / position transform lives on the mesh (modelMatrix), so the
|
|
@@ -964,6 +985,14 @@ uniform mat4 uShedModel; // the wave's matrixWorld (deformed LOCAL
|
|
|
964
985
|
uniform float uShedSpeed, uShedSeed;
|
|
965
986
|
${waveShapeChunk}
|
|
966
987
|
|
|
988
|
+
// The cursor. Same chunk the ribbon uses, mirrored onto this material in ParticleField.configure(),
|
|
989
|
+
// and behind the same POINTER_FX gate — a wave with no hover field compiles the point program it
|
|
990
|
+
// always did. uPartShove is the one particle-only knob (see the two samples in main).
|
|
991
|
+
#ifdef POINTER_FX
|
|
992
|
+
${pointerFieldChunk}
|
|
993
|
+
uniform float uPartShove; // how hard the cursor shoves dust that has already drifted free (0 = off)
|
|
994
|
+
#endif
|
|
995
|
+
|
|
967
996
|
varying float vAlpha;
|
|
968
997
|
varying vec3 vColor;
|
|
969
998
|
varying vec2 vDir; // screen-space motion direction (for the streak sprite)
|
|
@@ -992,17 +1021,43 @@ void main(){
|
|
|
992
1021
|
// Approximate the base hairpin point for this uv (length from uv.y; width centre), then deform it
|
|
993
1022
|
// exactly as the wave does. Good enough for dust — the fan / displacement dominate.
|
|
994
1023
|
vec3 base = vec3((aUv.y - 0.5) * 400.0, 0.0, ${(-8).toFixed(1)});
|
|
995
|
-
|
|
1024
|
+
WaveShape ws = waveShape(base, aUv, ts, loopOff);
|
|
1025
|
+
vec3 origin = (uShedModel * vec4(ws.pos, 1.0)).xyz;
|
|
996
1026
|
vec3 outward = normalize(origin - uCenter + vec3(1e-4));
|
|
1027
|
+
|
|
1028
|
+
#ifdef POINTER_FX
|
|
1029
|
+
// WELD (applied below, once the mote's own motion is known). The ribbon displaces its surface by
|
|
1030
|
+
// pointerField() along its own post-twist up-axis; a mote sitting ON that surface has to take the
|
|
1031
|
+
// same ride, or the cursor's dome lifts the silk out from under its own glitter. Sampled at the
|
|
1032
|
+
// SPAWN point and carried through the local→world matrix exactly as the ribbon's own
|
|
1033
|
+
// pos += dispAxis * disp is, so the two land on the same place. Sampling at the spawn point also
|
|
1034
|
+
// leaves outward derived from the UNDISPLACED origin, so a poke never bends the drift direction.
|
|
1035
|
+
mat4 pMvp = projectionMatrix * viewMatrix * uShedModel;
|
|
1036
|
+
vec4 originClip = pMvp * vec4(ws.pos, 1.0);
|
|
1037
|
+
vec3 dispAxis = mat3(uShedModel) * (((vec4(0.0, 1.0, 0.0, 0.0) * ws.rotA) * ws.rotB) * ws.rotC).xyz;
|
|
1038
|
+
PointerHit weld = pointerField(originClip.xy / max(originClip.w, 1.0e-6), pMvp,
|
|
1039
|
+
ws.rotA, ws.rotB, ws.rotC, ws.pos, ts, loopOff);
|
|
1040
|
+
#endif
|
|
1041
|
+
|
|
997
1042
|
vec3 p = origin + outward * age * uDrift + (aRnd.xyz - 0.5) * age * uDrift * 0.35;
|
|
998
1043
|
|
|
999
1044
|
// Motion styles, each 0 = off, all riding age so they stay loop-safe (the age wrap is hidden by
|
|
1000
1045
|
// fade→0 at birth/death). rise = screen-vertical buoyancy (embers up / snow down); swirl = orbit
|
|
1001
1046
|
// around the wave centre in the screen plane; wander = curl-noise turbulence (fireflies / motes).
|
|
1002
1047
|
p += uUp * age * uRise;
|
|
1048
|
+
// How far this mote travels away from its birth patch over a WHOLE life — the straight-line terms
|
|
1049
|
+
// plus, under swirl, the arc it sweeps at its own orbit radius. Only the pointer weld reads it
|
|
1050
|
+
// (0 for dust that merely clings to the surface, which is exactly the case age would get wrong),
|
|
1051
|
+
// so it is fenced like everything else the cursor drives.
|
|
1052
|
+
#ifdef POINTER_FX
|
|
1053
|
+
float span = abs(uDrift) * 1.35 + abs(uRise) + uWander;
|
|
1054
|
+
#endif
|
|
1003
1055
|
if (uSwirl != 0.0) {
|
|
1004
1056
|
vec3 nrm = cross(uRight, uUp);
|
|
1005
1057
|
vec3 rel = p - uCenter;
|
|
1058
|
+
#ifdef POINTER_FX
|
|
1059
|
+
span += abs(uSwirl) * TAU * length(rel);
|
|
1060
|
+
#endif
|
|
1006
1061
|
float rx = dot(rel, uRight), ry = dot(rel, uUp), rz = dot(rel, nrm);
|
|
1007
1062
|
float a = age * uSwirl * TAU;
|
|
1008
1063
|
float ca = cos(a), sa = sin(a);
|
|
@@ -1014,6 +1069,25 @@ void main(){
|
|
|
1014
1069
|
p += (uRight * wan.x + uUp * wan.y) * uWander;
|
|
1015
1070
|
}
|
|
1016
1071
|
|
|
1072
|
+
#ifdef POINTER_FX
|
|
1073
|
+
// How attached to its birth patch this mote still is: 1 while it sits on the surface, 0 once it
|
|
1074
|
+
// has travelled a full life's worth away. Measured from the DISTANCE it actually moved rather than
|
|
1075
|
+
// from age, because dust with no drift / rise / swirl / wander never leaves the surface at all —
|
|
1076
|
+
// an age fade would quietly stop that dust from following the ribbon halfway through its life.
|
|
1077
|
+
float attach = span > 1.0e-4 ? 1.0 - clamp(length(p - origin) / span, 0.0, 1.0) : 1.0;
|
|
1078
|
+
p += dispAxis * (weld.disp * attach);
|
|
1079
|
+
// SHOVE: the exact complement. The same field sampled at the mote's OWN screen position, so the
|
|
1080
|
+
// cursor also pushes dust that has already left the surface — and a click ripple visibly blows
|
|
1081
|
+
// through the cloud instead of stopping dead at the ribbon. Uniform branch (warp-coherent), so
|
|
1082
|
+
// uPartShove 0 costs nothing.
|
|
1083
|
+
if (uPartShove != 0.0) {
|
|
1084
|
+
vec4 pClip = projectionMatrix * viewMatrix * vec4(p, 1.0);
|
|
1085
|
+
PointerHit shove = pointerField(pClip.xy / max(pClip.w, 1.0e-6), pMvp,
|
|
1086
|
+
ws.rotA, ws.rotB, ws.rotC, ws.pos, ts, loopOff);
|
|
1087
|
+
p += dispAxis * (shove.disp * (1.0 - attach) * uPartShove);
|
|
1088
|
+
}
|
|
1089
|
+
#endif
|
|
1090
|
+
|
|
1017
1091
|
float tw = 0.5 + 0.5 * sin((age * 9.0 + aSeed) * TAU); // loop-safe flicker (rides age)
|
|
1018
1092
|
vAlpha = fade * mix(1.0, tw, clamp(uTwinkle, 0.0, 1.0));
|
|
1019
1093
|
vColor = mix(uColor, uColor2, aRnd.w); // two-tone dust: per-particle blend of the two colours
|
|
@@ -1030,7 +1104,24 @@ uniform float uShape; // 0 glitter · 1 soft · 2 ring · 3 star · 4 streak
|
|
|
1030
1104
|
varying float vAlpha;
|
|
1031
1105
|
varying vec3 vColor;
|
|
1032
1106
|
varying vec2 vDir;
|
|
1107
|
+
// User artwork (shape "sprite"), behind a define so a field without one compiles the exact same
|
|
1108
|
+
// program — and so the sampler only exists once a texture is actually bound to it. ONE texture is
|
|
1109
|
+
// shared by every particle in the field; see ParticleField.loadSprite for the rasterization.
|
|
1110
|
+
#ifdef PARTICLE_SPRITE
|
|
1111
|
+
uniform sampler2D uSprite;
|
|
1112
|
+
#endif
|
|
1033
1113
|
void main(){
|
|
1114
|
+
#ifdef PARTICLE_SPRITE
|
|
1115
|
+
// gl_PointCoord's origin is the sprite's TOP-left with y running DOWN, so it has to be flipped or
|
|
1116
|
+
// every sprite draws upside down. The procedural shapes below never needed this — they are all
|
|
1117
|
+
// symmetric about y, which is exactly why the bug would have gone unnoticed.
|
|
1118
|
+
vec4 tex = texture2D(uSprite, vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y));
|
|
1119
|
+
float a = tex.a * vAlpha;
|
|
1120
|
+
if (a <= 0.0) discard;
|
|
1121
|
+
// Tinted by the dust colour so color / color2 keep working: white artwork takes the tint
|
|
1122
|
+
// exactly, coloured artwork multiplies it.
|
|
1123
|
+
gl_FragColor = vec4(vColor * tex.rgb, a);
|
|
1124
|
+
#else
|
|
1034
1125
|
vec2 pc = gl_PointCoord - 0.5;
|
|
1035
1126
|
float d = length(pc);
|
|
1036
1127
|
int s = int(uShape + 0.5);
|
|
@@ -1053,6 +1144,7 @@ void main(){
|
|
|
1053
1144
|
a *= vAlpha;
|
|
1054
1145
|
if (a <= 0.0) discard;
|
|
1055
1146
|
gl_FragColor = vec4(vColor, a); // AdditiveBlending (src = SrcAlpha) → adds vColor·a
|
|
1147
|
+
#endif
|
|
1056
1148
|
}
|
|
1057
1149
|
`;
|
|
1058
1150
|
//#endregion
|