@wave3d/core 0.6.0 → 0.7.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 +72 -8
- package/dist/config/model.js +44 -1
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/presets.d.ts +8 -2
- package/dist/presets.js +437 -1
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveGeometry.d.ts +22 -3
- package/dist/renderer/WaveGeometry.js +22 -3
- package/dist/renderer/WaveGeometry.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +20 -0
- package/dist/renderer/WaveRenderer.js +101 -1
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/palette.d.ts +1 -1
- package/dist/renderer/palette.js +7 -5
- package/dist/renderer/palette.js.map +1 -1
- package/dist/renderer/particleField.d.ts +50 -0
- package/dist/renderer/particleField.js +220 -0
- package/dist/renderer/particleField.js.map +1 -0
- package/dist/renderer/shaders.js +247 -89
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +2712 -1951
- package/dist/standalone.d.ts +2 -2
- package/dist/standalone.js +2 -2
- package/dist/studio/thumbnail.d.ts +2 -2
- package/dist/studio/thumbnail.js +16 -7
- package/dist/studio/thumbnail.js.map +1 -1
- package/package.json +6 -4
- package/skills/wave3d/SKILL.md +1 -1
package/dist/renderer/shaders.js
CHANGED
|
@@ -157,6 +157,107 @@ vec3 applyColorGrade(vec3 c){
|
|
|
157
157
|
return hueShift(c, radians(uHueShift));
|
|
158
158
|
}
|
|
159
159
|
`;
|
|
160
|
+
const waveShapeChunk = `
|
|
161
|
+
// expStep: a falloff from 1 (at x=0) toward 0, sharpness set by n. The
|
|
162
|
+
// max() guards pow(0, n) (= Infinity → NaN) so negative n is safe — negative n
|
|
163
|
+
// just concentrates the twist toward the OTHER end instead.
|
|
164
|
+
float expStep(float x, float n){ return exp2(-exp2(n) * pow(max(x, 1.0e-3), n)); }
|
|
165
|
+
|
|
166
|
+
// rotationMatrix (mat4), used row-vector style: pos = (vec4(pos,1) * R).xyz
|
|
167
|
+
mat4 rotationMatrix(vec3 axis, float angle){
|
|
168
|
+
axis = normalize(axis);
|
|
169
|
+
float s = sin(angle), c = cos(angle), oc = 1.0 - c;
|
|
170
|
+
return mat4(
|
|
171
|
+
oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s, 0.0,
|
|
172
|
+
oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s, 0.0,
|
|
173
|
+
oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c, 0.0,
|
|
174
|
+
0.0, 0.0, 0.0, 1.0
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The wave SHAPE deform, shared by the wave vertex shader (below) and the particle SHED emitter
|
|
179
|
+
// (particleVertexShader): a base hairpin position + its uv → the displaced / helixed / twisted /
|
|
180
|
+
// fanned LOCAL position, plus the three twist matrices (the pointer field reads them). Every branch
|
|
181
|
+
// sits behind the SAME #ifdef gates as the code it replaces, so a given compiled program is
|
|
182
|
+
// byte-identical to the former inline version. t / loopOff are the linear / orbit time the
|
|
183
|
+
// caller computed; only the one selected by LOOP_MOTION is read (the other is a dead argument).
|
|
184
|
+
struct WaveShape { vec3 pos; mat4 rotA; mat4 rotB; mat4 rotC; };
|
|
185
|
+
WaveShape waveShape(vec3 position, vec2 uv, float t, vec2 loopOff){
|
|
186
|
+
// Displacement lifts Y by simplex noise of the (x,z) position.
|
|
187
|
+
vec3 pos = position;
|
|
188
|
+
#ifdef LOOP_MOTION
|
|
189
|
+
pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX, pos.z * uDispFreqZ) + loopOff);
|
|
190
|
+
#else
|
|
191
|
+
pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX + t, pos.z * uDispFreqZ + t));
|
|
192
|
+
#endif
|
|
193
|
+
#ifdef DETAIL_OCTAVE
|
|
194
|
+
// A second, finer octave riding on the broad swell (loop-orbit shared so it stays periodic).
|
|
195
|
+
#ifdef LOOP_MOTION
|
|
196
|
+
pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq, pos.z * uDetailFreq) + loopOff);
|
|
197
|
+
#else
|
|
198
|
+
pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq + t, pos.z * uDetailFreq + t));
|
|
199
|
+
#endif
|
|
200
|
+
#endif
|
|
201
|
+
|
|
202
|
+
#ifdef HELIX
|
|
203
|
+
// Helix — the periodic sweep the three twists (monotone falloffs) can't reach. Runs AFTER the
|
|
204
|
+
// displacement (so the noise still samples undeformed pos) and BEFORE the twist (so they compose).
|
|
205
|
+
float hAng = 6.28318530718 * uHelixTurns * uv.y + radians(uHelixPhase);
|
|
206
|
+
// Roll about the ribbon's width centre, not the origin — see RIBBON_Z_CENTER in WaveGeometry.
|
|
207
|
+
float rollA = hAng * uHelixRoll;
|
|
208
|
+
float rollC = cos(rollA), rollS = sin(rollA);
|
|
209
|
+
vec2 rel = vec2(pos.y, pos.z - ${(-8).toFixed(1)});
|
|
210
|
+
pos.y = rel.x * rollC - rel.y * rollS;
|
|
211
|
+
pos.z = ${(-8).toFixed(1)} + rel.x * rollS + rel.y * rollC;
|
|
212
|
+
pos.y += uHelixRadius * cos(hAng);
|
|
213
|
+
pos.z += uHelixRadius * sin(hAng);
|
|
214
|
+
#endif
|
|
215
|
+
|
|
216
|
+
// The X-twist frequency feeding rotB; the TWIST_MOTION variant modulates it with simplex noise
|
|
217
|
+
// indexed along the ribbon (uv.y) so the twist breathes over time.
|
|
218
|
+
float twistXFreq = uTwFreqX;
|
|
219
|
+
#ifdef TWIST_MOTION
|
|
220
|
+
#ifdef LOOP_MOTION
|
|
221
|
+
float twistXNoise = simplexNoise(vec2(uv.y * 2.0, 0.0) + loopOff);
|
|
222
|
+
#else
|
|
223
|
+
float twistXNoise = simplexNoise(vec2(uv.y * 2.0, t));
|
|
224
|
+
#endif
|
|
225
|
+
twistXFreq = uTwFreqX - twistXNoise * 0.1;
|
|
226
|
+
#endif
|
|
227
|
+
|
|
228
|
+
// Three-axis twist (see the falloff-axis note: rotA keys off uv.x/WIDTH, rotB/rotC off uv.y/LENGTH).
|
|
229
|
+
mat4 rotA = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqY * expStep(uv.x, uTwPowY));
|
|
230
|
+
mat4 rotB = rotationMatrix(vec3(0.0, 0.5, 0.5), twistXFreq * expStep(uv.y, uTwPowX));
|
|
231
|
+
mat4 rotC = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqZ * expStep(uv.y, uTwPowZ));
|
|
232
|
+
pos = (vec4(pos, 1.0) * rotA).xyz;
|
|
233
|
+
pos = (vec4(pos, 1.0) * rotB).xyz;
|
|
234
|
+
pos = (vec4(pos, 1.0) * rotC).xyz;
|
|
235
|
+
|
|
236
|
+
#ifdef RADIAL
|
|
237
|
+
// Radial fan: remap the ribbon to polar around the LOCAL origin so its LENGTH fans into a plume.
|
|
238
|
+
// uv.x (folded WIDTH) → fan ANGLE across uRadialArc; uv.y (LENGTH) → RADIUS, so a constant-uv.x
|
|
239
|
+
// combed fiber becomes a constant-angle radial spoke. mix(pos, fanned, 0) is identity → off is
|
|
240
|
+
// byte-identical. (Placement is the wave's position transform — the fan has no separate pivot.)
|
|
241
|
+
{
|
|
242
|
+
float rAng = radians(uRadialCenter) + (clamp(uv.x, 0.0, 1.0) - 0.5) * radians(uRadialArc);
|
|
243
|
+
float rRho = uRadialRadius + uv.y * 400.0 * uRadialSpread; // 400 = native ribbon length
|
|
244
|
+
vec3 rEr = vec3(cos(rAng), sin(rAng), 0.0); // radial dir, in local X–Y (screen plane)
|
|
245
|
+
vec3 rEt = vec3(-sin(rAng), cos(rAng), 0.0); // tangential
|
|
246
|
+
vec3 fanned = rEr * rRho
|
|
247
|
+
+ rEt * (pos.z - ${(-8).toFixed(1)}) * 0.5
|
|
248
|
+
+ vec3(0.0, 0.0, pos.y);
|
|
249
|
+
pos = mix(pos, fanned, clamp(uRadialAmount, 0.0, 1.0));
|
|
250
|
+
}
|
|
251
|
+
#endif
|
|
252
|
+
|
|
253
|
+
WaveShape s;
|
|
254
|
+
s.pos = pos;
|
|
255
|
+
s.rotA = rotA;
|
|
256
|
+
s.rotB = rotB;
|
|
257
|
+
s.rotC = rotC;
|
|
258
|
+
return s;
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
160
261
|
const vertexShader = `
|
|
161
262
|
${simplex2d}
|
|
162
263
|
|
|
@@ -175,6 +276,16 @@ uniform float uHelixRoll; // cross-section roll, as a fraction of the turns (1
|
|
|
175
276
|
uniform float uHelixPhase; // degrees
|
|
176
277
|
#endif
|
|
177
278
|
|
|
279
|
+
// Radial fan (optional). Behind RADIAL so a wave without one compiles the exact same program (same
|
|
280
|
+
// byte-identity contract as HELIX / POINTER_FX above).
|
|
281
|
+
#ifdef RADIAL
|
|
282
|
+
uniform float uRadialAmount; // 0..1 blend (0 = identity)
|
|
283
|
+
uniform float uRadialArc; // fan spread, degrees
|
|
284
|
+
uniform float uRadialSpread; // length → radius scale
|
|
285
|
+
uniform float uRadialRadius; // source / inner radius
|
|
286
|
+
uniform float uRadialCenter; // base angle, degrees
|
|
287
|
+
#endif
|
|
288
|
+
|
|
178
289
|
varying vec2 vUv;
|
|
179
290
|
varying vec3 vWorldPos;
|
|
180
291
|
varying vec3 vViewDir;
|
|
@@ -208,27 +319,13 @@ const float RIPPLE_MAX_R = 1.2; // reach where the crest has fully left th
|
|
|
208
319
|
#endif
|
|
209
320
|
#endif
|
|
210
321
|
|
|
211
|
-
|
|
212
|
-
// max() guards pow(0, n) (= Infinity → NaN) so negative n is safe — negative n
|
|
213
|
-
// just concentrates the twist toward the OTHER end instead.
|
|
214
|
-
float expStep(float x, float n){ return exp2(-exp2(n) * pow(max(x, 1.0e-3), n)); }
|
|
215
|
-
|
|
216
|
-
// rotationMatrix (mat4), used row-vector style: pos = (vec4(pos,1) * R).xyz
|
|
217
|
-
mat4 rotationMatrix(vec3 axis, float angle){
|
|
218
|
-
axis = normalize(axis);
|
|
219
|
-
float s = sin(angle), c = cos(angle), oc = 1.0 - c;
|
|
220
|
-
return mat4(
|
|
221
|
-
oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s, 0.0,
|
|
222
|
-
oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s, 0.0,
|
|
223
|
-
oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c, 0.0,
|
|
224
|
-
0.0, 0.0, 0.0, 1.0
|
|
225
|
-
);
|
|
226
|
-
}
|
|
322
|
+
${waveShapeChunk}
|
|
227
323
|
|
|
228
324
|
void main(){
|
|
229
325
|
vUv = uv;
|
|
230
326
|
#ifndef LOOP_MOTION
|
|
231
327
|
float t = uTime * uSpeed + uSeed;
|
|
328
|
+
vec2 loopOff = vec2(0.0); // unused under linear time; kept so waveShape's signature is uniform
|
|
232
329
|
#endif
|
|
233
330
|
|
|
234
331
|
#ifdef LOOP_MOTION
|
|
@@ -241,73 +338,14 @@ void main(){
|
|
|
241
338
|
float loopTheta = uTime * (6.28318530718 / uLoopSeconds) + uSeed;
|
|
242
339
|
float loopR = uSpeed * uLoopSeconds * 0.159154943092; // = uSpeed·uLoopSeconds / (2π)
|
|
243
340
|
vec2 loopOff = loopR * vec2(cos(loopTheta), sin(loopTheta));
|
|
341
|
+
float t = 0.0; // unused under loop time
|
|
244
342
|
#endif
|
|
245
343
|
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX, pos.z * uDispFreqZ) + loopOff);
|
|
252
|
-
#else
|
|
253
|
-
pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX + t, pos.z * uDispFreqZ + t));
|
|
254
|
-
#endif
|
|
255
|
-
#ifdef DETAIL_OCTAVE
|
|
256
|
-
// A second, finer octave riding on the broad swell — fine ripples on top of the big shape, a
|
|
257
|
-
// shape vocabulary single-octave displacement can't reach. Shares the loop orbit so it stays
|
|
258
|
-
// periodic when looping.
|
|
259
|
-
#ifdef LOOP_MOTION
|
|
260
|
-
pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq, pos.z * uDetailFreq) + loopOff);
|
|
261
|
-
#else
|
|
262
|
-
pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq + t, pos.z * uDetailFreq + t));
|
|
263
|
-
#endif
|
|
264
|
-
#endif
|
|
265
|
-
|
|
266
|
-
#ifdef HELIX
|
|
267
|
-
// Helix — the one shape the three twists below cannot reach. Their angle is freq * expStep(uv),
|
|
268
|
-
// a MONOTONE falloff, so it can only ramp once; this one is periodic in uv.y (the length), so
|
|
269
|
-
// uHelixTurns full turns land evenly from end to end. Runs AFTER the displacement so the noise
|
|
270
|
-
// above still samples the undeformed pos.x/pos.z (byte-identical sampling), and BEFORE the twist
|
|
271
|
-
// so the two compose.
|
|
272
|
-
// roll rolls the ribbon's own cross-section about the axis in step with the sweep, swinging
|
|
273
|
-
// its two long edges onto opposite sides — one wave becomes a ladder whose edges are
|
|
274
|
-
// both strands (pair with the wireframe theme's rungs for the rungs between them).
|
|
275
|
-
// radius carries the whole ribbon around the axis instead, orientation intact — a narrow ribbon
|
|
276
|
-
// then reads as ONE strand, and a second wave at phase+180 is the other.
|
|
277
|
-
float hAng = 6.28318530718 * uHelixTurns * uv.y + radians(uHelixPhase);
|
|
278
|
-
// Roll about the ribbon's width centre, not the origin — see RIBBON_Z_CENTER in WaveGeometry.
|
|
279
|
-
float rollA = hAng * uHelixRoll;
|
|
280
|
-
float rollC = cos(rollA), rollS = sin(rollA);
|
|
281
|
-
vec2 rel = vec2(pos.y, pos.z - ${(-8).toFixed(1)});
|
|
282
|
-
pos.y = rel.x * rollC - rel.y * rollS;
|
|
283
|
-
pos.z = ${(-8).toFixed(1)} + rel.x * rollS + rel.y * rollC;
|
|
284
|
-
pos.y += uHelixRadius * cos(hAng);
|
|
285
|
-
pos.z += uHelixRadius * sin(hAng);
|
|
286
|
-
#endif
|
|
287
|
-
|
|
288
|
-
// The X-twist frequency feeding rotB. Two modes: by default uTwFreqX is used
|
|
289
|
-
// directly; the variant (used by the Wave 4 preset) modulates it with
|
|
290
|
-
// simplex noise indexed along the ribbon (uv.y) so the twist breathes over time.
|
|
291
|
-
// We gate the wobble with a #define so the compiled program is unchanged when off.
|
|
292
|
-
float twistXFreq = uTwFreqX;
|
|
293
|
-
#ifdef TWIST_MOTION
|
|
294
|
-
#ifdef LOOP_MOTION
|
|
295
|
-
float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, 0.0) + loopOff);
|
|
296
|
-
#else
|
|
297
|
-
float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, t));
|
|
298
|
-
#endif
|
|
299
|
-
twistXFreq = uTwFreqX - twistXNoise * 0.1;
|
|
300
|
-
#endif
|
|
301
|
-
|
|
302
|
-
// Three-axis twist: expStep falloff sets how
|
|
303
|
-
// sharply each rotation concentrates toward an edge. rotA keys off uv.x, rotB/rotC
|
|
304
|
-
// off uv.y; axes (0.5,0,0.5) and (0,0.5,0.5) are normalised inside rotationMatrix.
|
|
305
|
-
mat4 rotA = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqY * expStep(uv.x, uTwPowY));
|
|
306
|
-
mat4 rotB = rotationMatrix(vec3(0.0, 0.5, 0.5), twistXFreq * expStep(uv.y, uTwPowX));
|
|
307
|
-
mat4 rotC = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqZ * expStep(uv.y, uTwPowZ));
|
|
308
|
-
pos = (vec4(pos, 1.0) * rotA).xyz;
|
|
309
|
-
pos = (vec4(pos, 1.0) * rotB).xyz;
|
|
310
|
-
pos = (vec4(pos, 1.0) * rotC).xyz;
|
|
344
|
+
// Deform the baked hairpin via the shared waveShape chunk (displacement + helix + twist + radial),
|
|
345
|
+
// which also drives the particle shed emitter. It returns the deformed local pos + the twist
|
|
346
|
+
// matrices the pointer field reads below.
|
|
347
|
+
WaveShape ws = waveShape(position, uv, t, loopOff);
|
|
348
|
+
vec3 pos = ws.pos;
|
|
311
349
|
|
|
312
350
|
#ifdef POINTER_FX
|
|
313
351
|
// Pointer field: displace along the wave's own (post-twist) up-axis, weighted by a screen-space
|
|
@@ -330,7 +368,7 @@ void main(){
|
|
|
330
368
|
// point-projection and no perspective divide. (A true per-pixel uv would need GPU picking — the
|
|
331
369
|
// visible surface is shader-displaced, so a CPU raycast of the base geometry misses.)
|
|
332
370
|
if (uShapeFlow > 0.0) {
|
|
333
|
-
vec3 tangentLocal = (((vec4(1.0, 0.0, 0.0, 0.0) * rotA) * rotB) * rotC).xyz;
|
|
371
|
+
vec3 tangentLocal = (((vec4(1.0, 0.0, 0.0, 0.0) * ws.rotA) * ws.rotB) * ws.rotC).xyz;
|
|
334
372
|
vec2 tang = (mvp * vec4(tangentLocal, 0.0)).xy * vec2(uPointerAspect, 1.0);
|
|
335
373
|
float tl = length(tang);
|
|
336
374
|
if (tl > 1.0e-6) {
|
|
@@ -343,7 +381,7 @@ void main(){
|
|
|
343
381
|
vPointerFall = fall * uPointerActive;
|
|
344
382
|
// Displacement axis = local +Y carried through the SAME three twist rotations as pos (row-vector
|
|
345
383
|
// convention). Rotations are linear, so post-twist axis displacement equals pre-twist Y displacement.
|
|
346
|
-
vec3 dispAxis = (((vec4(0.0, 1.0, 0.0, 0.0) * rotA) * rotB) * rotC).xyz;
|
|
384
|
+
vec3 dispAxis = (((vec4(0.0, 1.0, 0.0, 0.0) * ws.rotA) * ws.rotB) * ws.rotC).xyz;
|
|
347
385
|
// Agitation: a fast churn octave near the cursor (additive — never rewrites base noise t, which
|
|
348
386
|
// would force restructuring the shared path). Loop-safe under both time variants.
|
|
349
387
|
#ifdef LOOP_MOTION
|
|
@@ -434,7 +472,7 @@ uniform vec3 uDepthTintColor;
|
|
|
434
472
|
varying vec4 vClipPosition; // clip-space depth (written by the vertex shader for both programs)
|
|
435
473
|
#endif
|
|
436
474
|
#ifdef EDGE_FEATHER
|
|
437
|
-
uniform float uEdgeFeather; // ribbon
|
|
475
|
+
uniform float uEdgeFeather; // softness of the ribbon's two ENDS (only when it differs from 0.1)
|
|
438
476
|
#endif
|
|
439
477
|
#ifdef POINTER_FX
|
|
440
478
|
uniform float uPointerThin; // 0..1 local translucency near the cursor
|
|
@@ -474,8 +512,10 @@ vec3 surfaceStreaks(vec2 uv, vec3 color, float crease){
|
|
|
474
512
|
colorAtten = mix(colorAtten, prm.w, blend);
|
|
475
513
|
paraPow = mix(paraPow, uNoiseBandParaPow[i], blend);
|
|
476
514
|
}
|
|
477
|
-
// The high frequency runs along uv.x (the
|
|
478
|
-
//
|
|
515
|
+
// The high frequency runs along uv.x (the folded WIDTH — see WaveGeometry's UV AXES note),
|
|
516
|
+
// packing many thin stripes across the cross-section while uv.y is barely scaled, so each
|
|
517
|
+
// one stretches out into a fine LENGTHWISE fiber. 1 - parabola(uv.x) then weights them
|
|
518
|
+
// toward the two long edges and away from the width centreline.
|
|
479
519
|
float p = 1.0 - parabola(uv.x, paraPow);
|
|
480
520
|
float n0 = simplexNoise(vec2(uv.x * 0.1, uv.y * 0.5));
|
|
481
521
|
float n1 = simplexNoise(vec2(uv.x * (freq + freq * 0.5 * n0), uv.y * 4.0 * n0));
|
|
@@ -564,7 +604,8 @@ void main(){
|
|
|
564
604
|
|
|
565
605
|
if (uTexture > 0.001) col *= 1.0 + (grainHash(vUv * 850.0) - 0.5) * uTexture * 0.25;
|
|
566
606
|
|
|
567
|
-
// Soft
|
|
607
|
+
// Soft ribbon ENDS (it fades on vUv.y, the length) + optional viewport-edge fade. The edge
|
|
608
|
+
// softness is the hardcoded 0.1 by
|
|
568
609
|
// default (literal branch → byte-identical); EDGE_FEATHER swaps in the uEdgeFeather knob only
|
|
569
610
|
// when it differs, so razor-crisp or vapor-soft edges are both reachable.
|
|
570
611
|
#ifdef EDGE_FEATHER
|
|
@@ -638,7 +679,7 @@ void main(){
|
|
|
638
679
|
color *= 1.0 + uPointerLighten * vPointerFall;
|
|
639
680
|
#endif
|
|
640
681
|
|
|
641
|
-
// Carve into fine
|
|
682
|
+
// Carve into fine lengthwise strands; thickness from the screen-space uv derivative.
|
|
642
683
|
vec2 dy = dFdy(vUv);
|
|
643
684
|
float lineThickness = uLineThickness * pow(abs(dy.x * uMaxWidth), uLineDerivativePower);
|
|
644
685
|
#ifdef POINTER_FX
|
|
@@ -897,7 +938,124 @@ void main(){
|
|
|
897
938
|
gl_FragColor = vec4(mix(src.rgb, outc, clamp(uHalftoneCmyk, 0.0, 1.0)), src.a);
|
|
898
939
|
}
|
|
899
940
|
`;
|
|
941
|
+
const particleVertexShader = `
|
|
942
|
+
attribute float aSeed;
|
|
943
|
+
attribute vec4 aRnd;
|
|
944
|
+
attribute vec2 aUv; // where this particle spawns on the ribbon (x = flank, y = along length; edge-biased at build)
|
|
945
|
+
|
|
946
|
+
uniform float uTime, uLoopSeconds, uLife, uSize, uSizeJitter, uTwinkle, uPixelRatio;
|
|
947
|
+
uniform float uPartSpeed;
|
|
948
|
+
uniform vec3 uColor, uColor2, uCenter, uRight, uUp;
|
|
949
|
+
uniform float uDrift, uRise, uSwirl, uWander;
|
|
950
|
+
|
|
951
|
+
// The owning wave's shape, mirrored in configure() so the dust rides the SAME deform as the ribbon.
|
|
952
|
+
// The HELIX/RADIAL uniform blocks are declared only when the matching #define is set.
|
|
953
|
+
${simplex2d}
|
|
954
|
+
uniform float uDispFreqX, uDispFreqZ, uDispAmount;
|
|
955
|
+
uniform float uDetailFreq, uDetailAmount;
|
|
956
|
+
uniform float uTwFreqX, uTwFreqY, uTwFreqZ, uTwPowX, uTwPowY, uTwPowZ;
|
|
957
|
+
#ifdef HELIX
|
|
958
|
+
uniform float uHelixTurns, uHelixRadius, uHelixRoll, uHelixPhase;
|
|
959
|
+
#endif
|
|
960
|
+
#ifdef RADIAL
|
|
961
|
+
uniform float uRadialAmount, uRadialArc, uRadialSpread, uRadialRadius, uRadialCenter;
|
|
962
|
+
#endif
|
|
963
|
+
uniform mat4 uShedModel; // the wave's matrixWorld (deformed LOCAL → world)
|
|
964
|
+
uniform float uShedSpeed, uShedSeed;
|
|
965
|
+
${waveShapeChunk}
|
|
966
|
+
|
|
967
|
+
varying float vAlpha;
|
|
968
|
+
varying vec3 vColor;
|
|
969
|
+
varying vec2 vDir; // screen-space motion direction (for the streak sprite)
|
|
970
|
+
|
|
971
|
+
const float TAU = 6.28318530718;
|
|
972
|
+
|
|
973
|
+
void main(){
|
|
974
|
+
// Deterministic life: age 0..1 from uTime + a per-particle seed. Advances once per loop period when
|
|
975
|
+
// looping (so the whole field repeats seamlessly), else once per uLife seconds. uPartSpeed scales the
|
|
976
|
+
// cadence (motion speed); under a loop it snaps to a whole number of cycles so the seam stays seamless.
|
|
977
|
+
float cyc = max(1.0, floor(uPartSpeed + 0.5));
|
|
978
|
+
float rate = (uLoopSeconds > 0.0) ? (uTime / uLoopSeconds * cyc) : (uTime * uPartSpeed / max(uLife, 0.001));
|
|
979
|
+
float age = fract(rate + aSeed);
|
|
980
|
+
float fade = sin(3.14159265 * age); // 0 at birth/death, 1 mid-life
|
|
981
|
+
|
|
982
|
+
// Spawn on the owning wave's DEFORMED surface / edge at aUv (via the shared waveShape), then peel
|
|
983
|
+
// outward from the wave centre as the particle ages — silk dissolving into glitter.
|
|
984
|
+
float ts = uTime * uShedSpeed + uShedSeed;
|
|
985
|
+
vec2 loopOff = vec2(0.0);
|
|
986
|
+
#ifdef LOOP_MOTION
|
|
987
|
+
float loopTheta = uTime * (TAU / uLoopSeconds) + uShedSeed;
|
|
988
|
+
float loopR = uShedSpeed * uLoopSeconds * 0.159154943092;
|
|
989
|
+
loopOff = loopR * vec2(cos(loopTheta), sin(loopTheta));
|
|
990
|
+
ts = 0.0;
|
|
991
|
+
#endif
|
|
992
|
+
// Approximate the base hairpin point for this uv (length from uv.y; width centre), then deform it
|
|
993
|
+
// exactly as the wave does. Good enough for dust — the fan / displacement dominate.
|
|
994
|
+
vec3 base = vec3((aUv.y - 0.5) * 400.0, 0.0, ${(-8).toFixed(1)});
|
|
995
|
+
vec3 origin = (uShedModel * vec4(waveShape(base, aUv, ts, loopOff).pos, 1.0)).xyz;
|
|
996
|
+
vec3 outward = normalize(origin - uCenter + vec3(1e-4));
|
|
997
|
+
vec3 p = origin + outward * age * uDrift + (aRnd.xyz - 0.5) * age * uDrift * 0.35;
|
|
998
|
+
|
|
999
|
+
// Motion styles, each 0 = off, all riding age so they stay loop-safe (the age wrap is hidden by
|
|
1000
|
+
// fade→0 at birth/death). rise = screen-vertical buoyancy (embers up / snow down); swirl = orbit
|
|
1001
|
+
// around the wave centre in the screen plane; wander = curl-noise turbulence (fireflies / motes).
|
|
1002
|
+
p += uUp * age * uRise;
|
|
1003
|
+
if (uSwirl != 0.0) {
|
|
1004
|
+
vec3 nrm = cross(uRight, uUp);
|
|
1005
|
+
vec3 rel = p - uCenter;
|
|
1006
|
+
float rx = dot(rel, uRight), ry = dot(rel, uUp), rz = dot(rel, nrm);
|
|
1007
|
+
float a = age * uSwirl * TAU;
|
|
1008
|
+
float ca = cos(a), sa = sin(a);
|
|
1009
|
+
p = uCenter + uRight * (rx * ca - ry * sa) + uUp * (rx * sa + ry * ca) + nrm * rz;
|
|
1010
|
+
}
|
|
1011
|
+
if (uWander != 0.0) {
|
|
1012
|
+
vec2 wan = vec2(simplexNoise(vec2(aSeed * 17.0, age * 3.0)),
|
|
1013
|
+
simplexNoise(vec2(age * 3.0, aSeed * 23.0)));
|
|
1014
|
+
p += (uRight * wan.x + uUp * wan.y) * uWander;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
float tw = 0.5 + 0.5 * sin((age * 9.0 + aSeed) * TAU); // loop-safe flicker (rides age)
|
|
1018
|
+
vAlpha = fade * mix(1.0, tw, clamp(uTwinkle, 0.0, 1.0));
|
|
1019
|
+
vColor = mix(uColor, uColor2, aRnd.w); // two-tone dust: per-particle blend of the two colours
|
|
1020
|
+
vDir = normalize(vec2(dot(outward, uRight), dot(outward, uUp)) + vec2(1e-4)); // outward, in screen space
|
|
1021
|
+
gl_Position = projectionMatrix * viewMatrix * vec4(p, 1.0);
|
|
1022
|
+
// Orthographic camera → point size is constant in device pixels (no perspective depth divide).
|
|
1023
|
+
float jitter = 1.0 + uSizeJitter * (aSeed - 0.5) * 2.0;
|
|
1024
|
+
gl_PointSize = max(uSize * uPixelRatio * jitter * fade, 0.0);
|
|
1025
|
+
}
|
|
1026
|
+
`;
|
|
1027
|
+
const particleFragmentShader = `
|
|
1028
|
+
precision highp float;
|
|
1029
|
+
uniform float uShape; // 0 glitter · 1 soft · 2 ring · 3 star · 4 streak
|
|
1030
|
+
varying float vAlpha;
|
|
1031
|
+
varying vec3 vColor;
|
|
1032
|
+
varying vec2 vDir;
|
|
1033
|
+
void main(){
|
|
1034
|
+
vec2 pc = gl_PointCoord - 0.5;
|
|
1035
|
+
float d = length(pc);
|
|
1036
|
+
int s = int(uShape + 0.5);
|
|
1037
|
+
float a;
|
|
1038
|
+
if (s == 1) { // soft: a diffuse gaussian blob (motes / pollen)
|
|
1039
|
+
a = exp(-d * d * 7.0);
|
|
1040
|
+
} else if (s == 2) { // ring: a hollow band (bubbles)
|
|
1041
|
+
a = smoothstep(0.09, 0.0, abs(d - 0.34));
|
|
1042
|
+
} else if (s == 3) { // star: a 4-point sparkle
|
|
1043
|
+
float ang = atan(pc.y, pc.x);
|
|
1044
|
+
float spike = pow(abs(cos(ang * 2.0)), 6.0);
|
|
1045
|
+
a = smoothstep(1.0, 0.0, d / (0.14 + 0.5 * spike));
|
|
1046
|
+
} else if (s == 4) { // streak: an elongated comet along the motion direction
|
|
1047
|
+
float along = dot(pc, vDir);
|
|
1048
|
+
float perp = dot(pc, vec2(-vDir.y, vDir.x));
|
|
1049
|
+
a = smoothstep(0.5, 0.0, length(vec2(along * 0.42, perp * 2.2)));
|
|
1050
|
+
} else { // glitter (0): the soft round additive disc
|
|
1051
|
+
a = smoothstep(0.5, 0.0, d);
|
|
1052
|
+
}
|
|
1053
|
+
a *= vAlpha;
|
|
1054
|
+
if (a <= 0.0) discard;
|
|
1055
|
+
gl_FragColor = vec4(vColor, a); // AdditiveBlending (src = SrcAlpha) → adds vColor·a
|
|
1056
|
+
}
|
|
1057
|
+
`;
|
|
900
1058
|
//#endregion
|
|
901
|
-
export { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, postFragmentShader, postVertexShader, vertexShader };
|
|
1059
|
+
export { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, particleFragmentShader, particleVertexShader, postFragmentShader, postVertexShader, vertexShader };
|
|
902
1060
|
|
|
903
1061
|
//# sourceMappingURL=shaders.js.map
|