vireuikit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -285,6 +285,7 @@ var MEDIUM_DEFAULTS = {
285
285
  condensationFloor: 0.12,
286
286
  evaporationRate: 0.2,
287
287
  condensateSettleSpeed: 8,
288
+ condensateSettleWiggle: 0.4,
288
289
  condensateSpreadRate: 0.08,
289
290
  condensateTint: 0.2,
290
291
  condensateGain: 5,
@@ -484,6 +485,71 @@ float2 vgCurlVelocity(float2 p, float phase) {
484
485
  float py0 = vgPotential(p - float2(0.0, e), phase);
485
486
  return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
486
487
  }
488
+
489
+ // vgPotential above is periodic in TIME only: the simulation domain is a torus (gl.REPEAT), but
490
+ // the lattice itself never wraps in x/y, so velocity jumps at the domain seam and dye on either
491
+ // side of it is transported differently \u2014 a straight seam, made worse wherever something re-reads
492
+ // the field at a different scale (the far depth plane's tile boundary). vgValueNoise3Seamless
493
+ // wraps ALL THREE lattice axes: xyPeriod/zPeriod fold BOTH neighbors of a cell into their own
494
+ // period before hashing, so the field is EXACTLY the same one period over, in space exactly as it
495
+ // already was in time.
496
+ //
497
+ // The period has to be an integer number of lattice cells, or the wrap would jump instead of
498
+ // closing seamlessly (the same requirement MEDIUM_TIME_OCTAVES/mediumOctavePeriod enforces for
499
+ // time) \u2014 but unlike time, the spatial period depends on the GRID SIZE, which this shader has no
500
+ // notion of. So xyPeriod travels together with a per-octave spatial frequency the CPU computes to
501
+ // make gridSize * frequency land on that exact integer (see computeMediumSpatialPeriods below);
502
+ // both arrive together as one uniform per octave (u_spatialN.xy = frequency, .zw = period),
503
+ // recomputed by web/medium.ts whenever the grid resizes.
504
+ float vgValueNoise3Seamless(float3 p, float2 xyPeriod, float zPeriod) {
505
+ float3 i = floor(p);
506
+ float3 f = fract(p);
507
+ float3 u = f * f * (3.0 - 2.0 * f);
508
+ float x0 = mod(i.x, xyPeriod.x);
509
+ float x1 = mod(i.x + 1.0, xyPeriod.x);
510
+ float y0 = mod(i.y, xyPeriod.y);
511
+ float y1 = mod(i.y + 1.0, xyPeriod.y);
512
+ float z0 = mod(i.z, zPeriod);
513
+ float z1 = mod(i.z + 1.0, zPeriod);
514
+ float n000 = vgHash31(float3(x0, y0, z0));
515
+ float n100 = vgHash31(float3(x1, y0, z0));
516
+ float n010 = vgHash31(float3(x0, y1, z0));
517
+ float n110 = vgHash31(float3(x1, y1, z0));
518
+ float n001 = vgHash31(float3(x0, y0, z1));
519
+ float n101 = vgHash31(float3(x1, y0, z1));
520
+ float n011 = vgHash31(float3(x0, y1, z1));
521
+ float n111 = vgHash31(float3(x1, y1, z1));
522
+ float nx00 = mix(n000, n100, u.x);
523
+ float nx10 = mix(n010, n110, u.x);
524
+ float nx01 = mix(n001, n101, u.x);
525
+ float nx11 = mix(n011, n111, u.x);
526
+ float nxy0 = mix(nx00, nx10, u.y);
527
+ float nxy1 = mix(nx01, nx11, u.y);
528
+ return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
529
+ }
530
+
531
+ // Same four-octave fbm as vgPotential, but every octave reads the seamless lattice above at its
532
+ // OWN (frequency, period) pair \u2014 sN.xy replaces that octave's compile-time \`scale\` constant (it
533
+ // has to be a uniform now: the adjustment depends on the live grid size), sN.zw is the matching
534
+ // integer period. \`p\` is still curlFreq-scaled exactly as vgPotential's caller scales it \u2014 only
535
+ // the per-octave multiplier changed from a constant to a grid-dependent uniform, so the finite
536
+ // difference in vgCurlVelocitySeamless below needs no epsilon change of its own.
537
+ float vgPotentialSeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
538
+ float v = vgValueNoise3Seamless(float3(p * s0.xy, phase * ${VG_OCT0.rate} + ${VG_OCT0.offset}), s0.zw, ${VG_OCT0.period}) * 0.5;
539
+ v += vgValueNoise3Seamless(float3(p * s1.xy, phase * ${VG_OCT1.rate} + ${VG_OCT1.offset}), s1.zw, ${VG_OCT1.period}) * 0.25;
540
+ v += vgValueNoise3Seamless(float3(p * s2.xy, phase * ${VG_OCT2.rate} + ${VG_OCT2.offset}), s2.zw, ${VG_OCT2.period}) * 0.125;
541
+ float macro = vgValueNoise3Seamless(float3(p * s3.xy, phase * ${VG_OCT3.rate} + ${VG_OCT3.offset}), s3.zw, ${VG_OCT3.period});
542
+ return v * (0.7 + 0.3 * macro);
543
+ }
544
+
545
+ float2 vgCurlVelocitySeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
546
+ float e = 0.6;
547
+ float px1 = vgPotentialSeamless(p + float2(e, 0.0), phase, s0, s1, s2, s3);
548
+ float px0 = vgPotentialSeamless(p - float2(e, 0.0), phase, s0, s1, s2, s3);
549
+ float py1 = vgPotentialSeamless(p + float2(0.0, e), phase, s0, s1, s2, s3);
550
+ float py0 = vgPotentialSeamless(p - float2(0.0, e), phase, s0, s1, s2, s3);
551
+ return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
552
+ }
487
553
  `;
488
554
 
489
555
  // src/medium/seed-shader.ts
@@ -527,15 +593,25 @@ uniform float u_phase;
527
593
  uniform float u_curlFreq;
528
594
  uniform float u_advectSpeed;
529
595
  uniform float u_turbulence;
596
+ // One float4 per MEDIUM_TIME_OCTAVES entry: xy is that octave's seamless spatial frequency, zw its
597
+ // matching integer lattice period \u2014 both grid-size-dependent, so they can't be compile-time
598
+ // constants (see vgPotentialSeamless/computeMediumSpatialPeriods in noise.ts). Recomputed by
599
+ // web/medium.ts's bindVelocityUniforms every frame from the live grid dimensions.
600
+ uniform float4 u_spatial0;
601
+ uniform float4 u_spatial1;
602
+ uniform float4 u_spatial2;
603
+ uniform float4 u_spatial3;
530
604
 
531
605
  ${VG_CURL_NOISE}
532
606
  `;
533
607
  var MEDIUM_ADVECT_VELOCITY = `
534
- float2 vel = vgCurlVelocity(xy * u_curlFreq, u_phase) * u_advectSpeed * u_turbulence;
608
+ float2 curl = vgCurlVelocitySeamless(xy * u_curlFreq, u_phase, u_spatial0, u_spatial1, u_spatial2, u_spatial3);
609
+ float2 vel = curl * u_advectSpeed * u_turbulence;
535
610
  `;
536
611
  var MEDIUM_ADVECT_VELOCITY_SETTLE = `
537
612
  ${MEDIUM_ADVECT_VELOCITY}
538
613
  vel.y += u_settleSpeed;
614
+ vel.x += curl.x * u_advectSpeed * u_settleWiggle;
539
615
  `;
540
616
  var MEDIUM_ADVECT_VELOCITY_VAPOR = `
541
617
  ${MEDIUM_ADVECT_VELOCITY}
@@ -554,6 +630,7 @@ ${MEDIUM_ADVECT_VELOCITY_VAPOR}
554
630
  var MEDIUM_CONDENSATE_FORWARD_SHADER = `
555
631
  ${MEDIUM_ADVECT_UNIFORMS}
556
632
  uniform float u_settleSpeed;
633
+ uniform float u_settleWiggle;
557
634
 
558
635
  half4 main(float2 xy) {
559
636
  ${MEDIUM_ADVECT_VELOCITY_SETTLE}
@@ -593,6 +670,7 @@ ${MEDIUM_MACCORMACK_CORRECT}
593
670
  var MEDIUM_CONDENSATE_CORRECT_SHADER = `
594
671
  ${MEDIUM_ADVECT_UNIFORMS}
595
672
  uniform float u_settleSpeed;
673
+ uniform float u_settleWiggle;
596
674
  uniform shader u_forward;
597
675
 
598
676
  half4 main(float2 xy) {
@@ -692,6 +770,39 @@ ${MEDIUM_ADVECT_VELOCITY}
692
770
  `;
693
771
 
694
772
  // src/medium/composite-shader.ts
773
+ var VG_CUBIC_WEIGHTS = `
774
+ float4 vgCubicWeights(float v) {
775
+ float4 n = float4(1.0, 2.0, 3.0, 4.0) - v;
776
+ float4 s = n * n * n;
777
+ float x = s.x;
778
+ float y = s.y - 4.0 * s.x;
779
+ float z = s.z - 4.0 * s.y + 6.0 * s.x;
780
+ float w = 6.0 - x - y - z;
781
+ return float4(x, y, z, w) * (1.0 / 6.0);
782
+ }
783
+ `;
784
+ function vgBicubicSampler(fnName, textureName) {
785
+ return `
786
+ half4 ${fnName}(float2 px) {
787
+ float2 shifted = px - 0.5;
788
+ float2 fxy = fract(shifted);
789
+ float2 base = shifted - fxy;
790
+ float4 xw = vgCubicWeights(fxy.x);
791
+ float4 yw = vgCubicWeights(fxy.y);
792
+ float2 sx = float2(xw.x + xw.y, xw.z + xw.w);
793
+ float2 sy = float2(yw.x + yw.y, yw.z + yw.w);
794
+ float2 offsetX = float2(base.x - 0.5 + xw.y / sx.x, base.x + 1.5 + xw.w / sx.y);
795
+ float2 offsetY = float2(base.y - 0.5 + yw.y / sy.x, base.y + 1.5 + yw.w / sy.y);
796
+ half4 s00 = ${textureName}.eval(float2(offsetX.x, offsetY.x));
797
+ half4 s10 = ${textureName}.eval(float2(offsetX.y, offsetY.x));
798
+ half4 s01 = ${textureName}.eval(float2(offsetX.x, offsetY.y));
799
+ half4 s11 = ${textureName}.eval(float2(offsetX.y, offsetY.y));
800
+ float wx = sx.x / (sx.x + sx.y);
801
+ float wy = sy.x / (sy.x + sy.y);
802
+ return mix(mix(s11, s01, wx), mix(s10, s00, wx), wy);
803
+ }
804
+ `;
805
+ }
695
806
  var MEDIUM_COMPOSITE_SHADER = `
696
807
  uniform shader u_vapor;
697
808
  uniform shader u_condensate;
@@ -713,25 +824,44 @@ uniform float u_gravityBoost;
713
824
  uniform float u_gravityBoostStart;
714
825
 
715
826
  ${VG_OKLAB_TO_SRGB}
827
+ ${VG_CUBIC_WEIGHTS}
828
+ ${vgBicubicSampler("vgBicubicVapor", "u_vapor")}
829
+ ${vgBicubicSampler("vgBicubicCondensate", "u_condensate")}
716
830
 
717
831
  half4 main(float2 xy) {
718
832
  float2 baseSrc = xy * u_dyeScale;
719
833
 
720
- // Near plane \u2014 sampled exactly as before the depth work; the calibrated look lives here.
721
- half4 vapor = u_vapor.eval(baseSrc);
722
- half4 condensate = u_condensate.eval(baseSrc);
834
+ // Near plane \u2014 the calibrated look lives here. Bicubic (see the file header) on vapor and
835
+ // condensate; track stays a plain bilinear read.
836
+ half4 vapor = vgBicubicVapor(baseSrc);
837
+ half4 condensate = vgBicubicCondensate(baseSrc);
723
838
  half4 track = u_track.eval(baseSrc);
724
839
 
725
- // Far plane \u2014 the identical field, scaled and offset (see the file header). A 4-tap box blur
726
- // (no separate center sample, the cheapest isotropic blur available) supplies aerial perspective's
727
- // blur and, as a side effect of averaging, its lower contrast too.
840
+ // Far plane \u2014 the identical field, scaled and offset (see the file header). An 8-tap ring, not
841
+ // the near plane's \xB1x/\xB1y box: axis-aligned taps read the SAME column/row on both sides of any
842
+ // residual axis-aligned structure (a seam, a resize artifact), which is exactly how a straight
843
+ // line gets doubled into four parallel copies instead of softened away \u2014 measured directly via
844
+ // check-medium.mjs's isotropy gate (see its report). The ring is rotated a half-step (22.5\xB0) off
845
+ // the axes so no tap direction ever coincides with a grid row or column. Radius stays in the same
846
+ // far-plane texture-space unit as before (u_farBlurRadius, grid-px) \u2014 only the tap PATTERN
847
+ // changed, not what it measures in.
728
848
  float2 farSrc = baseSrc * u_farScale + u_farOffset;
729
- float2 blurX = float2(u_farBlurRadius, 0.0);
730
- float2 blurY = float2(0.0, u_farBlurRadius);
731
- half4 vaporFar = (u_vapor.eval(farSrc + blurX) + u_vapor.eval(farSrc - blurX) +
732
- u_vapor.eval(farSrc + blurY) + u_vapor.eval(farSrc - blurY)) * half4(0.25);
733
- half4 condensateFar = (u_condensate.eval(farSrc + blurX) + u_condensate.eval(farSrc - blurX) +
734
- u_condensate.eval(farSrc + blurY) + u_condensate.eval(farSrc - blurY)) * half4(0.25);
849
+ float2 ring0 = float2( 0.9238795, 0.3826834) * u_farBlurRadius;
850
+ float2 ring1 = float2( 0.3826834, 0.9238795) * u_farBlurRadius;
851
+ float2 ring2 = float2(-0.3826834, 0.9238795) * u_farBlurRadius;
852
+ float2 ring3 = float2(-0.9238795, 0.3826834) * u_farBlurRadius;
853
+ float2 ring4 = float2(-0.9238795, -0.3826834) * u_farBlurRadius;
854
+ float2 ring5 = float2(-0.3826834, -0.9238795) * u_farBlurRadius;
855
+ float2 ring6 = float2( 0.3826834, -0.9238795) * u_farBlurRadius;
856
+ float2 ring7 = float2( 0.9238795, -0.3826834) * u_farBlurRadius;
857
+ half4 vaporFar = (u_vapor.eval(farSrc + ring0) + u_vapor.eval(farSrc + ring1) +
858
+ u_vapor.eval(farSrc + ring2) + u_vapor.eval(farSrc + ring3) +
859
+ u_vapor.eval(farSrc + ring4) + u_vapor.eval(farSrc + ring5) +
860
+ u_vapor.eval(farSrc + ring6) + u_vapor.eval(farSrc + ring7)) * half4(0.125);
861
+ half4 condensateFar = (u_condensate.eval(farSrc + ring0) + u_condensate.eval(farSrc + ring1) +
862
+ u_condensate.eval(farSrc + ring2) + u_condensate.eval(farSrc + ring3) +
863
+ u_condensate.eval(farSrc + ring4) + u_condensate.eval(farSrc + ring5) +
864
+ u_condensate.eval(farSrc + ring6) + u_condensate.eval(farSrc + ring7)) * half4(0.125);
735
865
 
736
866
  // Gravity's "denser layer at the bottom of the chamber" \u2014 a compositing-only boost (see
737
867
  // MEDIUM_DEFAULTS.gravityBottomBoost): it scales how density already in the conserved buffers is