vireuikit 0.3.0 → 0.4.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/README.md +27 -7
- package/dist/{chunk-2AFVQ65S.js → chunk-VY22O2II.js} +324 -33
- package/dist/chunk-VY22O2II.js.map +1 -0
- package/dist/{dynamics-7Bq3LLds.d.cts → dynamics-DqGk_f1n.d.cts} +157 -13
- package/dist/{dynamics-7Bq3LLds.d.ts → dynamics-DqGk_f1n.d.ts} +157 -13
- package/dist/index.cjs +291 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.js +1 -1
- package/dist/web/index.cjs +693 -9
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +63 -2
- package/dist/web/index.d.ts +63 -2
- package/dist/web/index.js +449 -7
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2AFVQ65S.js.map +0 -1
package/dist/web/index.cjs
CHANGED
|
@@ -199,6 +199,51 @@ float2 vgCurlVelocitySeamless(float2 p, float phase, float4 s0, float4 s1, float
|
|
|
199
199
|
return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
|
|
200
200
|
}
|
|
201
201
|
`;
|
|
202
|
+
function hash31JS(x, y, z) {
|
|
203
|
+
const h = x * 127.1 + y * 311.7 + z * 74.7;
|
|
204
|
+
const s = Math.sin(h) * 43758.5453123;
|
|
205
|
+
return s - Math.floor(s);
|
|
206
|
+
}
|
|
207
|
+
var smoothstep3JS = (f) => f * f * (3 - 2 * f);
|
|
208
|
+
var glslMod = (x, y) => x - y * Math.floor(x / y);
|
|
209
|
+
function vgValueNoise3PeriodicJS(px, py, pz, period) {
|
|
210
|
+
const ix = Math.floor(px);
|
|
211
|
+
const iy = Math.floor(py);
|
|
212
|
+
const iz = Math.floor(pz);
|
|
213
|
+
const ux = smoothstep3JS(px - ix);
|
|
214
|
+
const uy = smoothstep3JS(py - iy);
|
|
215
|
+
const uz = smoothstep3JS(pz - iz);
|
|
216
|
+
const z0 = glslMod(iz, period);
|
|
217
|
+
const z1 = glslMod(iz + 1, period);
|
|
218
|
+
const n000 = hash31JS(ix, iy, z0);
|
|
219
|
+
const n100 = hash31JS(ix + 1, iy, z0);
|
|
220
|
+
const n010 = hash31JS(ix, iy + 1, z0);
|
|
221
|
+
const n110 = hash31JS(ix + 1, iy + 1, z0);
|
|
222
|
+
const n001 = hash31JS(ix, iy, z1);
|
|
223
|
+
const n101 = hash31JS(ix + 1, iy, z1);
|
|
224
|
+
const n011 = hash31JS(ix, iy + 1, z1);
|
|
225
|
+
const n111 = hash31JS(ix + 1, iy + 1, z1);
|
|
226
|
+
const nx00 = n000 + (n100 - n000) * ux;
|
|
227
|
+
const nx10 = n010 + (n110 - n010) * ux;
|
|
228
|
+
const nx01 = n001 + (n101 - n001) * ux;
|
|
229
|
+
const nx11 = n011 + (n111 - n011) * ux;
|
|
230
|
+
const nxy0 = nx00 + (nx10 - nx00) * uy;
|
|
231
|
+
const nxy1 = nx01 + (nx11 - nx01) * uy;
|
|
232
|
+
return (nxy0 + (nxy1 - nxy0) * uz) * 2 - 1;
|
|
233
|
+
}
|
|
234
|
+
function vgPotentialJS(px, py, phase) {
|
|
235
|
+
const [o0, o1, o2, o3] = MEDIUM_TIME_OCTAVES;
|
|
236
|
+
let v = vgValueNoise3PeriodicJS(px * o0.scale, py * o0.scale, phase * o0.rate + o0.offset, mediumOctavePeriod(o0.rate)) * 0.5;
|
|
237
|
+
v += vgValueNoise3PeriodicJS(px * o1.scale, py * o1.scale, phase * o1.rate + o1.offset, mediumOctavePeriod(o1.rate)) * 0.25;
|
|
238
|
+
v += vgValueNoise3PeriodicJS(px * o2.scale, py * o2.scale, phase * o2.rate + o2.offset, mediumOctavePeriod(o2.rate)) * 0.125;
|
|
239
|
+
const macro = vgValueNoise3PeriodicJS(
|
|
240
|
+
px * o3.scale,
|
|
241
|
+
py * o3.scale,
|
|
242
|
+
phase * o3.rate + o3.offset,
|
|
243
|
+
mediumOctavePeriod(o3.rate)
|
|
244
|
+
);
|
|
245
|
+
return v * (0.7 + 0.3 * macro);
|
|
246
|
+
}
|
|
202
247
|
var MIN_SPATIAL_PERIOD = 2;
|
|
203
248
|
function computeMediumSpatialPeriods(gridWidth, gridHeight, curlFreq) {
|
|
204
249
|
const freq = Math.max(curlFreq, 1e-6);
|
|
@@ -307,9 +352,46 @@ function neutralSpecies(baseColor) {
|
|
|
307
352
|
}
|
|
308
353
|
|
|
309
354
|
// src/medium/params.ts
|
|
355
|
+
var MEDIUM_MAX_LIGHTS = 3;
|
|
310
356
|
var BASE_COLOR = [0.07, 0.08, 0.1];
|
|
357
|
+
var normalize2 = (v) => {
|
|
358
|
+
const len = Math.hypot(v[0], v[1]) || 1;
|
|
359
|
+
return [v[0] / len, v[1] / len];
|
|
360
|
+
};
|
|
361
|
+
var MEDIUM_DEFAULT_LIGHT_COLOR = [0.78, 0.85, 0.94];
|
|
362
|
+
var MEDIUM_DEFAULT_LIGHTS = [
|
|
363
|
+
{
|
|
364
|
+
position: [0.5, 0],
|
|
365
|
+
direction: [0, 1],
|
|
366
|
+
coneAngle: Math.PI,
|
|
367
|
+
falloff: 16,
|
|
368
|
+
color: MEDIUM_DEFAULT_LIGHT_COLOR,
|
|
369
|
+
intensity: 0.28
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
position: [0.12, 1.45],
|
|
373
|
+
direction: normalize2([0.38, -1.03]),
|
|
374
|
+
coneAngle: 9 * Math.PI / 180,
|
|
375
|
+
falloff: 0.7,
|
|
376
|
+
color: MEDIUM_DEFAULT_LIGHT_COLOR,
|
|
377
|
+
intensity: 1.1
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
position: [0.9, 1.4],
|
|
381
|
+
direction: normalize2([-0.4, -0.98]),
|
|
382
|
+
coneAngle: 8 * Math.PI / 180,
|
|
383
|
+
falloff: 0.75,
|
|
384
|
+
color: MEDIUM_DEFAULT_LIGHT_COLOR,
|
|
385
|
+
intensity: 0.9
|
|
386
|
+
}
|
|
387
|
+
];
|
|
311
388
|
var MEDIUM_DEFAULTS = {
|
|
312
|
-
|
|
389
|
+
// A stamped track has to visibly broaden and fade within the reference's "roughly one to two
|
|
390
|
+
// seconds", not the ~11s half-life a 0.06 decay gives — measured against check-medium.mjs's track
|
|
391
|
+
// gate (birth vs +1.5s cross-section width and total density): 1.1 leaves ~30% of the peak at 1s
|
|
392
|
+
// and ~17% at 1.5s, comfortably faded without dying before MacCormack's own diffusion has time to
|
|
393
|
+
// visibly broaden the stamp.
|
|
394
|
+
decay: 1.1,
|
|
313
395
|
curlFreq: 0.045,
|
|
314
396
|
curlSpeed: 0.12,
|
|
315
397
|
advectSpeed: 26,
|
|
@@ -324,6 +406,7 @@ var MEDIUM_DEFAULTS = {
|
|
|
324
406
|
condensateGain: 5,
|
|
325
407
|
condensateColor: [0.94, 0.95, 0.97],
|
|
326
408
|
channelColors: neutralSpecies(BASE_COLOR),
|
|
409
|
+
channelScatter: [1, 1, 1],
|
|
327
410
|
baseColor: BASE_COLOR,
|
|
328
411
|
baseWeight: 0.9,
|
|
329
412
|
depthFarScale: 1.6,
|
|
@@ -332,11 +415,61 @@ var MEDIUM_DEFAULTS = {
|
|
|
332
415
|
depthFarWeight: 0.5,
|
|
333
416
|
gravityVaporDrift: 0.6,
|
|
334
417
|
gravityBottomBoost: 2,
|
|
335
|
-
gravityBottomBoostStart: 0.75
|
|
418
|
+
gravityBottomBoostStart: 0.75,
|
|
419
|
+
lights: MEDIUM_DEFAULT_LIGHTS,
|
|
420
|
+
shadowExtinction: 3,
|
|
421
|
+
trackLightFloor: 0.8,
|
|
422
|
+
trackLayerAmount: 1
|
|
336
423
|
};
|
|
337
424
|
function depthFarOffsetPx(offsetFrac, gridWidth, gridHeight) {
|
|
338
425
|
return [offsetFrac[0] * gridWidth, offsetFrac[1] * gridHeight];
|
|
339
426
|
}
|
|
427
|
+
var MEDIUM_TRACK_LAYER_LIFE_SECONDS = {
|
|
428
|
+
alpha: 1.8,
|
|
429
|
+
electron: 1.4,
|
|
430
|
+
muon: 2.2
|
|
431
|
+
};
|
|
432
|
+
var MEDIUM_TRACK_LAYER_MAX_TRACKS = 64;
|
|
433
|
+
var MEDIUM_TRACK_LAYER_LOOK = {
|
|
434
|
+
alpha: {
|
|
435
|
+
sigmaFrac: 22e-4,
|
|
436
|
+
endSigmaMul: 1.9,
|
|
437
|
+
broaden: 3.2,
|
|
438
|
+
wiggleFrac: 0,
|
|
439
|
+
beadFrac: 6e-3,
|
|
440
|
+
dropletFrac: 35e-4,
|
|
441
|
+
dropletChance: 0.5,
|
|
442
|
+
startFrac: 0.035,
|
|
443
|
+
gain: 1.8
|
|
444
|
+
},
|
|
445
|
+
electron: {
|
|
446
|
+
sigmaFrac: 9e-4,
|
|
447
|
+
endSigmaMul: 1.3,
|
|
448
|
+
broaden: 6,
|
|
449
|
+
wiggleFrac: 0.012,
|
|
450
|
+
beadFrac: 8e-3,
|
|
451
|
+
dropletFrac: 6e-3,
|
|
452
|
+
dropletChance: 0.55,
|
|
453
|
+
startFrac: 0,
|
|
454
|
+
gain: 2.2
|
|
455
|
+
},
|
|
456
|
+
muon: {
|
|
457
|
+
sigmaFrac: 8e-4,
|
|
458
|
+
endSigmaMul: 1,
|
|
459
|
+
broaden: 5,
|
|
460
|
+
wiggleFrac: 0,
|
|
461
|
+
beadFrac: 7e-3,
|
|
462
|
+
dropletFrac: 5e-3,
|
|
463
|
+
dropletChance: 0.5,
|
|
464
|
+
startFrac: 0,
|
|
465
|
+
gain: 2.2
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
var MEDIUM_TRACK_LAYER_BURST_SPREAD = 1.6;
|
|
469
|
+
var MEDIUM_TRACK_LAYER_BURST_ACCENT = 3;
|
|
470
|
+
var MEDIUM_TRACK_LAYER_FADE_GAMMA = 1.6;
|
|
471
|
+
var MEDIUM_TRACK_LAYER_GRAVITY_SAG = 0.018;
|
|
472
|
+
var MEDIUM_TRACK_LAYER_CURL_PUSH = 0.05;
|
|
340
473
|
|
|
341
474
|
// src/medium/seed-shader.ts
|
|
342
475
|
var MEDIUM_SEED_SHADER = `
|
|
@@ -589,10 +722,35 @@ half4 ${fnName}(float2 px) {
|
|
|
589
722
|
}
|
|
590
723
|
`;
|
|
591
724
|
}
|
|
725
|
+
var VG_LIGHT = `
|
|
726
|
+
float vgLightAmount(float2 xy, float2 lightPosFrac, float2 lightDir, float cosCone, float falloff, float2 resolution, float seed) {
|
|
727
|
+
float2 rel = xy - lightPosFrac * resolution;
|
|
728
|
+
// A cone of 180\xB0 or more is an edge strip: light spreads from a line, not a point.
|
|
729
|
+
if (cosCone <= -0.999) {
|
|
730
|
+
float depth = max(dot(rel, lightDir), 0.0);
|
|
731
|
+
return exp(-(depth / max(resolution.y, 1.0)) * falloff);
|
|
732
|
+
}
|
|
733
|
+
float dist = length(rel);
|
|
734
|
+
float2 dirN = dist > 1e-4 ? rel / dist : lightDir;
|
|
735
|
+
float cosAngle = dot(dirN, lightDir);
|
|
736
|
+
// A soft lobe with half intensity at the cone angle and no boundary at all: a lamp in haze has
|
|
737
|
+
// no drawn edge.
|
|
738
|
+
float lobe = log(0.5) / log(clamp(cosCone, 0.05, 0.999));
|
|
739
|
+
float edge = pow(max(cosAngle, 0.0), lobe);
|
|
740
|
+
float atten = exp(-(dist / max(resolution.y, 1.0)) * falloff);
|
|
741
|
+
if (edge * atten < 0.001) return 0.0;
|
|
742
|
+
// Static shafts inside the beam: brightness varies with the angle around the lamp, so streaks
|
|
743
|
+
// fan out from the source the way light through dusty air does.
|
|
744
|
+
float angle = atan(dirN.x * lightDir.y - dirN.y * lightDir.x, cosAngle);
|
|
745
|
+
float shafts = 0.6 + 0.32 * vgValueNoise(float2(angle * 14.0, seed)) + 0.18 * vgValueNoise(float2(angle * 33.0, seed + 5.3));
|
|
746
|
+
return edge * atten * clamp(shafts, 0.0, 1.0);
|
|
747
|
+
}
|
|
748
|
+
`;
|
|
592
749
|
var MEDIUM_COMPOSITE_SHADER = `
|
|
593
750
|
uniform shader u_vapor;
|
|
594
751
|
uniform shader u_condensate;
|
|
595
752
|
uniform shader u_track;
|
|
753
|
+
uniform shader u_shadow;
|
|
596
754
|
uniform float2 u_dyeScale;
|
|
597
755
|
uniform float3 u_lab0;
|
|
598
756
|
uniform float3 u_lab1;
|
|
@@ -608,11 +766,33 @@ uniform float u_farBlurRadius;
|
|
|
608
766
|
uniform float u_farWeight;
|
|
609
767
|
uniform float u_gravityBoost;
|
|
610
768
|
uniform float u_gravityBoostStart;
|
|
769
|
+
uniform float3 u_channelScatter;
|
|
770
|
+
uniform float2 u_light0Pos;
|
|
771
|
+
uniform float2 u_light0Dir;
|
|
772
|
+
uniform float u_light0CosCone;
|
|
773
|
+
uniform float u_light0Falloff;
|
|
774
|
+
uniform float3 u_light0Color;
|
|
775
|
+
uniform float u_light0Intensity;
|
|
776
|
+
uniform float2 u_light1Pos;
|
|
777
|
+
uniform float2 u_light1Dir;
|
|
778
|
+
uniform float u_light1CosCone;
|
|
779
|
+
uniform float u_light1Falloff;
|
|
780
|
+
uniform float3 u_light1Color;
|
|
781
|
+
uniform float u_light1Intensity;
|
|
782
|
+
uniform float2 u_light2Pos;
|
|
783
|
+
uniform float2 u_light2Dir;
|
|
784
|
+
uniform float u_light2CosCone;
|
|
785
|
+
uniform float u_light2Falloff;
|
|
786
|
+
uniform float3 u_light2Color;
|
|
787
|
+
uniform float u_light2Intensity;
|
|
788
|
+
uniform float u_trackLightFloor;
|
|
611
789
|
|
|
612
790
|
${VG_OKLAB_TO_SRGB}
|
|
613
791
|
${VG_CUBIC_WEIGHTS}
|
|
614
792
|
${vgBicubicSampler("vgBicubicVapor", "u_vapor")}
|
|
615
793
|
${vgBicubicSampler("vgBicubicCondensate", "u_condensate")}
|
|
794
|
+
${VG_VALUE_NOISE}
|
|
795
|
+
${VG_LIGHT}
|
|
616
796
|
|
|
617
797
|
half4 main(float2 xy) {
|
|
618
798
|
float2 baseSrc = xy * u_dyeScale;
|
|
@@ -680,7 +860,34 @@ half4 main(float2 xy) {
|
|
|
680
860
|
float total = max(w0 + w1 + w2 + wBg + wCondensate, 1e-4);
|
|
681
861
|
float3 mixLab =
|
|
682
862
|
(u_labBg * wBg + u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2 + condensateLab * wCondensate) / total;
|
|
683
|
-
|
|
863
|
+
// The dark ambient base \u2014 this is where the OLD mechanism stops (see the file header on
|
|
864
|
+
// VG_LIGHT): unclamped here, clamped once at the very end, after the lit layer adds in.
|
|
865
|
+
float3 rgbLinearBase = vgOklabToLinear(mixLab);
|
|
866
|
+
|
|
867
|
+
// Spotlights reach this point dimmed by the gas in front of them (shadow-shader.ts).
|
|
868
|
+
half4 shadow = u_shadow.eval(baseSrc);
|
|
869
|
+
float light0 = vgLightAmount(xy, u_light0Pos, u_light0Dir, u_light0CosCone, u_light0Falloff, u_resolution, 1.7) * u_light0Intensity;
|
|
870
|
+
float light1 = vgLightAmount(xy, u_light1Pos, u_light1Dir, u_light1CosCone, u_light1Falloff, u_resolution, 4.1) * u_light1Intensity
|
|
871
|
+
* float(shadow.r);
|
|
872
|
+
float light2 = vgLightAmount(xy, u_light2Pos, u_light2Dir, u_light2CosCone, u_light2Falloff, u_resolution, 8.3) * u_light2Intensity
|
|
873
|
+
* float(shadow.g);
|
|
874
|
+
float3 illum = u_light0Color * light0 + u_light1Color * light1 + u_light2Color * light2;
|
|
875
|
+
|
|
876
|
+
float totalScatter = gasR * u_channelScatter.x + gasG * u_channelScatter.y + gasB * u_channelScatter.z;
|
|
877
|
+
float fogDensity = totalScatter + wCondensate;
|
|
878
|
+
float mist = 0.006 + fogDensity * 0.2;
|
|
879
|
+
|
|
880
|
+
// TRACKS: lit the same way as the mist, plus a floor so a track already on screen never vanishes
|
|
881
|
+
// entirely just because it drifted out of a cone (a fresh ionization trail reads brighter than
|
|
882
|
+
// ambient mist in a real chamber). The floor is added to ITS OWN color, not multiplied into illum:
|
|
883
|
+
// illum is exactly [0,0,0] in a fully unlit spot (every light's amount is 0 there), so a floor
|
|
884
|
+
// multiplied by illum would vanish exactly where it exists to help \u2014 it needs a color of its own
|
|
885
|
+
// (a neutral, self-luminous glow) rather than borrowing whichever light happens to be nearby.
|
|
886
|
+
float trackDensity = max(float(track.r), max(float(track.g), float(track.b)));
|
|
887
|
+
float3 trackGlow = (1.0 - exp(-trackDensity * 0.4)) * (illum + float3(u_trackLightFloor)) * 0.08;
|
|
888
|
+
|
|
889
|
+
float3 rgbLinear = rgbLinearBase * 0.025 + illum * mist + trackGlow;
|
|
890
|
+
rgbLinear = float3(1.0) - exp(-rgbLinear * 1.0);
|
|
684
891
|
float3 srgb = vgLinearToSrgb(rgbLinear);
|
|
685
892
|
return half4(half3(srgb), half(1.0));
|
|
686
893
|
}
|
|
@@ -698,7 +905,6 @@ uniform float u_settle;
|
|
|
698
905
|
uniform float u_tailDim;
|
|
699
906
|
uniform float u_intensity;
|
|
700
907
|
uniform float u_seed;
|
|
701
|
-
uniform float3 u_channelMask;
|
|
702
908
|
|
|
703
909
|
${VG_VALUE_NOISE}
|
|
704
910
|
|
|
@@ -721,10 +927,407 @@ half4 main(float2 xy) {
|
|
|
721
927
|
best = max(best, v);
|
|
722
928
|
}
|
|
723
929
|
|
|
724
|
-
float3 dye =
|
|
930
|
+
float3 dye = float3(best * u_intensity);
|
|
725
931
|
return half4(half3(dye), half(1.0));
|
|
726
932
|
}
|
|
727
933
|
`;
|
|
934
|
+
|
|
935
|
+
// src/medium/shadow-shader.ts
|
|
936
|
+
var MEDIUM_SHADOW_SHADER = `
|
|
937
|
+
uniform shader u_vapor;
|
|
938
|
+
uniform shader u_condensate;
|
|
939
|
+
uniform float u_condensateGain;
|
|
940
|
+
uniform float2 u_light1Grid;
|
|
941
|
+
uniform float2 u_light2Grid;
|
|
942
|
+
uniform float u_extinction;
|
|
943
|
+
|
|
944
|
+
float vgOpticalDepth(float2 p, float2 lightP) {
|
|
945
|
+
float2 toLight = lightP - p;
|
|
946
|
+
float optical = 0.0;
|
|
947
|
+
for (int i = 1; i <= 24; i++) {
|
|
948
|
+
float2 q = p + toLight * (float(i) / 24.0);
|
|
949
|
+
float inside = step(0.0, q.x) * step(q.x, u_resolution.x) * step(0.0, q.y) * step(q.y, u_resolution.y);
|
|
950
|
+
half4 v = u_vapor.eval(q);
|
|
951
|
+
half4 c = u_condensate.eval(q);
|
|
952
|
+
optical += inside * (float(v.r + v.g + v.b) + float(c.r) * u_condensateGain);
|
|
953
|
+
}
|
|
954
|
+
return optical * length(toLight) / 24.0 / max(u_resolution.y, 1.0);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
half4 main(float2 xy) {
|
|
958
|
+
float2 p = float2(xy.x, u_resolution.y - xy.y);
|
|
959
|
+
float t1 = exp(-vgOpticalDepth(p, u_light1Grid) * u_extinction);
|
|
960
|
+
float t2 = exp(-vgOpticalDepth(p, u_light2Grid) * u_extinction);
|
|
961
|
+
return half4(half(t1), half(t2), half(0.0), half(1.0));
|
|
962
|
+
}
|
|
963
|
+
`;
|
|
964
|
+
|
|
965
|
+
// src/web/track-layer.ts
|
|
966
|
+
function trackLayerFade(age, life) {
|
|
967
|
+
if (life <= 0) return 0;
|
|
968
|
+
const t = Math.min(Math.max(age / life, 0), 1);
|
|
969
|
+
return (1 - t) ** MEDIUM_TRACK_LAYER_FADE_GAMMA;
|
|
970
|
+
}
|
|
971
|
+
function trackLayerSigmaMul(age, broaden) {
|
|
972
|
+
return Math.sqrt(1 + broaden * Math.max(age, 0));
|
|
973
|
+
}
|
|
974
|
+
function hash01(n) {
|
|
975
|
+
const s = Math.sin(n * 91.3458) * 47453.5453;
|
|
976
|
+
return s - Math.floor(s);
|
|
977
|
+
}
|
|
978
|
+
function curlVelocityJS(px, py, phase) {
|
|
979
|
+
const e = 0.6;
|
|
980
|
+
const px1 = vgPotentialJS(px + e, py, phase);
|
|
981
|
+
const px0 = vgPotentialJS(px - e, py, phase);
|
|
982
|
+
const py1 = vgPotentialJS(px, py + e, phase);
|
|
983
|
+
const py0 = vgPotentialJS(px, py - e, phase);
|
|
984
|
+
return [(py1 - py0) / (2 * e), -(px1 - px0) / (2 * e)];
|
|
985
|
+
}
|
|
986
|
+
var CURL_SAMPLE_SCALE = 40;
|
|
987
|
+
var PHASE_WRAP = 1e4;
|
|
988
|
+
function createTrackLayer() {
|
|
989
|
+
let tracks = [];
|
|
990
|
+
let phase = 0;
|
|
991
|
+
function step(dt, emissions) {
|
|
992
|
+
phase = (phase + dt) % PHASE_WRAP;
|
|
993
|
+
const next = [];
|
|
994
|
+
for (const track of tracks) {
|
|
995
|
+
const life = MEDIUM_TRACK_LAYER_LIFE_SECONDS[track.emission.kind];
|
|
996
|
+
const age = track.age + dt;
|
|
997
|
+
if (age >= life) continue;
|
|
998
|
+
if (age <= 0) {
|
|
999
|
+
next.push({ ...track, age });
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const midX = track.emission.source[0] + Math.cos(track.emission.angle) * track.emission.lengthFrac * 0.5;
|
|
1003
|
+
const midY = track.emission.source[1] + Math.sin(track.emission.angle) * track.emission.lengthFrac * 0.5;
|
|
1004
|
+
const [curlX, curlY] = curlVelocityJS(midX * CURL_SAMPLE_SCALE, midY * CURL_SAMPLE_SCALE, phase);
|
|
1005
|
+
const activeDt = Math.min(dt, age);
|
|
1006
|
+
next.push({
|
|
1007
|
+
emission: track.emission,
|
|
1008
|
+
age,
|
|
1009
|
+
driftX: track.driftX + curlX * MEDIUM_TRACK_LAYER_CURL_PUSH * activeDt,
|
|
1010
|
+
driftY: track.driftY + (curlY * MEDIUM_TRACK_LAYER_CURL_PUSH - MEDIUM_TRACK_LAYER_GRAVITY_SAG) * activeDt
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
let burstIndex = 0;
|
|
1014
|
+
for (const emission of emissions) {
|
|
1015
|
+
let age = 0;
|
|
1016
|
+
if (emission.kind === "alpha") {
|
|
1017
|
+
if (burstIndex >= MEDIUM_TRACK_LAYER_BURST_ACCENT) {
|
|
1018
|
+
age = -hash01(emission.seed * 3.17 + burstIndex) * MEDIUM_TRACK_LAYER_BURST_SPREAD;
|
|
1019
|
+
}
|
|
1020
|
+
burstIndex += 1;
|
|
1021
|
+
}
|
|
1022
|
+
next.push({ emission, age, driftX: 0, driftY: 0 });
|
|
1023
|
+
}
|
|
1024
|
+
tracks = next.length > MEDIUM_TRACK_LAYER_MAX_TRACKS ? next.slice(next.length - MEDIUM_TRACK_LAYER_MAX_TRACKS) : next;
|
|
1025
|
+
return tracks;
|
|
1026
|
+
}
|
|
1027
|
+
return { step };
|
|
1028
|
+
}
|
|
1029
|
+
var TRACK_LAYER_VERTEX_SOURCE = `#version 300 es
|
|
1030
|
+
layout(location = 0) in vec2 a_corner;
|
|
1031
|
+
layout(location = 1) in vec4 a_geom;
|
|
1032
|
+
layout(location = 2) in vec4 a_span;
|
|
1033
|
+
layout(location = 3) in vec4 a_path;
|
|
1034
|
+
layout(location = 4) in vec4 a_grain;
|
|
1035
|
+
|
|
1036
|
+
uniform vec2 u_resolution;
|
|
1037
|
+
uniform float u_trackLightFloor;
|
|
1038
|
+
uniform sampler2D u_shadow;
|
|
1039
|
+
uniform vec2 u_light0Pos;
|
|
1040
|
+
uniform vec2 u_light0Dir;
|
|
1041
|
+
uniform float u_light0CosCone;
|
|
1042
|
+
uniform float u_light0Falloff;
|
|
1043
|
+
uniform vec3 u_light0Color;
|
|
1044
|
+
uniform float u_light0Intensity;
|
|
1045
|
+
uniform vec2 u_light1Pos;
|
|
1046
|
+
uniform vec2 u_light1Dir;
|
|
1047
|
+
uniform float u_light1CosCone;
|
|
1048
|
+
uniform float u_light1Falloff;
|
|
1049
|
+
uniform vec3 u_light1Color;
|
|
1050
|
+
uniform float u_light1Intensity;
|
|
1051
|
+
uniform vec2 u_light2Pos;
|
|
1052
|
+
uniform vec2 u_light2Dir;
|
|
1053
|
+
uniform float u_light2CosCone;
|
|
1054
|
+
uniform float u_light2Falloff;
|
|
1055
|
+
uniform vec3 u_light2Color;
|
|
1056
|
+
uniform float u_light2Intensity;
|
|
1057
|
+
|
|
1058
|
+
out vec2 v_local;
|
|
1059
|
+
flat out vec4 v_span;
|
|
1060
|
+
flat out vec4 v_path;
|
|
1061
|
+
flat out vec4 v_grain;
|
|
1062
|
+
flat out float v_reach;
|
|
1063
|
+
out vec3 v_illum;
|
|
1064
|
+
|
|
1065
|
+
float vgHash21(vec2 p) {
|
|
1066
|
+
float h = dot(p, vec2(127.1, 311.7));
|
|
1067
|
+
return fract(sin(h) * 43758.5453123);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
float vgValueNoise(vec2 p) {
|
|
1071
|
+
vec2 i = floor(p);
|
|
1072
|
+
vec2 f = fract(p);
|
|
1073
|
+
float a = vgHash21(i);
|
|
1074
|
+
float b = vgHash21(i + vec2(1.0, 0.0));
|
|
1075
|
+
float c = vgHash21(i + vec2(0.0, 1.0));
|
|
1076
|
+
float d = vgHash21(i + vec2(1.0, 1.0));
|
|
1077
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
1078
|
+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 2.0 - 1.0;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Keep in step with vgLightAmount in composite-shader.ts.
|
|
1082
|
+
float vgLightAmount(vec2 xy, vec2 lightPosFrac, vec2 lightDir, float cosCone, float falloff, vec2 resolution, float seed) {
|
|
1083
|
+
vec2 rel = xy - lightPosFrac * resolution;
|
|
1084
|
+
if (cosCone <= -0.999) {
|
|
1085
|
+
float depth = max(dot(rel, lightDir), 0.0);
|
|
1086
|
+
return exp(-(depth / max(resolution.y, 1.0)) * falloff);
|
|
1087
|
+
}
|
|
1088
|
+
float dist = length(rel);
|
|
1089
|
+
vec2 dirN = dist > 1e-4 ? rel / dist : lightDir;
|
|
1090
|
+
float cosAngle = dot(dirN, lightDir);
|
|
1091
|
+
float lobe = log(0.5) / log(clamp(cosCone, 0.05, 0.999));
|
|
1092
|
+
float edge = pow(max(cosAngle, 0.0), lobe);
|
|
1093
|
+
float atten = exp(-(dist / max(resolution.y, 1.0)) * falloff);
|
|
1094
|
+
if (edge * atten < 0.001) return 0.0;
|
|
1095
|
+
float angle = atan(dirN.x * lightDir.y - dirN.y * lightDir.x, cosAngle);
|
|
1096
|
+
float shafts = 0.6 + 0.32 * vgValueNoise(vec2(angle * 14.0, seed)) + 0.18 * vgValueNoise(vec2(angle * 33.0, seed + 5.3));
|
|
1097
|
+
return edge * atten * clamp(shafts, 0.0, 1.0);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
void main() {
|
|
1101
|
+
vec2 dir = a_geom.zw;
|
|
1102
|
+
vec2 side = vec2(-dir.y, dir.x);
|
|
1103
|
+
// Beads widen sigma by up to 20%; 3.5 sigma leaves under 1% of the core outside the quad.
|
|
1104
|
+
float reach = max(a_span.z, a_span.w) * 1.2 * 3.5 + 1.5;
|
|
1105
|
+
float margin = reach + a_path.x * 1.7;
|
|
1106
|
+
float u = mix(a_span.y - reach, a_span.x + reach, a_corner.y);
|
|
1107
|
+
float s = a_corner.x * margin;
|
|
1108
|
+
vec2 pos = a_geom.xy + dir * u + side * s;
|
|
1109
|
+
v_local = vec2(u, s);
|
|
1110
|
+
v_span = a_span;
|
|
1111
|
+
v_path = a_path;
|
|
1112
|
+
v_grain = a_grain;
|
|
1113
|
+
v_reach = reach;
|
|
1114
|
+
// Lit per corner: light varies over hundreds of px, a track is a few px wide. The backdrop
|
|
1115
|
+
// target stores the scene top row at y = 0; lights use y up.
|
|
1116
|
+
vec2 xy = vec2(pos.x, u_resolution.y - pos.y);
|
|
1117
|
+
// The same gas shadow the composite applies to the spotlights (shadow-shader.ts, y up).
|
|
1118
|
+
vec4 shadow = texture(u_shadow, xy / u_resolution);
|
|
1119
|
+
float light0 = vgLightAmount(xy, u_light0Pos, u_light0Dir, u_light0CosCone, u_light0Falloff, u_resolution, 1.7) * u_light0Intensity;
|
|
1120
|
+
float light1 = vgLightAmount(xy, u_light1Pos, u_light1Dir, u_light1CosCone, u_light1Falloff, u_resolution, 4.1) * u_light1Intensity * shadow.r;
|
|
1121
|
+
float light2 = vgLightAmount(xy, u_light2Pos, u_light2Dir, u_light2CosCone, u_light2Falloff, u_resolution, 8.3) * u_light2Intensity * shadow.g;
|
|
1122
|
+
v_illum = u_light0Color * light0 + u_light1Color * light1 + u_light2Color * light2 + vec3(u_trackLightFloor);
|
|
1123
|
+
gl_Position = vec4((pos / u_resolution) * 2.0 - 1.0, 0.0, 1.0);
|
|
1124
|
+
}
|
|
1125
|
+
`;
|
|
1126
|
+
var TRACK_LAYER_FRAGMENT_SOURCE = `#version 300 es
|
|
1127
|
+
precision highp float;
|
|
1128
|
+
|
|
1129
|
+
in vec2 v_local;
|
|
1130
|
+
flat in vec4 v_span;
|
|
1131
|
+
flat in vec4 v_path;
|
|
1132
|
+
flat in vec4 v_grain;
|
|
1133
|
+
flat in float v_reach;
|
|
1134
|
+
in vec3 v_illum;
|
|
1135
|
+
|
|
1136
|
+
out vec4 fragColor;
|
|
1137
|
+
|
|
1138
|
+
float vgHash(float a, float b) {
|
|
1139
|
+
uint x = floatBitsToUint(a) * 0x9E3779B1u ^ (floatBitsToUint(b) + 0x7F4A7C15u);
|
|
1140
|
+
x ^= x >> 16;
|
|
1141
|
+
x *= 0x7FEB352Du;
|
|
1142
|
+
x ^= x >> 15;
|
|
1143
|
+
x *= 0x846CA68Bu;
|
|
1144
|
+
x ^= x >> 16;
|
|
1145
|
+
return float(x) * (1.0 / 4294967295.0);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
float vgNoise1(float x, float salt) {
|
|
1149
|
+
float i = floor(x);
|
|
1150
|
+
float f = fract(x);
|
|
1151
|
+
float u = f * f * (3.0 - 2.0 * f);
|
|
1152
|
+
return mix(vgHash(i, salt), vgHash(i + 1.0, salt), u);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
void main() {
|
|
1156
|
+
float len = v_span.x;
|
|
1157
|
+
float start = v_span.y;
|
|
1158
|
+
float sigma0 = v_span.z;
|
|
1159
|
+
float sigma1 = v_span.w;
|
|
1160
|
+
float wiggleAmp = v_path.x;
|
|
1161
|
+
float wiggleFreq = v_path.y;
|
|
1162
|
+
float amp = v_path.z;
|
|
1163
|
+
float seed = v_path.w;
|
|
1164
|
+
float beadPx = v_grain.x;
|
|
1165
|
+
float dropPx = v_grain.y;
|
|
1166
|
+
float dropChance = v_grain.z;
|
|
1167
|
+
float blur = v_grain.w;
|
|
1168
|
+
|
|
1169
|
+
float u = v_local.x;
|
|
1170
|
+
float s = v_local.y;
|
|
1171
|
+
float t = clamp((u - start) / max(len - start, 1.0), 0.0, 1.0);
|
|
1172
|
+
|
|
1173
|
+
float wiggle = 0.0;
|
|
1174
|
+
if (wiggleAmp > 0.0) {
|
|
1175
|
+
float x = u * wiggleFreq;
|
|
1176
|
+
wiggle = wiggleAmp * smoothstep(0.0, 0.2, t) *
|
|
1177
|
+
((vgNoise1(x, seed) - 0.5) * 2.0 + (vgNoise1(x * 3.1, seed + 1.0) - 0.5) * 0.7);
|
|
1178
|
+
}
|
|
1179
|
+
float across = s - wiggle;
|
|
1180
|
+
if (abs(across) > v_reach) discard;
|
|
1181
|
+
|
|
1182
|
+
float bead = vgNoise1(u / beadPx, seed + 2.0);
|
|
1183
|
+
// Blur (age, defocus) washes the beads out rather than stretching them into ribs.
|
|
1184
|
+
float sigma = mix(sigma0, sigma1, t * t) * (1.0 + (bead - 0.5) * 0.4 / blur);
|
|
1185
|
+
float beyond = u < start ? start - u : max(u - len, 0.0);
|
|
1186
|
+
float core = exp(-0.5 * (across * across + beyond * beyond) / (sigma * sigma));
|
|
1187
|
+
float along = 1.0 - 0.35 / blur * vgNoise1(u / (beadPx * 2.3), seed + 3.0);
|
|
1188
|
+
|
|
1189
|
+
float drops = 0.0;
|
|
1190
|
+
float cell0 = floor(u / dropPx);
|
|
1191
|
+
for (int k = -1; k <= 1; k++) {
|
|
1192
|
+
float c = cell0 + float(k);
|
|
1193
|
+
if (vgHash(c, seed + 4.0) > dropChance) continue;
|
|
1194
|
+
float cu = (c + vgHash(c, seed + 5.0)) * dropPx;
|
|
1195
|
+
if (cu < start || cu > len) continue;
|
|
1196
|
+
float cs = (vgHash(c, seed + 6.0) + vgHash(c, seed + 7.0) - 1.0) * 1.7 * sigma + wiggle;
|
|
1197
|
+
float r = max(0.7, sigma * mix(0.35, 0.8, vgHash(c, seed + 8.0)));
|
|
1198
|
+
float dx = u - cu;
|
|
1199
|
+
float dy = s - cs;
|
|
1200
|
+
drops += mix(0.35, 1.0, vgHash(c, seed + 9.0)) * exp(-0.5 * (dx * dx + dy * dy) / (r * r));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
float lum = amp * (core * along + drops * 0.7 / (blur * blur));
|
|
1204
|
+
if (lum < 0.002) discard;
|
|
1205
|
+
|
|
1206
|
+
vec3 glow = lum * v_illum;
|
|
1207
|
+
fragColor = vec4(vec3(1.0) - exp(-glow), 1.0);
|
|
1208
|
+
}
|
|
1209
|
+
`;
|
|
1210
|
+
var FLOATS_PER_INSTANCE = 16;
|
|
1211
|
+
var DEFOCUS_FROM_DEPTH = 0.7;
|
|
1212
|
+
var DEFOCUS_MAX = 2.4;
|
|
1213
|
+
function buildTrackLayerInstances(tracks, contentWidth, contentHeight, amount) {
|
|
1214
|
+
const minDim = Math.min(contentWidth, contentHeight);
|
|
1215
|
+
const data = new Float32Array(tracks.length * FLOATS_PER_INSTANCE);
|
|
1216
|
+
let count = 0;
|
|
1217
|
+
if (amount <= 0) return { data, count };
|
|
1218
|
+
for (const track of tracks) {
|
|
1219
|
+
if (track.age < 0) continue;
|
|
1220
|
+
const e = track.emission;
|
|
1221
|
+
const fade = trackLayerFade(track.age, MEDIUM_TRACK_LAYER_LIFE_SECONDS[e.kind]);
|
|
1222
|
+
if (fade <= 1e-4) continue;
|
|
1223
|
+
const look = MEDIUM_TRACK_LAYER_LOOK[e.kind];
|
|
1224
|
+
const sigmaMul = trackLayerSigmaMul(track.age, look.broaden);
|
|
1225
|
+
const near = Math.max(0, (e.depth - DEFOCUS_FROM_DEPTH) / (1 - DEFOCUS_FROM_DEPTH));
|
|
1226
|
+
const defocus = 1 + (DEFOCUS_MAX - 1) * near * near;
|
|
1227
|
+
const sigma0 = Math.max(look.sigmaFrac * minDim, 0.6) * sigmaMul * defocus;
|
|
1228
|
+
const o = count * FLOATS_PER_INSTANCE;
|
|
1229
|
+
data[o + 0] = e.source[0] * contentWidth + track.driftX * minDim;
|
|
1230
|
+
data[o + 1] = e.source[1] * contentHeight - track.driftY * minDim;
|
|
1231
|
+
data[o + 2] = Math.cos(e.angle);
|
|
1232
|
+
data[o + 3] = Math.sin(e.angle);
|
|
1233
|
+
const len = e.lengthFrac * minDim;
|
|
1234
|
+
data[o + 4] = len;
|
|
1235
|
+
data[o + 5] = Math.min(look.startFrac * minDim, len * 0.5);
|
|
1236
|
+
data[o + 6] = sigma0;
|
|
1237
|
+
data[o + 7] = sigma0 * look.endSigmaMul;
|
|
1238
|
+
data[o + 8] = e.raggedFrac > 0 ? look.wiggleFrac * minDim : 0;
|
|
1239
|
+
data[o + 9] = e.raggedFreq / Math.max(len, 1);
|
|
1240
|
+
data[o + 10] = e.intensity * look.gain * fade * amount / (sigmaMul ** 0.7 * defocus);
|
|
1241
|
+
data[o + 11] = e.seed;
|
|
1242
|
+
data[o + 12] = look.beadFrac * minDim;
|
|
1243
|
+
data[o + 13] = look.dropletFrac * minDim;
|
|
1244
|
+
data[o + 14] = look.dropletChance;
|
|
1245
|
+
data[o + 15] = sigmaMul * defocus;
|
|
1246
|
+
count += 1;
|
|
1247
|
+
}
|
|
1248
|
+
return { data, count };
|
|
1249
|
+
}
|
|
1250
|
+
function compile(gl, type, source, label) {
|
|
1251
|
+
const shader = gl.createShader(type);
|
|
1252
|
+
if (!shader) throw new Error(`vireuikit: track layer ${label} shader creation failed`);
|
|
1253
|
+
gl.shaderSource(shader, source);
|
|
1254
|
+
gl.compileShader(shader);
|
|
1255
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1256
|
+
throw new Error(`vireuikit: track layer ${label} shader failed:
|
|
1257
|
+
${gl.getShaderInfoLog(shader) ?? ""}`);
|
|
1258
|
+
}
|
|
1259
|
+
return shader;
|
|
1260
|
+
}
|
|
1261
|
+
function createTrackLayerProgram(gl) {
|
|
1262
|
+
const vertex = compile(gl, gl.VERTEX_SHADER, TRACK_LAYER_VERTEX_SOURCE, "vertex");
|
|
1263
|
+
const fragment = compile(gl, gl.FRAGMENT_SHADER, TRACK_LAYER_FRAGMENT_SOURCE, "fragment");
|
|
1264
|
+
const program = gl.createProgram();
|
|
1265
|
+
if (!program) throw new Error("vireuikit: track layer program creation failed");
|
|
1266
|
+
gl.attachShader(program, vertex);
|
|
1267
|
+
gl.attachShader(program, fragment);
|
|
1268
|
+
gl.linkProgram(program);
|
|
1269
|
+
gl.deleteShader(vertex);
|
|
1270
|
+
gl.deleteShader(fragment);
|
|
1271
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1272
|
+
throw new Error(`vireuikit: track layer program failed to link:
|
|
1273
|
+
${gl.getProgramInfoLog(program) ?? ""}`);
|
|
1274
|
+
}
|
|
1275
|
+
const loc = web.locationCache(gl, program);
|
|
1276
|
+
const quadBuffer = gl.createBuffer();
|
|
1277
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
1278
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, 0, 1, 0, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
|
1279
|
+
const instanceBuffer = gl.createBuffer();
|
|
1280
|
+
const vao = gl.createVertexArray();
|
|
1281
|
+
gl.bindVertexArray(vao);
|
|
1282
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
1283
|
+
gl.enableVertexAttribArray(0);
|
|
1284
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
1285
|
+
const stride = FLOATS_PER_INSTANCE * 4;
|
|
1286
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
1287
|
+
for (let location = 1; location <= 4; location += 1) {
|
|
1288
|
+
gl.enableVertexAttribArray(location);
|
|
1289
|
+
gl.vertexAttribPointer(location, 4, gl.FLOAT, false, stride, (location - 1) * 16);
|
|
1290
|
+
gl.vertexAttribDivisor(location, 1);
|
|
1291
|
+
}
|
|
1292
|
+
gl.bindVertexArray(null);
|
|
1293
|
+
function bindLight(slot, light) {
|
|
1294
|
+
const position = light?.position ?? [0, 0];
|
|
1295
|
+
const direction = light?.direction ?? [0, 1];
|
|
1296
|
+
const dirLen = Math.hypot(direction[0], direction[1]) || 1;
|
|
1297
|
+
web.setUniform(gl, loc(`u_light${slot}Pos`), position);
|
|
1298
|
+
web.setUniform(gl, loc(`u_light${slot}Dir`), [direction[0] / dirLen, direction[1] / dirLen]);
|
|
1299
|
+
web.setUniform(gl, loc(`u_light${slot}CosCone`), Math.cos(light?.coneAngle ?? 0));
|
|
1300
|
+
web.setUniform(gl, loc(`u_light${slot}Falloff`), light?.falloff ?? 0);
|
|
1301
|
+
web.setUniform(gl, loc(`u_light${slot}Color`), light?.color ?? [0, 0, 0]);
|
|
1302
|
+
web.setUniform(gl, loc(`u_light${slot}Intensity`), light?.intensity ?? 0);
|
|
1303
|
+
}
|
|
1304
|
+
function draw(contentWidth, contentHeight, data, count, trackLightFloor, lights, shadow) {
|
|
1305
|
+
if (count <= 0) return;
|
|
1306
|
+
gl.useProgram(program);
|
|
1307
|
+
gl.bindVertexArray(vao);
|
|
1308
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
1309
|
+
gl.bufferData(gl.ARRAY_BUFFER, data.subarray(0, count * FLOATS_PER_INSTANCE), gl.DYNAMIC_DRAW);
|
|
1310
|
+
web.setUniform(gl, loc("u_resolution"), [contentWidth, contentHeight]);
|
|
1311
|
+
web.setUniform(gl, loc("u_trackLightFloor"), trackLightFloor);
|
|
1312
|
+
for (let slot = 0; slot < 3; slot += 1) bindLight(slot, lights[slot]);
|
|
1313
|
+
web.bindTextureAt(gl, 0, shadow, program, "u_shadow");
|
|
1314
|
+
gl.enable(gl.BLEND);
|
|
1315
|
+
gl.blendFunc(gl.ONE, gl.ONE);
|
|
1316
|
+
gl.disable(gl.DEPTH_TEST);
|
|
1317
|
+
gl.viewport(0, 0, contentWidth, contentHeight);
|
|
1318
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
|
|
1319
|
+
gl.bindVertexArray(null);
|
|
1320
|
+
}
|
|
1321
|
+
function destroy() {
|
|
1322
|
+
gl.deleteBuffer(quadBuffer);
|
|
1323
|
+
gl.deleteBuffer(instanceBuffer);
|
|
1324
|
+
gl.deleteVertexArray(vao);
|
|
1325
|
+
gl.deleteProgram(program);
|
|
1326
|
+
}
|
|
1327
|
+
return { draw, destroy };
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/web/medium.ts
|
|
728
1331
|
function pickDyeFormat(gl) {
|
|
729
1332
|
const hasFloat = gl.getExtension("EXT_color_buffer_float") !== null;
|
|
730
1333
|
return hasFloat ? { internalFormat: gl.RGBA16F, type: gl.HALF_FLOAT } : { internalFormat: gl.RGBA8, type: gl.UNSIGNED_BYTE };
|
|
@@ -742,6 +1345,10 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
742
1345
|
const seedProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_SEED_SHADER));
|
|
743
1346
|
const emitProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_EMIT_SHADER));
|
|
744
1347
|
const resampleProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_RESAMPLE_SHADER));
|
|
1348
|
+
const shadowProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_SHADOW_SHADER));
|
|
1349
|
+
const trackLayerProgram = createTrackLayerProgram(gl);
|
|
1350
|
+
const trackLayer = createTrackLayer();
|
|
1351
|
+
let liveTracks = [];
|
|
745
1352
|
const vaporForwardLoc = web.locationCache(gl, vaporForwardProgram);
|
|
746
1353
|
const vaporCorrectLoc = web.locationCache(gl, vaporCorrectProgram);
|
|
747
1354
|
const vaporReactLoc = web.locationCache(gl, vaporReactProgram);
|
|
@@ -753,6 +1360,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
753
1360
|
const seedLoc = web.locationCache(gl, seedProgram);
|
|
754
1361
|
const emitLoc = web.locationCache(gl, emitProgram);
|
|
755
1362
|
const resampleLoc = web.locationCache(gl, resampleProgram);
|
|
1363
|
+
const shadowLoc = web.locationCache(gl, shadowProgram);
|
|
756
1364
|
let gridW = 0;
|
|
757
1365
|
let gridH = 0;
|
|
758
1366
|
let vapor = null;
|
|
@@ -762,6 +1370,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
762
1370
|
let vaporCorrected = null;
|
|
763
1371
|
let condensateForward = null;
|
|
764
1372
|
let condensateCorrected = null;
|
|
1373
|
+
let shadow = null;
|
|
765
1374
|
let front = 0;
|
|
766
1375
|
function makeTarget(w, h) {
|
|
767
1376
|
const texture = web.createTexture(gl, {
|
|
@@ -805,6 +1414,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
805
1414
|
destroyTarget(vaporCorrected);
|
|
806
1415
|
destroyTarget(condensateForward);
|
|
807
1416
|
destroyTarget(condensateCorrected);
|
|
1417
|
+
destroyTarget(shadow);
|
|
808
1418
|
vapor = null;
|
|
809
1419
|
condensate = null;
|
|
810
1420
|
track = null;
|
|
@@ -812,6 +1422,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
812
1422
|
vaporCorrected = null;
|
|
813
1423
|
condensateForward = null;
|
|
814
1424
|
condensateCorrected = null;
|
|
1425
|
+
shadow = null;
|
|
815
1426
|
}
|
|
816
1427
|
function seed(w, h) {
|
|
817
1428
|
if (!vapor || !condensate || !track) return;
|
|
@@ -865,6 +1476,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
865
1476
|
vaporCorrected = makeTarget(w, h);
|
|
866
1477
|
condensateForward = makeTarget(w, h);
|
|
867
1478
|
condensateCorrected = makeTarget(w, h);
|
|
1479
|
+
shadow = makeTarget(w, h);
|
|
868
1480
|
front = 0;
|
|
869
1481
|
const areaRatio = w * h / (oldW * oldH);
|
|
870
1482
|
waterTargetTotal *= areaRatio;
|
|
@@ -883,6 +1495,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
883
1495
|
vaporCorrected = makeTarget(w, h);
|
|
884
1496
|
condensateForward = makeTarget(w, h);
|
|
885
1497
|
condensateCorrected = makeTarget(w, h);
|
|
1498
|
+
shadow = makeTarget(w, h);
|
|
886
1499
|
front = 0;
|
|
887
1500
|
seed(w, h);
|
|
888
1501
|
waterTargetTotal = readTarget(vapor[front]) + readTarget(condensate[front]);
|
|
@@ -893,6 +1506,17 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
893
1506
|
}
|
|
894
1507
|
resizeGrid(w, h);
|
|
895
1508
|
}
|
|
1509
|
+
function bindLightUniforms(slot, light) {
|
|
1510
|
+
const position = light?.position ?? [0, 0];
|
|
1511
|
+
const direction = light?.direction ?? [0, 1];
|
|
1512
|
+
const dirLen = Math.hypot(direction[0], direction[1]) || 1;
|
|
1513
|
+
web.setUniform(gl, compositeLoc(`u_light${slot}Pos`), position);
|
|
1514
|
+
web.setUniform(gl, compositeLoc(`u_light${slot}Dir`), [direction[0] / dirLen, direction[1] / dirLen]);
|
|
1515
|
+
web.setUniform(gl, compositeLoc(`u_light${slot}CosCone`), Math.cos(light?.coneAngle ?? 0));
|
|
1516
|
+
web.setUniform(gl, compositeLoc(`u_light${slot}Falloff`), light?.falloff ?? 0);
|
|
1517
|
+
web.setUniform(gl, compositeLoc(`u_light${slot}Color`), light?.color ?? [0, 0, 0]);
|
|
1518
|
+
web.setUniform(gl, compositeLoc(`u_light${slot}Intensity`), light?.intensity ?? 0);
|
|
1519
|
+
}
|
|
896
1520
|
function bindVelocityUniforms(loc, dt, phase2, p, spatial) {
|
|
897
1521
|
web.setUniform(gl, loc("u_dyeSize"), [gridW, gridH]);
|
|
898
1522
|
web.setUniform(gl, loc("u_resolution"), [gridW, gridH]);
|
|
@@ -918,7 +1542,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
918
1542
|
let timeSinceRenorm = 0;
|
|
919
1543
|
let framesSinceRenorm = 0;
|
|
920
1544
|
function step(dt, params = {}) {
|
|
921
|
-
if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected) {
|
|
1545
|
+
if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected || !shadow) {
|
|
922
1546
|
return;
|
|
923
1547
|
}
|
|
924
1548
|
const p = { ...MEDIUM_DEFAULTS, ...params };
|
|
@@ -992,6 +1616,20 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
992
1616
|
bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p, spatial);
|
|
993
1617
|
web.setUniform(gl, trackAdvectLoc("u_decay"), p.decay);
|
|
994
1618
|
web.drawFullscreenTriangle(gl);
|
|
1619
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, shadow.fbo);
|
|
1620
|
+
gl.useProgram(shadowProgram);
|
|
1621
|
+
web.bindTextureAt(gl, 0, vapor[back].texture, shadowProgram, "u_vapor");
|
|
1622
|
+
web.bindTextureAt(gl, 1, condensate[back].texture, shadowProgram, "u_condensate");
|
|
1623
|
+
web.setUniform(gl, shadowLoc("u_vaporSize"), gridSize);
|
|
1624
|
+
web.setUniform(gl, shadowLoc("u_condensateSize"), gridSize);
|
|
1625
|
+
web.setUniform(gl, shadowLoc("u_resolution"), gridSize);
|
|
1626
|
+
web.setUniform(gl, shadowLoc("u_condensateGain"), p.condensateGain);
|
|
1627
|
+
web.setUniform(gl, shadowLoc("u_extinction"), p.shadowExtinction);
|
|
1628
|
+
for (const slot of [1, 2]) {
|
|
1629
|
+
const position = p.lights[slot]?.position ?? [0, 0];
|
|
1630
|
+
web.setUniform(gl, shadowLoc(`u_light${slot}Grid`), [position[0] * gridW, position[1] * gridH]);
|
|
1631
|
+
}
|
|
1632
|
+
web.drawFullscreenTriangle(gl);
|
|
995
1633
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
996
1634
|
front = back;
|
|
997
1635
|
framesSinceRenorm += 1;
|
|
@@ -1033,14 +1671,16 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
1033
1671
|
web.setUniform(gl, emitLoc("u_tailDim"), e.tailDim);
|
|
1034
1672
|
web.setUniform(gl, emitLoc("u_intensity"), e.intensity);
|
|
1035
1673
|
web.setUniform(gl, emitLoc("u_seed"), e.seed);
|
|
1036
|
-
web.setUniform(gl, emitLoc("u_channelMask"), e.channel);
|
|
1037
1674
|
web.drawFullscreenTriangle(gl);
|
|
1038
1675
|
}
|
|
1039
1676
|
gl.disable(gl.BLEND);
|
|
1040
1677
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1041
1678
|
}
|
|
1679
|
+
function stepTrackLayer(dt, emissions) {
|
|
1680
|
+
liveTracks = trackLayer.step(dt, emissions);
|
|
1681
|
+
}
|
|
1042
1682
|
function composite(contentWidth, contentHeight, params = {}) {
|
|
1043
|
-
if (!vapor || !condensate || !track) return;
|
|
1683
|
+
if (!vapor || !condensate || !track || !shadow) return;
|
|
1044
1684
|
const p = { ...MEDIUM_DEFAULTS, ...params };
|
|
1045
1685
|
gl.viewport(0, 0, contentWidth, contentHeight);
|
|
1046
1686
|
gl.disable(gl.BLEND);
|
|
@@ -1048,9 +1688,11 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
1048
1688
|
web.bindTextureAt(gl, 0, vapor[front].texture, compositeProgram, "u_vapor");
|
|
1049
1689
|
web.bindTextureAt(gl, 1, condensate[front].texture, compositeProgram, "u_condensate");
|
|
1050
1690
|
web.bindTextureAt(gl, 2, track[front].texture, compositeProgram, "u_track");
|
|
1691
|
+
web.bindTextureAt(gl, 3, shadow.texture, compositeProgram, "u_shadow");
|
|
1051
1692
|
web.setUniform(gl, compositeLoc("u_vaporSize"), [gridW, gridH]);
|
|
1052
1693
|
web.setUniform(gl, compositeLoc("u_condensateSize"), [gridW, gridH]);
|
|
1053
1694
|
web.setUniform(gl, compositeLoc("u_trackSize"), [gridW, gridH]);
|
|
1695
|
+
web.setUniform(gl, compositeLoc("u_shadowSize"), [gridW, gridH]);
|
|
1054
1696
|
web.setUniform(gl, compositeLoc("u_resolution"), [contentWidth, contentHeight]);
|
|
1055
1697
|
web.setUniform(gl, compositeLoc("u_dyeScale"), [gridW / contentWidth, gridH / contentHeight]);
|
|
1056
1698
|
web.setUniform(gl, compositeLoc("u_lab0"), srgbToOklab(p.channelColors[0]));
|
|
@@ -1067,7 +1709,12 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
1067
1709
|
web.setUniform(gl, compositeLoc("u_farWeight"), p.depthFarWeight);
|
|
1068
1710
|
web.setUniform(gl, compositeLoc("u_gravityBoost"), p.gravityBottomBoost);
|
|
1069
1711
|
web.setUniform(gl, compositeLoc("u_gravityBoostStart"), p.gravityBottomBoostStart);
|
|
1712
|
+
web.setUniform(gl, compositeLoc("u_channelScatter"), p.channelScatter);
|
|
1713
|
+
for (let slot = 0; slot < MEDIUM_MAX_LIGHTS; slot += 1) bindLightUniforms(slot, p.lights[slot]);
|
|
1714
|
+
web.setUniform(gl, compositeLoc("u_trackLightFloor"), p.trackLightFloor);
|
|
1070
1715
|
web.drawFullscreenTriangle(gl);
|
|
1716
|
+
const { data, count } = buildTrackLayerInstances(liveTracks, contentWidth, contentHeight, p.trackLayerAmount);
|
|
1717
|
+
trackLayerProgram.draw(contentWidth, contentHeight, data, count, p.trackLightFloor, p.lights, shadow.texture);
|
|
1071
1718
|
}
|
|
1072
1719
|
function readTarget(target) {
|
|
1073
1720
|
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
|
|
@@ -1134,6 +1781,23 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
1134
1781
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1135
1782
|
return { cols: gridW, rows: gridH, data };
|
|
1136
1783
|
}
|
|
1784
|
+
function readTrackGrid() {
|
|
1785
|
+
if (!track) return null;
|
|
1786
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, track[front].fbo);
|
|
1787
|
+
const cells = gridW * gridH;
|
|
1788
|
+
const data = new Array(cells);
|
|
1789
|
+
if (format.type === gl.HALF_FLOAT) {
|
|
1790
|
+
const buf = new Float32Array(cells * 4);
|
|
1791
|
+
gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
|
|
1792
|
+
for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4];
|
|
1793
|
+
} else {
|
|
1794
|
+
const buf = new Uint8Array(cells * 4);
|
|
1795
|
+
gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
|
|
1796
|
+
for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4] / 255;
|
|
1797
|
+
}
|
|
1798
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1799
|
+
return { cols: gridW, rows: gridH, data };
|
|
1800
|
+
}
|
|
1137
1801
|
function destroy() {
|
|
1138
1802
|
destroyTargets();
|
|
1139
1803
|
gl.deleteProgram(vaporForwardProgram);
|
|
@@ -1146,9 +1810,22 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
1146
1810
|
gl.deleteProgram(compositeProgram);
|
|
1147
1811
|
gl.deleteProgram(seedProgram);
|
|
1148
1812
|
gl.deleteProgram(emitProgram);
|
|
1813
|
+
gl.deleteProgram(shadowProgram);
|
|
1149
1814
|
gl.deleteProgram(resampleProgram);
|
|
1815
|
+
trackLayerProgram.destroy();
|
|
1150
1816
|
}
|
|
1151
|
-
return {
|
|
1817
|
+
return {
|
|
1818
|
+
ensureGrid,
|
|
1819
|
+
step,
|
|
1820
|
+
emit,
|
|
1821
|
+
stepTrackLayer,
|
|
1822
|
+
composite,
|
|
1823
|
+
readTotals,
|
|
1824
|
+
readVaporGrid,
|
|
1825
|
+
readCondensateGrid,
|
|
1826
|
+
readTrackGrid,
|
|
1827
|
+
destroy
|
|
1828
|
+
};
|
|
1152
1829
|
}
|
|
1153
1830
|
function createMediumBackdrop() {
|
|
1154
1831
|
let runtime = null;
|
|
@@ -1161,12 +1838,14 @@ function createMediumBackdrop() {
|
|
|
1161
1838
|
runtime.ensureGrid(frame.gridWidth, frame.gridHeight);
|
|
1162
1839
|
runtime.step(frame.dt, frame.params);
|
|
1163
1840
|
runtime.emit(frame.emissions ?? []);
|
|
1841
|
+
runtime.stepTrackLayer(frame.dt, frame.emissions ?? []);
|
|
1164
1842
|
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
1165
1843
|
runtime.composite(target.width, target.height, frame.params);
|
|
1166
1844
|
},
|
|
1167
1845
|
readTotals: () => runtime?.readTotals() ?? null,
|
|
1168
1846
|
readVaporGrid: () => runtime?.readVaporGrid() ?? null,
|
|
1169
1847
|
readCondensateGrid: () => runtime?.readCondensateGrid() ?? null,
|
|
1848
|
+
readTrackGrid: () => runtime?.readTrackGrid() ?? null,
|
|
1170
1849
|
destroy: () => {
|
|
1171
1850
|
runtime?.destroy();
|
|
1172
1851
|
runtime = null;
|
|
@@ -1175,7 +1854,12 @@ function createMediumBackdrop() {
|
|
|
1175
1854
|
};
|
|
1176
1855
|
}
|
|
1177
1856
|
|
|
1857
|
+
exports.buildTrackLayerInstances = buildTrackLayerInstances;
|
|
1178
1858
|
exports.createMediumBackdrop = createMediumBackdrop;
|
|
1179
1859
|
exports.createMediumRuntime = createMediumRuntime;
|
|
1860
|
+
exports.createTrackLayer = createTrackLayer;
|
|
1861
|
+
exports.createTrackLayerProgram = createTrackLayerProgram;
|
|
1862
|
+
exports.trackLayerFade = trackLayerFade;
|
|
1863
|
+
exports.trackLayerSigmaMul = trackLayerSigmaMul;
|
|
1180
1864
|
//# sourceMappingURL=index.cjs.map
|
|
1181
1865
|
//# sourceMappingURL=index.cjs.map
|