vireuikit 0.4.0 → 0.5.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.
@@ -1,1865 +0,0 @@
1
- 'use strict';
2
-
3
- var vireglass = require('vireglass');
4
- var web = require('vireglass/web');
5
-
6
- // src/medium/noise.ts
7
- var VG_VALUE_NOISE = `
8
- float vgHash21(float2 p) {
9
- float h = dot(p, float2(127.1, 311.7));
10
- return fract(sin(h) * 43758.5453123);
11
- }
12
-
13
- float vgValueNoise(float2 p) {
14
- float2 i = floor(p);
15
- float2 f = fract(p);
16
- float a = vgHash21(i);
17
- float b = vgHash21(i + float2(1.0, 0.0));
18
- float c = vgHash21(i + float2(0.0, 1.0));
19
- float d = vgHash21(i + float2(1.0, 1.0));
20
- float2 u = f * f * (3.0 - 2.0 * f);
21
- return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 2.0 - 1.0;
22
- }
23
- `;
24
- var glslFloat = (v) => Number.isInteger(v) ? v.toFixed(1) : String(v);
25
- var MEDIUM_TIME_PERIOD = 1e3;
26
- var MEDIUM_TIME_OCTAVES = [
27
- { rate: 0.6, offset: 0, scale: 1 },
28
- { rate: 1.7, offset: 11, scale: 2.03 },
29
- { rate: 0.9, offset: 37, scale: 4.11 },
30
- { rate: 0.15, offset: 5, scale: 0.35 }
31
- ];
32
- function mediumOctavePeriod(rate) {
33
- const period = rate * MEDIUM_TIME_PERIOD;
34
- if (!Number.isInteger(period)) {
35
- throw new Error(
36
- `medium noise: octave rate ${rate} * MEDIUM_TIME_PERIOD ${MEDIUM_TIME_PERIOD} = ${period}, not an integer \u2014 the phase wrap would not be seamless`
37
- );
38
- }
39
- return period;
40
- }
41
- var glslOctave = (o) => ({
42
- rate: glslFloat(o.rate),
43
- offset: glslFloat(o.offset),
44
- scale: glslFloat(o.scale),
45
- period: glslFloat(mediumOctavePeriod(o.rate))
46
- });
47
- var [VG_OCT0, VG_OCT1, VG_OCT2, VG_OCT3] = MEDIUM_TIME_OCTAVES.map(glslOctave);
48
- var VG_CURL_NOISE = `
49
- ${VG_VALUE_NOISE}
50
-
51
- float vgHash31(float3 p) {
52
- float h = dot(p, float3(127.1, 311.7, 74.7));
53
- return fract(sin(h) * 43758.5453123);
54
- }
55
-
56
- // 3D value-noise: time is a third grid axis, not a coordinate shift. A shift would just SLIDE the
57
- // pattern (a "traveling" look, when motion is meant to read as random), while trilinear
58
- // interpolation over t CHANGES the pattern itself between grid nodes \u2014 nearby moments look alike,
59
- // distant ones don't, and no repeat ever shows up.
60
- float vgValueNoise3(float3 p) {
61
- float3 i = floor(p);
62
- float3 f = fract(p);
63
- float3 u = f * f * (3.0 - 2.0 * f);
64
- float n000 = vgHash31(i);
65
- float n100 = vgHash31(i + float3(1.0, 0.0, 0.0));
66
- float n010 = vgHash31(i + float3(0.0, 1.0, 0.0));
67
- float n110 = vgHash31(i + float3(1.0, 1.0, 0.0));
68
- float n001 = vgHash31(i + float3(0.0, 0.0, 1.0));
69
- float n101 = vgHash31(i + float3(1.0, 0.0, 1.0));
70
- float n011 = vgHash31(i + float3(0.0, 1.0, 1.0));
71
- float n111 = vgHash31(i + float3(1.0, 1.0, 1.0));
72
- float nx00 = mix(n000, n100, u.x);
73
- float nx10 = mix(n010, n110, u.x);
74
- float nx01 = mix(n001, n101, u.x);
75
- float nx11 = mix(n011, n111, u.x);
76
- float nxy0 = mix(nx00, nx10, u.y);
77
- float nxy1 = mix(nx01, nx11, u.y);
78
- return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
79
- }
80
-
81
- // Same as vgValueNoise3, but the z axis wraps at \`period\`: BOTH z-lattice neighbors (i.z and
82
- // i.z + 1.0) are folded into [0, period) before hashing, so this is EXACTLY the same function of
83
- // z and of z + period \u2014 not just close. vgPotential below calls this once per octave, each with
84
- // the period matching its own rate (MEDIUM_TIME_OCTAVES), so a phase that wraps at
85
- // MEDIUM_TIME_PERIOD reproduces the identical field instead of jumping to a new one.
86
- float vgValueNoise3Periodic(float3 p, float period) {
87
- float3 i = floor(p);
88
- float3 f = fract(p);
89
- float3 u = f * f * (3.0 - 2.0 * f);
90
- float z0 = mod(i.z, period);
91
- float z1 = mod(i.z + 1.0, period);
92
- float n000 = vgHash31(float3(i.xy, z0));
93
- float n100 = vgHash31(float3(i.xy + float2(1.0, 0.0), z0));
94
- float n010 = vgHash31(float3(i.xy + float2(0.0, 1.0), z0));
95
- float n110 = vgHash31(float3(i.xy + float2(1.0, 1.0), z0));
96
- float n001 = vgHash31(float3(i.xy, z1));
97
- float n101 = vgHash31(float3(i.xy + float2(1.0, 0.0), z1));
98
- float n011 = vgHash31(float3(i.xy + float2(0.0, 1.0), z1));
99
- float n111 = vgHash31(float3(i.xy + float2(1.0, 1.0), z1));
100
- float nx00 = mix(n000, n100, u.x);
101
- float nx10 = mix(n010, n110, u.x);
102
- float nx01 = mix(n001, n101, u.x);
103
- float nx11 = mix(n011, n111, u.x);
104
- float nxy0 = mix(nx00, nx10, u.y);
105
- float nxy1 = mix(nx01, nx11, u.y);
106
- return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
107
- }
108
-
109
- // The potential is an fbm of three octaves, each evolving over the field's PHASE at its own speed
110
- // (unrelated to the others, plus a phase offset): no single phase shift can align them all at
111
- // once, so no traveling pattern remains. Each octave wraps its own z-lattice at the period that
112
- // matches its rate (see MEDIUM_TIME_OCTAVES/vgValueNoise3Periodic), so
113
- // vgPotential(p, phase) === vgPotential(p, phase + ${glslFloat(MEDIUM_TIME_PERIOD)}) exactly \u2014 a
114
- // CPU-side phase accumulator can wrap at that period with no visible jump (see web/medium.ts). The
115
- // large-scale factor is a slow non-uniformity layered over the sum \u2014 without it, a field that's
116
- // uniform by statistics still reads as a regular cell pattern.
117
- float vgPotential(float2 p, float phase) {
118
- float v = vgValueNoise3Periodic(float3(p * ${VG_OCT0.scale}, phase * ${VG_OCT0.rate} + ${VG_OCT0.offset}), ${VG_OCT0.period}) * 0.5;
119
- v += vgValueNoise3Periodic(float3(p * ${VG_OCT1.scale}, phase * ${VG_OCT1.rate} + ${VG_OCT1.offset}), ${VG_OCT1.period}) * 0.25;
120
- v += vgValueNoise3Periodic(float3(p * ${VG_OCT2.scale}, phase * ${VG_OCT2.rate} + ${VG_OCT2.offset}), ${VG_OCT2.period}) * 0.125;
121
- float macro = vgValueNoise3Periodic(float3(p * ${VG_OCT3.scale}, phase * ${VG_OCT3.rate} + ${VG_OCT3.offset}), ${VG_OCT3.period});
122
- return v * (0.7 + 0.3 * macro);
123
- }
124
-
125
- // Velocity = (dPsi/dy, -dPsi/dx) \u2014 the curl of a scalar field, solenoidal for any Psi. Central
126
- // differences instead of an analytic derivative: the potential is a sum of three value-noise
127
- // calls, and there is no cheaper way to differentiate it by hand than four extra samples.
128
- float2 vgCurlVelocity(float2 p, float phase) {
129
- float e = 0.6;
130
- float px1 = vgPotential(p + float2(e, 0.0), phase);
131
- float px0 = vgPotential(p - float2(e, 0.0), phase);
132
- float py1 = vgPotential(p + float2(0.0, e), phase);
133
- float py0 = vgPotential(p - float2(0.0, e), phase);
134
- return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
135
- }
136
-
137
- // vgPotential above is periodic in TIME only: the simulation domain is a torus (gl.REPEAT), but
138
- // the lattice itself never wraps in x/y, so velocity jumps at the domain seam and dye on either
139
- // side of it is transported differently \u2014 a straight seam, made worse wherever something re-reads
140
- // the field at a different scale (the far depth plane's tile boundary). vgValueNoise3Seamless
141
- // wraps ALL THREE lattice axes: xyPeriod/zPeriod fold BOTH neighbors of a cell into their own
142
- // period before hashing, so the field is EXACTLY the same one period over, in space exactly as it
143
- // already was in time.
144
- //
145
- // The period has to be an integer number of lattice cells, or the wrap would jump instead of
146
- // closing seamlessly (the same requirement MEDIUM_TIME_OCTAVES/mediumOctavePeriod enforces for
147
- // time) \u2014 but unlike time, the spatial period depends on the GRID SIZE, which this shader has no
148
- // notion of. So xyPeriod travels together with a per-octave spatial frequency the CPU computes to
149
- // make gridSize * frequency land on that exact integer (see computeMediumSpatialPeriods below);
150
- // both arrive together as one uniform per octave (u_spatialN.xy = frequency, .zw = period),
151
- // recomputed by web/medium.ts whenever the grid resizes.
152
- float vgValueNoise3Seamless(float3 p, float2 xyPeriod, float zPeriod) {
153
- float3 i = floor(p);
154
- float3 f = fract(p);
155
- float3 u = f * f * (3.0 - 2.0 * f);
156
- float x0 = mod(i.x, xyPeriod.x);
157
- float x1 = mod(i.x + 1.0, xyPeriod.x);
158
- float y0 = mod(i.y, xyPeriod.y);
159
- float y1 = mod(i.y + 1.0, xyPeriod.y);
160
- float z0 = mod(i.z, zPeriod);
161
- float z1 = mod(i.z + 1.0, zPeriod);
162
- float n000 = vgHash31(float3(x0, y0, z0));
163
- float n100 = vgHash31(float3(x1, y0, z0));
164
- float n010 = vgHash31(float3(x0, y1, z0));
165
- float n110 = vgHash31(float3(x1, y1, z0));
166
- float n001 = vgHash31(float3(x0, y0, z1));
167
- float n101 = vgHash31(float3(x1, y0, z1));
168
- float n011 = vgHash31(float3(x0, y1, z1));
169
- float n111 = vgHash31(float3(x1, y1, z1));
170
- float nx00 = mix(n000, n100, u.x);
171
- float nx10 = mix(n010, n110, u.x);
172
- float nx01 = mix(n001, n101, u.x);
173
- float nx11 = mix(n011, n111, u.x);
174
- float nxy0 = mix(nx00, nx10, u.y);
175
- float nxy1 = mix(nx01, nx11, u.y);
176
- return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
177
- }
178
-
179
- // Same four-octave fbm as vgPotential, but every octave reads the seamless lattice above at its
180
- // OWN (frequency, period) pair \u2014 sN.xy replaces that octave's compile-time \`scale\` constant (it
181
- // has to be a uniform now: the adjustment depends on the live grid size), sN.zw is the matching
182
- // integer period. \`p\` is still curlFreq-scaled exactly as vgPotential's caller scales it \u2014 only
183
- // the per-octave multiplier changed from a constant to a grid-dependent uniform, so the finite
184
- // difference in vgCurlVelocitySeamless below needs no epsilon change of its own.
185
- float vgPotentialSeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
186
- float v = vgValueNoise3Seamless(float3(p * s0.xy, phase * ${VG_OCT0.rate} + ${VG_OCT0.offset}), s0.zw, ${VG_OCT0.period}) * 0.5;
187
- v += vgValueNoise3Seamless(float3(p * s1.xy, phase * ${VG_OCT1.rate} + ${VG_OCT1.offset}), s1.zw, ${VG_OCT1.period}) * 0.25;
188
- v += vgValueNoise3Seamless(float3(p * s2.xy, phase * ${VG_OCT2.rate} + ${VG_OCT2.offset}), s2.zw, ${VG_OCT2.period}) * 0.125;
189
- float macro = vgValueNoise3Seamless(float3(p * s3.xy, phase * ${VG_OCT3.rate} + ${VG_OCT3.offset}), s3.zw, ${VG_OCT3.period});
190
- return v * (0.7 + 0.3 * macro);
191
- }
192
-
193
- float2 vgCurlVelocitySeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
194
- float e = 0.6;
195
- float px1 = vgPotentialSeamless(p + float2(e, 0.0), phase, s0, s1, s2, s3);
196
- float px0 = vgPotentialSeamless(p - float2(e, 0.0), phase, s0, s1, s2, s3);
197
- float py1 = vgPotentialSeamless(p + float2(0.0, e), phase, s0, s1, s2, s3);
198
- float py0 = vgPotentialSeamless(p - float2(0.0, e), phase, s0, s1, s2, s3);
199
- return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
200
- }
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
- }
247
- var MIN_SPATIAL_PERIOD = 2;
248
- function computeMediumSpatialPeriods(gridWidth, gridHeight, curlFreq) {
249
- const freq = Math.max(curlFreq, 1e-6);
250
- return MEDIUM_TIME_OCTAVES.map((o) => {
251
- const rawX = gridWidth * freq * o.scale;
252
- const rawY = gridHeight * freq * o.scale;
253
- const minRaw = Math.min(rawX, rawY);
254
- const scaleUp = minRaw > 0 && minRaw < MIN_SPATIAL_PERIOD ? MIN_SPATIAL_PERIOD / minRaw : 1;
255
- const periodX = Math.max(MIN_SPATIAL_PERIOD, Math.round(rawX * scaleUp));
256
- const periodY = Math.max(MIN_SPATIAL_PERIOD, Math.round(rawY * scaleUp));
257
- return {
258
- freqX: periodX / (gridWidth * freq),
259
- freqY: periodY / (gridHeight * freq),
260
- periodX,
261
- periodY
262
- };
263
- });
264
- }
265
-
266
- // src/medium/oklab.ts
267
- var LINEAR_TO_LMS = [
268
- [0.4122214708, 0.5363325363, 0.0514459929],
269
- [0.2119034982, 0.6806995451, 0.1073969566],
270
- [0.0883024619, 0.2817188376, 0.6299787005]
271
- ];
272
- var LMS_TO_OKLAB = [
273
- [0.2104542553, 0.793617785, -0.0040720468],
274
- [1.9779984951, -2.428592205, 0.4505937099],
275
- [0.0259040371, 0.7827717662, -0.808675766]
276
- ];
277
- var OKLAB_TO_LMS = [
278
- [1, 0.3963377774, 0.2158037573],
279
- [1, -0.1055613458, -0.0638541728],
280
- [1, -0.0894841775, -1.291485548]
281
- ];
282
- var LMS_TO_LINEAR = [
283
- [4.0767416621, -3.3077115913, 0.2309699292],
284
- [-1.2684380046, 2.6097574011, -0.3413193965],
285
- [-0.0041960863, -0.7034186147, 1.707614701]
286
- ];
287
- var glsl = (v) => Number.isInteger(v) ? v.toFixed(1) : String(v);
288
- var row = (m, i, vars) => m[i].map((k, j) => `${glsl(k)} * ${vars[j]}`).join(" + ");
289
- var VG_OKLAB_TO_SRGB = `
290
- float3 vgLinearToSrgb(float3 c) {
291
- float3 lo = c * 12.92;
292
- float3 hi = 1.055 * pow(max(c, float3(0.0)), float3(1.0 / 2.4)) - 0.055;
293
- return mix(lo, hi, step(float3(0.0031308), c));
294
- }
295
-
296
- float3 vgOklabToLinear(float3 lab) {
297
- float l_ = ${row(OKLAB_TO_LMS, 0, ["lab.x", "lab.y", "lab.z"])};
298
- float m_ = ${row(OKLAB_TO_LMS, 1, ["lab.x", "lab.y", "lab.z"])};
299
- float s_ = ${row(OKLAB_TO_LMS, 2, ["lab.x", "lab.y", "lab.z"])};
300
- float l = l_ * l_ * l_;
301
- float m = m_ * m_ * m_;
302
- float s = s_ * s_ * s_;
303
- return float3(
304
- ${row(LMS_TO_LINEAR, 0, ["l", "m", "s"])},
305
- ${row(LMS_TO_LINEAR, 1, ["l", "m", "s"])},
306
- ${row(LMS_TO_LINEAR, 2, ["l", "m", "s"])}
307
- );
308
- }
309
- `;
310
- var apply = (m, v) => [
311
- m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
312
- m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
313
- m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]
314
- ];
315
- var srgbChannelToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
316
- var linearChannelToSrgb = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * Math.pow(Math.max(c, 0), 1 / 2.4) - 0.055;
317
- function srgbToOklab(rgb) {
318
- const linear = rgb.map(srgbChannelToLinear);
319
- const lms = apply(LINEAR_TO_LMS, linear);
320
- return apply(LMS_TO_OKLAB, lms.map((v) => Math.cbrt(Math.max(v, 0))));
321
- }
322
- function oklabToSrgb(lab) {
323
- const lms = apply(OKLAB_TO_LMS, lab).map((v) => v * v * v);
324
- return apply(LMS_TO_LINEAR, lms).map(linearChannelToSrgb);
325
- }
326
-
327
- // src/medium/species.ts
328
- var MEDIUM_SPECIES_LIGHTNESS = 0.44;
329
- var MEDIUM_SPECIES_HUE_SPREAD = 32;
330
- var clamp01 = (v) => Math.min(1, Math.max(0, v));
331
- function characterOf(color) {
332
- const [lightness, a, b] = srgbToOklab(color);
333
- return {
334
- hue: Math.atan2(b, a) * 180 / Math.PI,
335
- chroma: Math.hypot(a, b),
336
- lightness
337
- };
338
- }
339
- function speciesFamily(character, spreadDeg = MEDIUM_SPECIES_HUE_SPREAD) {
340
- const { hue, chroma, lightness } = character;
341
- const tone = (deg) => {
342
- const rad = deg * Math.PI / 180;
343
- return oklabToSrgb([lightness, chroma * Math.cos(rad), chroma * Math.sin(rad)]).map(
344
- clamp01
345
- );
346
- };
347
- return [tone(hue - spreadDeg), tone(hue), tone(hue + spreadDeg)];
348
- }
349
- function neutralSpecies(baseColor) {
350
- const { hue, chroma } = characterOf(baseColor);
351
- return speciesFamily({ hue, chroma, lightness: MEDIUM_SPECIES_LIGHTNESS });
352
- }
353
-
354
- // src/medium/params.ts
355
- var MEDIUM_MAX_LIGHTS = 3;
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
- ];
388
- var MEDIUM_DEFAULTS = {
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,
395
- curlFreq: 0.045,
396
- curlSpeed: 0.12,
397
- advectSpeed: 26,
398
- turbulence: 1,
399
- condensationRate: 0.18,
400
- condensationFloor: 0.12,
401
- evaporationRate: 0.2,
402
- condensateSettleSpeed: 8,
403
- condensateSettleWiggle: 0.4,
404
- condensateSpreadRate: 0.08,
405
- condensateTint: 0.2,
406
- condensateGain: 5,
407
- condensateColor: [0.94, 0.95, 0.97],
408
- channelColors: neutralSpecies(BASE_COLOR),
409
- channelScatter: [1, 1, 1],
410
- baseColor: BASE_COLOR,
411
- baseWeight: 0.9,
412
- depthFarScale: 1.6,
413
- depthFarOffsetFrac: [0.47, 0.53],
414
- depthFarBlurRadius: 5,
415
- depthFarWeight: 0.5,
416
- gravityVaporDrift: 0.6,
417
- gravityBottomBoost: 2,
418
- gravityBottomBoostStart: 0.75,
419
- lights: MEDIUM_DEFAULT_LIGHTS,
420
- shadowExtinction: 3,
421
- trackLightFloor: 0.8,
422
- trackLayerAmount: 1
423
- };
424
- function depthFarOffsetPx(offsetFrac, gridWidth, gridHeight) {
425
- return [offsetFrac[0] * gridWidth, offsetFrac[1] * gridHeight];
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;
473
-
474
- // src/medium/seed-shader.ts
475
- var MEDIUM_SEED_SHADER = `
476
- uniform float2 u_spot0;
477
- uniform float2 u_spot1;
478
- uniform float2 u_spot2;
479
- uniform float u_spotRadius;
480
-
481
- float vgSpot(float2 p, float2 c, float r) {
482
- float d = length(p - c) / max(r, 1.0);
483
- return exp(-d * d * 2.0);
484
- }
485
-
486
- half4 main(float2 xy) {
487
- float3 dye = float3(
488
- vgSpot(xy, u_spot0, u_spotRadius),
489
- vgSpot(xy, u_spot1, u_spotRadius),
490
- vgSpot(xy, u_spot2, u_spotRadius)
491
- );
492
- return half4(half3(dye), half(1.0));
493
- }
494
- `;
495
-
496
- // src/medium/resample-shader.ts
497
- var MEDIUM_RESAMPLE_SHADER = `
498
- uniform shader u_src;
499
-
500
- half4 main(float2 xy) {
501
- float2 uv = xy / u_resolution;
502
- float2 src = uv * u_srcSize;
503
- return u_src.eval(float2(src.x, u_srcSize.y - src.y));
504
- }
505
- `;
506
-
507
- // src/medium/advect-shader.ts
508
- var MEDIUM_ADVECT_UNIFORMS = `
509
- uniform shader u_dye;
510
- uniform float u_dt;
511
- uniform float u_phase;
512
- uniform float u_curlFreq;
513
- uniform float u_advectSpeed;
514
- uniform float u_turbulence;
515
- // One float4 per MEDIUM_TIME_OCTAVES entry: xy is that octave's seamless spatial frequency, zw its
516
- // matching integer lattice period \u2014 both grid-size-dependent, so they can't be compile-time
517
- // constants (see vgPotentialSeamless/computeMediumSpatialPeriods in noise.ts). Recomputed by
518
- // web/medium.ts's bindVelocityUniforms every frame from the live grid dimensions.
519
- uniform float4 u_spatial0;
520
- uniform float4 u_spatial1;
521
- uniform float4 u_spatial2;
522
- uniform float4 u_spatial3;
523
-
524
- ${VG_CURL_NOISE}
525
- `;
526
- var MEDIUM_ADVECT_VELOCITY = `
527
- float2 curl = vgCurlVelocitySeamless(xy * u_curlFreq, u_phase, u_spatial0, u_spatial1, u_spatial2, u_spatial3);
528
- float2 vel = curl * u_advectSpeed * u_turbulence;
529
- `;
530
- var MEDIUM_ADVECT_VELOCITY_SETTLE = `
531
- ${MEDIUM_ADVECT_VELOCITY}
532
- vel.y += u_settleSpeed;
533
- vel.x += curl.x * u_advectSpeed * u_settleWiggle;
534
- `;
535
- var MEDIUM_ADVECT_VELOCITY_VAPOR = `
536
- ${MEDIUM_ADVECT_VELOCITY}
537
- vel.y += u_vaporDrift;
538
- `;
539
- var MEDIUM_VAPOR_FORWARD_SHADER = `
540
- ${MEDIUM_ADVECT_UNIFORMS}
541
- uniform float u_vaporDrift;
542
-
543
- half4 main(float2 xy) {
544
- ${MEDIUM_ADVECT_VELOCITY_VAPOR}
545
- float2 src = xy - vel * u_dt;
546
- return u_dye.eval(float2(src.x, u_dyeSize.y - src.y));
547
- }
548
- `;
549
- var MEDIUM_CONDENSATE_FORWARD_SHADER = `
550
- ${MEDIUM_ADVECT_UNIFORMS}
551
- uniform float u_settleSpeed;
552
- uniform float u_settleWiggle;
553
-
554
- half4 main(float2 xy) {
555
- ${MEDIUM_ADVECT_VELOCITY_SETTLE}
556
- float2 src = xy - vel * u_dt;
557
- return u_dye.eval(float2(src.x, u_dyeSize.y - src.y));
558
- }
559
- `;
560
- var MEDIUM_MACCORMACK_CORRECT = `
561
- float2 srcForward = xy - vel * u_dt;
562
- float2 srcReverse = xy + vel * u_dt;
563
- half4 forwardSelf = u_forward.eval(float2(xy.x, u_forwardSize.y - xy.y));
564
- half4 forwardReverse = u_forward.eval(float2(srcReverse.x, u_forwardSize.y - srcReverse.y));
565
- half4 prevSelf = u_dye.eval(float2(xy.x, u_dyeSize.y - xy.y));
566
- half4 corrected = forwardSelf + half4(0.5) * (prevSelf - forwardReverse);
567
-
568
- float2 src = float2(srcForward.x, u_dyeSize.y - srcForward.y);
569
- float2 base = floor(src - 0.5);
570
- half4 c00 = u_dye.eval(base + float2(0.5, 0.5));
571
- half4 c10 = u_dye.eval(base + float2(1.5, 0.5));
572
- half4 c01 = u_dye.eval(base + float2(0.5, 1.5));
573
- half4 c11 = u_dye.eval(base + float2(1.5, 1.5));
574
- half4 lo = min(min(c00, c10), min(c01, c11));
575
- half4 hi = max(max(c00, c10), max(c01, c11));
576
- half4 limited = clamp(corrected, lo, hi);
577
- `;
578
- var MEDIUM_VAPOR_CORRECT_SHADER = `
579
- ${MEDIUM_ADVECT_UNIFORMS}
580
- uniform shader u_forward;
581
- uniform float u_vaporDrift;
582
-
583
- half4 main(float2 xy) {
584
- ${MEDIUM_ADVECT_VELOCITY_VAPOR}
585
- ${MEDIUM_MACCORMACK_CORRECT}
586
- return limited;
587
- }
588
- `;
589
- var MEDIUM_CONDENSATE_CORRECT_SHADER = `
590
- ${MEDIUM_ADVECT_UNIFORMS}
591
- uniform float u_settleSpeed;
592
- uniform float u_settleWiggle;
593
- uniform shader u_forward;
594
-
595
- half4 main(float2 xy) {
596
- ${MEDIUM_ADVECT_VELOCITY_SETTLE}
597
- ${MEDIUM_MACCORMACK_CORRECT}
598
- return limited;
599
- }
600
- `;
601
- var MEDIUM_VAPOR_REACT_SHADER = `
602
- uniform shader u_transported;
603
- uniform shader u_condensateTransported;
604
- uniform float u_dt;
605
- uniform float u_condensationRate;
606
- uniform float u_condensationFloor;
607
- uniform float u_evaporationRate;
608
- uniform float u_totalScale;
609
-
610
- half4 main(float2 xy) {
611
- float3 prevVapor = float3(u_transported.eval(float2(xy.x, u_transportedSize.y - xy.y)).rgb);
612
- float condensateAtXy = float(u_condensateTransported.eval(float2(xy.x, u_condensateTransportedSize.y - xy.y)).r);
613
- float vaporTotal = prevVapor.r + prevVapor.g + prevVapor.b;
614
-
615
- // Only the EXCESS of vapor above the background floor condenses, not any nonzero density \u2014
616
- // otherwise condensate would exactly retrace the vapor spot's shape (the same outline, or
617
- // softer from blur), leaving the two phases nowhere to visually diverge: vapor is already soft,
618
- // and condensate needs to read brighter and sharper than it. The floor concentrates condensate
619
- // in dense turbulence cores, leaving the sparse vapor around it uncondensed \u2014 that's where the
620
- // contrast comes from.
621
- float excess = max(0.0, vaporTotal - u_condensationFloor);
622
- float condensedAmount = excess * (1.0 - exp(-u_condensationRate * u_dt));
623
- float lossFraction = vaporTotal > 1e-6 ? condensedAmount / vaporTotal : 0.0;
624
- float3 afterLoss = prevVapor * (1.0 - lossFraction);
625
-
626
- float condKeep = exp(-u_evaporationRate * u_dt);
627
- float returned = condensateAtXy * (1.0 - condKeep);
628
-
629
- // Evaporated water is distributed by the CURRENT vapor proportion \u2014 the condensation/
630
- // evaporation cycle doesn't shift the hue, water only oscillates between phases (color belongs
631
- // to vapor).
632
- float sumAfterLoss = afterLoss.r + afterLoss.g + afterLoss.b;
633
- float3 hueShare = sumAfterLoss > 1e-6 ? afterLoss / sumAfterLoss : float3(1.0 / 3.0);
634
- float3 vapor = (afterLoss + hueShare * returned) * u_totalScale;
635
- return half4(half3(vapor), half(1.0));
636
- }
637
- `;
638
- var MEDIUM_CONDENSATE_REACT_SHADER = `
639
- uniform shader u_transported;
640
- uniform shader u_vaporTransported;
641
- uniform shader u_dye;
642
- uniform float u_dt;
643
- uniform float u_condensationRate;
644
- uniform float u_condensationFloor;
645
- uniform float u_evaporationRate;
646
- uniform float u_spreadRate;
647
- uniform float u_totalScale;
648
-
649
- half4 main(float2 xy) {
650
- float prevCondensate = float(u_transported.eval(float2(xy.x, u_transportedSize.y - xy.y)).r);
651
- float3 vaporAtXy = float3(u_vaporTransported.eval(float2(xy.x, u_vaporTransportedSize.y - xy.y)).rgb);
652
- float vaporTotal = vaporAtXy.r + vaporAtXy.g + vaporAtXy.b;
653
-
654
- float excess = max(0.0, vaporTotal - u_condensationFloor);
655
- float condensed = excess * (1.0 - exp(-u_condensationRate * u_dt));
656
- float condKeep = exp(-u_evaporationRate * u_dt);
657
- float afterReaction = prevCondensate * condKeep + condensed;
658
-
659
- // Growth is an Euler step separate from transport: the neighbors are sampled at THIS cell's own
660
- // xy in the ORIGINAL (pre-transport-this-frame) field u_dye, not the already
661
- // transported/corrected value. On a non-uniform flow, warping compresses the domain in some
662
- // places and stretches it in others, and blurring at a warped point stacks those distortions \u2014
663
- // the error accumulates frame over frame (measured: condensate steadily gained about 5% extra
664
- // water alongside its own mass growth). Right on the grid, with no warp, the same blur is an
665
- // honest averaging kernel that conserves the sum on a periodic domain.
666
- float2 selfSrc = float2(xy.x, u_dyeSize.y - xy.y);
667
- float n0 = float(u_dye.eval(float2(selfSrc.x + 1.0, selfSrc.y)).r);
668
- float n1 = float(u_dye.eval(float2(selfSrc.x - 1.0, selfSrc.y)).r);
669
- float n2 = float(u_dye.eval(float2(selfSrc.x, selfSrc.y - 1.0)).r);
670
- float n3 = float(u_dye.eval(float2(selfSrc.x, selfSrc.y + 1.0)).r);
671
- float neighborAvg = (n0 + n1 + n2 + n3) * 0.25;
672
- float spreadWeight = 1.0 - exp(-u_spreadRate * u_dt);
673
- float grown = mix(afterReaction, neighborAvg, spreadWeight) * u_totalScale;
674
-
675
- return half4(half3(grown, 0.0, 0.0), half(1.0));
676
- }
677
- `;
678
- var MEDIUM_TRACK_ADVECT_SHADER = `
679
- ${MEDIUM_ADVECT_UNIFORMS}
680
- uniform float u_decay;
681
-
682
- half4 main(float2 xy) {
683
- ${MEDIUM_ADVECT_VELOCITY}
684
- float2 back = xy - vel * u_dt;
685
- half4 prev = u_dye.eval(float2(back.x, u_dyeSize.y - back.y));
686
- float3 decayed = float3(prev.rgb) * exp(-u_decay * u_dt);
687
- return half4(half3(decayed), half(1.0));
688
- }
689
- `;
690
-
691
- // src/medium/composite-shader.ts
692
- var VG_CUBIC_WEIGHTS = `
693
- float4 vgCubicWeights(float v) {
694
- float4 n = float4(1.0, 2.0, 3.0, 4.0) - v;
695
- float4 s = n * n * n;
696
- float x = s.x;
697
- float y = s.y - 4.0 * s.x;
698
- float z = s.z - 4.0 * s.y + 6.0 * s.x;
699
- float w = 6.0 - x - y - z;
700
- return float4(x, y, z, w) * (1.0 / 6.0);
701
- }
702
- `;
703
- function vgBicubicSampler(fnName, textureName) {
704
- return `
705
- half4 ${fnName}(float2 px) {
706
- float2 shifted = px - 0.5;
707
- float2 fxy = fract(shifted);
708
- float2 base = shifted - fxy;
709
- float4 xw = vgCubicWeights(fxy.x);
710
- float4 yw = vgCubicWeights(fxy.y);
711
- float2 sx = float2(xw.x + xw.y, xw.z + xw.w);
712
- float2 sy = float2(yw.x + yw.y, yw.z + yw.w);
713
- float2 offsetX = float2(base.x - 0.5 + xw.y / sx.x, base.x + 1.5 + xw.w / sx.y);
714
- float2 offsetY = float2(base.y - 0.5 + yw.y / sy.x, base.y + 1.5 + yw.w / sy.y);
715
- half4 s00 = ${textureName}.eval(float2(offsetX.x, offsetY.x));
716
- half4 s10 = ${textureName}.eval(float2(offsetX.y, offsetY.x));
717
- half4 s01 = ${textureName}.eval(float2(offsetX.x, offsetY.y));
718
- half4 s11 = ${textureName}.eval(float2(offsetX.y, offsetY.y));
719
- float wx = sx.x / (sx.x + sx.y);
720
- float wy = sy.x / (sy.x + sy.y);
721
- return mix(mix(s11, s01, wx), mix(s10, s00, wx), wy);
722
- }
723
- `;
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
- `;
749
- var MEDIUM_COMPOSITE_SHADER = `
750
- uniform shader u_vapor;
751
- uniform shader u_condensate;
752
- uniform shader u_track;
753
- uniform shader u_shadow;
754
- uniform float2 u_dyeScale;
755
- uniform float3 u_lab0;
756
- uniform float3 u_lab1;
757
- uniform float3 u_lab2;
758
- uniform float3 u_labBg;
759
- uniform float3 u_labCondensate;
760
- uniform float u_baseWeight;
761
- uniform float u_condensateTint;
762
- uniform float u_condensateGain;
763
- uniform float u_farScale;
764
- uniform float2 u_farOffset;
765
- uniform float u_farBlurRadius;
766
- uniform float u_farWeight;
767
- uniform float u_gravityBoost;
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;
789
-
790
- ${VG_OKLAB_TO_SRGB}
791
- ${VG_CUBIC_WEIGHTS}
792
- ${vgBicubicSampler("vgBicubicVapor", "u_vapor")}
793
- ${vgBicubicSampler("vgBicubicCondensate", "u_condensate")}
794
- ${VG_VALUE_NOISE}
795
- ${VG_LIGHT}
796
-
797
- half4 main(float2 xy) {
798
- float2 baseSrc = xy * u_dyeScale;
799
-
800
- // Near plane \u2014 the calibrated look lives here. Bicubic (see the file header) on vapor and
801
- // condensate; track stays a plain bilinear read.
802
- half4 vapor = vgBicubicVapor(baseSrc);
803
- half4 condensate = vgBicubicCondensate(baseSrc);
804
- half4 track = u_track.eval(baseSrc);
805
-
806
- // Far plane \u2014 the identical field, scaled and offset (see the file header). An 8-tap ring, not
807
- // the near plane's \xB1x/\xB1y box: axis-aligned taps read the SAME column/row on both sides of any
808
- // residual axis-aligned structure (a seam, a resize artifact), which is exactly how a straight
809
- // line gets doubled into four parallel copies instead of softened away \u2014 measured directly via
810
- // check-medium.mjs's isotropy gate (see its report). The ring is rotated a half-step (22.5\xB0) off
811
- // the axes so no tap direction ever coincides with a grid row or column. Radius stays in the same
812
- // far-plane texture-space unit as before (u_farBlurRadius, grid-px) \u2014 only the tap PATTERN
813
- // changed, not what it measures in.
814
- float2 farSrc = baseSrc * u_farScale + u_farOffset;
815
- float2 ring0 = float2( 0.9238795, 0.3826834) * u_farBlurRadius;
816
- float2 ring1 = float2( 0.3826834, 0.9238795) * u_farBlurRadius;
817
- float2 ring2 = float2(-0.3826834, 0.9238795) * u_farBlurRadius;
818
- float2 ring3 = float2(-0.9238795, 0.3826834) * u_farBlurRadius;
819
- float2 ring4 = float2(-0.9238795, -0.3826834) * u_farBlurRadius;
820
- float2 ring5 = float2(-0.3826834, -0.9238795) * u_farBlurRadius;
821
- float2 ring6 = float2( 0.3826834, -0.9238795) * u_farBlurRadius;
822
- float2 ring7 = float2( 0.9238795, -0.3826834) * u_farBlurRadius;
823
- half4 vaporFar = (u_vapor.eval(farSrc + ring0) + u_vapor.eval(farSrc + ring1) +
824
- u_vapor.eval(farSrc + ring2) + u_vapor.eval(farSrc + ring3) +
825
- u_vapor.eval(farSrc + ring4) + u_vapor.eval(farSrc + ring5) +
826
- u_vapor.eval(farSrc + ring6) + u_vapor.eval(farSrc + ring7)) * half4(0.125);
827
- half4 condensateFar = (u_condensate.eval(farSrc + ring0) + u_condensate.eval(farSrc + ring1) +
828
- u_condensate.eval(farSrc + ring2) + u_condensate.eval(farSrc + ring3) +
829
- u_condensate.eval(farSrc + ring4) + u_condensate.eval(farSrc + ring5) +
830
- u_condensate.eval(farSrc + ring6) + u_condensate.eval(farSrc + ring7)) * half4(0.125);
831
-
832
- // Gravity's "denser layer at the bottom of the chamber" \u2014 a compositing-only boost (see
833
- // MEDIUM_DEFAULTS.gravityBottomBoost): it scales how density already in the conserved buffers is
834
- // DISPLAYED, never the buffers themselves, so it can't threaten water conservation. Shared by
835
- // both planes: the chamber's floor is denser everywhere along its depth, not just up close.
836
- //
837
- // Verified with a headless render, not assumed: ramping this with xy.y directly lit up the TOP
838
- // of the frame, not the bottom (contentTexture's own orientation contract flips y once more
839
- // between this pass and the final scene \u2014 see the file header) \u2014 hence the 1.0 - below.
840
- float bottomFrac = 1.0 - xy.y / u_resolution.y;
841
- float bottomT = smoothstep(u_gravityBoostStart, 1.0, bottomFrac);
842
- float bottomBoost = 1.0 + u_gravityBoost * bottomT;
843
-
844
- float gasR = (float(vapor.r) + float(vaporFar.r) * u_farWeight) * bottomBoost;
845
- float gasG = (float(vapor.g) + float(vaporFar.g) * u_farWeight) * bottomBoost;
846
- float gasB = (float(vapor.b) + float(vaporFar.b) * u_farWeight) * bottomBoost;
847
- float w0 = gasR + float(track.r);
848
- float w1 = gasG + float(track.g);
849
- float w2 = gasB + float(track.b);
850
- float wBg = u_baseWeight;
851
-
852
- // A faint condensate hint from the LOCAL vapor, not its own color (droplets scatter light
853
- // rather than tint it) \u2014 a fraction of the way from white to the vapor tone sampled right here.
854
- float vaporLocal = max(w0 + w1 + w2, 1e-4);
855
- float3 vaporHue = (u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2) / vaporLocal;
856
- float3 condensateLab = mix(u_labCondensate, vaporHue, u_condensateTint);
857
- float wCondensate =
858
- (float(condensate.r) + float(condensateFar.r) * u_farWeight) * u_condensateGain * bottomBoost;
859
-
860
- float total = max(w0 + w1 + w2 + wBg + wCondensate, 1e-4);
861
- float3 mixLab =
862
- (u_labBg * wBg + u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2 + condensateLab * wCondensate) / total;
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);
891
- float3 srgb = vgLinearToSrgb(rgbLinear);
892
- return half4(half3(srgb), half(1.0));
893
- }
894
- `;
895
-
896
- // src/medium/emit-shader.ts
897
- var MEDIUM_EMIT_SHADER = `
898
- uniform float2 u_source;
899
- uniform float2 u_head;
900
- uniform float u_headWidth;
901
- uniform float u_tailWidth;
902
- uniform float u_raggedAmp;
903
- uniform float u_raggedFreq;
904
- uniform float u_settle;
905
- uniform float u_tailDim;
906
- uniform float u_intensity;
907
- uniform float u_seed;
908
-
909
- ${VG_VALUE_NOISE}
910
-
911
- half4 main(float2 xy) {
912
- float2 dir = u_head - u_source;
913
- float len = max(length(dir), 1.0);
914
- float2 side = float2(-dir.y, dir.x) / len;
915
-
916
- float best = 0.0;
917
- const int STEPS = 12;
918
- for (int i = 0; i < STEPS; i++) {
919
- float s = float(i) / float(STEPS - 1);
920
- float tailness = 1.0 - s;
921
- float ragged = u_raggedAmp * tailness * vgValueNoise(float2(s * u_raggedFreq + u_seed, u_seed * 3.7));
922
- float2 p = mix(u_source, u_head, s) + side * ragged + float2(0.0, u_settle * tailness * tailness);
923
- float w = max(mix(u_tailWidth, u_headWidth, s), 0.5);
924
- float amp = mix(u_tailDim, 1.0, s);
925
- float d = length(xy - p);
926
- float v = amp * exp(-(d * d) / (2.0 * w * w));
927
- best = max(best, v);
928
- }
929
-
930
- float3 dye = float3(best * u_intensity);
931
- return half4(half3(dye), half(1.0));
932
- }
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
1331
- function pickDyeFormat(gl) {
1332
- const hasFloat = gl.getExtension("EXT_color_buffer_float") !== null;
1333
- return hasFloat ? { internalFormat: gl.RGBA16F, type: gl.HALF_FLOAT } : { internalFormat: gl.RGBA8, type: gl.UNSIGNED_BYTE };
1334
- }
1335
- function createMediumRuntime(gl, vertexSource) {
1336
- const format = pickDyeFormat(gl);
1337
- const vaporForwardProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_VAPOR_FORWARD_SHADER));
1338
- const vaporCorrectProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_VAPOR_CORRECT_SHADER));
1339
- const vaporReactProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_VAPOR_REACT_SHADER));
1340
- const condensateForwardProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_CONDENSATE_FORWARD_SHADER));
1341
- const condensateCorrectProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_CONDENSATE_CORRECT_SHADER));
1342
- const condensateReactProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_CONDENSATE_REACT_SHADER));
1343
- const trackAdvectProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_TRACK_ADVECT_SHADER));
1344
- const compositeProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_COMPOSITE_SHADER));
1345
- const seedProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_SEED_SHADER));
1346
- const emitProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_EMIT_SHADER));
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 = [];
1352
- const vaporForwardLoc = web.locationCache(gl, vaporForwardProgram);
1353
- const vaporCorrectLoc = web.locationCache(gl, vaporCorrectProgram);
1354
- const vaporReactLoc = web.locationCache(gl, vaporReactProgram);
1355
- const condensateForwardLoc = web.locationCache(gl, condensateForwardProgram);
1356
- const condensateCorrectLoc = web.locationCache(gl, condensateCorrectProgram);
1357
- const condensateReactLoc = web.locationCache(gl, condensateReactProgram);
1358
- const trackAdvectLoc = web.locationCache(gl, trackAdvectProgram);
1359
- const compositeLoc = web.locationCache(gl, compositeProgram);
1360
- const seedLoc = web.locationCache(gl, seedProgram);
1361
- const emitLoc = web.locationCache(gl, emitProgram);
1362
- const resampleLoc = web.locationCache(gl, resampleProgram);
1363
- const shadowLoc = web.locationCache(gl, shadowProgram);
1364
- let gridW = 0;
1365
- let gridH = 0;
1366
- let vapor = null;
1367
- let condensate = null;
1368
- let track = null;
1369
- let vaporForward = null;
1370
- let vaporCorrected = null;
1371
- let condensateForward = null;
1372
- let condensateCorrected = null;
1373
- let shadow = null;
1374
- let front = 0;
1375
- function makeTarget(w, h) {
1376
- const texture = web.createTexture(gl, {
1377
- width: w,
1378
- height: h,
1379
- internalFormat: format.internalFormat,
1380
- type: format.type,
1381
- // REPEAT: a domain with no border — see TextureOptions.wrap. All buffers carry the same
1382
- // field, so the border needs to behave identically for each of them, or vapor would wrap
1383
- // while condensate or a track at the same coordinate cuts off. The MacCormack limiter also
1384
- // reads neighbors across this same border — it needs REPEAT exactly as much as transport does.
1385
- wrap: gl.REPEAT
1386
- });
1387
- const fbo = web.createFramebuffer(gl, texture);
1388
- return { texture, fbo };
1389
- }
1390
- function clearTarget(target, w, h) {
1391
- gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
1392
- gl.viewport(0, 0, w, h);
1393
- gl.clearColor(0, 0, 0, 1);
1394
- gl.clear(gl.COLOR_BUFFER_BIT);
1395
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1396
- }
1397
- function destroyPair(pair) {
1398
- if (!pair) return;
1399
- for (const t of pair) {
1400
- gl.deleteTexture(t.texture);
1401
- gl.deleteFramebuffer(t.fbo);
1402
- }
1403
- }
1404
- function destroyTarget(target) {
1405
- if (!target) return;
1406
- gl.deleteTexture(target.texture);
1407
- gl.deleteFramebuffer(target.fbo);
1408
- }
1409
- function destroyTargets() {
1410
- destroyPair(vapor);
1411
- destroyPair(condensate);
1412
- destroyPair(track);
1413
- destroyTarget(vaporForward);
1414
- destroyTarget(vaporCorrected);
1415
- destroyTarget(condensateForward);
1416
- destroyTarget(condensateCorrected);
1417
- destroyTarget(shadow);
1418
- vapor = null;
1419
- condensate = null;
1420
- track = null;
1421
- vaporForward = null;
1422
- vaporCorrected = null;
1423
- condensateForward = null;
1424
- condensateCorrected = null;
1425
- shadow = null;
1426
- }
1427
- function seed(w, h) {
1428
- if (!vapor || !condensate || !track) return;
1429
- gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[front].fbo);
1430
- gl.viewport(0, 0, w, h);
1431
- gl.disable(gl.BLEND);
1432
- gl.useProgram(seedProgram);
1433
- web.setUniform(gl, seedLoc("u_resolution"), [w, h]);
1434
- web.setUniform(gl, seedLoc("u_spot0"), [w * 0.28, h * 0.64]);
1435
- web.setUniform(gl, seedLoc("u_spot1"), [w * 0.7, h * 0.32]);
1436
- web.setUniform(gl, seedLoc("u_spot2"), [w * 0.48, h * 0.84]);
1437
- web.setUniform(gl, seedLoc("u_spotRadius"), Math.max(w, h) * 0.18);
1438
- web.drawFullscreenTriangle(gl);
1439
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1440
- clearTarget(condensate[0], w, h);
1441
- clearTarget(condensate[1], w, h);
1442
- clearTarget(track[0], w, h);
1443
- clearTarget(track[1], w, h);
1444
- }
1445
- function resample(src, srcW, srcH, dst, dstW, dstH) {
1446
- gl.bindFramebuffer(gl.FRAMEBUFFER, dst.fbo);
1447
- gl.viewport(0, 0, dstW, dstH);
1448
- gl.disable(gl.BLEND);
1449
- gl.useProgram(resampleProgram);
1450
- web.bindTextureAt(gl, 0, src.texture, resampleProgram, "u_src");
1451
- web.setUniform(gl, resampleLoc("u_srcSize"), [srcW, srcH]);
1452
- web.setUniform(gl, resampleLoc("u_resolution"), [dstW, dstH]);
1453
- web.drawFullscreenTriangle(gl);
1454
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1455
- }
1456
- function resizeGrid(w, h) {
1457
- if (!vapor || !condensate || !track) return;
1458
- const oldVapor = vapor;
1459
- const oldCondensate = condensate;
1460
- const oldTrack = track;
1461
- const oldW = gridW;
1462
- const oldH = gridH;
1463
- const newVaporFront = makeTarget(w, h);
1464
- const newCondensateFront = makeTarget(w, h);
1465
- const newTrackFront = makeTarget(w, h);
1466
- resample(oldVapor[front], oldW, oldH, newVaporFront, w, h);
1467
- resample(oldCondensate[front], oldW, oldH, newCondensateFront, w, h);
1468
- resample(oldTrack[front], oldW, oldH, newTrackFront, w, h);
1469
- destroyTargets();
1470
- gridW = w;
1471
- gridH = h;
1472
- vapor = [newVaporFront, makeTarget(w, h)];
1473
- condensate = [newCondensateFront, makeTarget(w, h)];
1474
- track = [newTrackFront, makeTarget(w, h)];
1475
- vaporForward = makeTarget(w, h);
1476
- vaporCorrected = makeTarget(w, h);
1477
- condensateForward = makeTarget(w, h);
1478
- condensateCorrected = makeTarget(w, h);
1479
- shadow = makeTarget(w, h);
1480
- front = 0;
1481
- const areaRatio = w * h / (oldW * oldH);
1482
- waterTargetTotal *= areaRatio;
1483
- }
1484
- function ensureGrid(gridWidth, gridHeight) {
1485
- const w = Math.max(2, Math.round(gridWidth));
1486
- const h = Math.max(2, Math.round(gridHeight));
1487
- if (vapor && condensate && track && w === gridW && h === gridH) return;
1488
- if (!vapor || !condensate || !track) {
1489
- gridW = w;
1490
- gridH = h;
1491
- vapor = [makeTarget(w, h), makeTarget(w, h)];
1492
- condensate = [makeTarget(w, h), makeTarget(w, h)];
1493
- track = [makeTarget(w, h), makeTarget(w, h)];
1494
- vaporForward = makeTarget(w, h);
1495
- vaporCorrected = makeTarget(w, h);
1496
- condensateForward = makeTarget(w, h);
1497
- condensateCorrected = makeTarget(w, h);
1498
- shadow = makeTarget(w, h);
1499
- front = 0;
1500
- seed(w, h);
1501
- waterTargetTotal = readTarget(vapor[front]) + readTarget(condensate[front]);
1502
- waterScale = 1;
1503
- timeSinceRenorm = 0;
1504
- framesSinceRenorm = 0;
1505
- return;
1506
- }
1507
- resizeGrid(w, h);
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
- }
1520
- function bindVelocityUniforms(loc, dt, phase2, p, spatial) {
1521
- web.setUniform(gl, loc("u_dyeSize"), [gridW, gridH]);
1522
- web.setUniform(gl, loc("u_resolution"), [gridW, gridH]);
1523
- web.setUniform(gl, loc("u_dt"), dt);
1524
- web.setUniform(gl, loc("u_phase"), phase2);
1525
- web.setUniform(gl, loc("u_curlFreq"), p.curlFreq);
1526
- web.setUniform(gl, loc("u_advectSpeed"), p.advectSpeed);
1527
- web.setUniform(gl, loc("u_turbulence"), p.turbulence);
1528
- web.setUniform(gl, loc("u_vaporDrift"), p.gravityVaporDrift);
1529
- web.setUniform(gl, loc("u_spatial0"), [spatial[0].freqX, spatial[0].freqY, spatial[0].periodX, spatial[0].periodY]);
1530
- web.setUniform(gl, loc("u_spatial1"), [spatial[1].freqX, spatial[1].freqY, spatial[1].periodX, spatial[1].periodY]);
1531
- web.setUniform(gl, loc("u_spatial2"), [spatial[2].freqX, spatial[2].freqY, spatial[2].periodX, spatial[2].periodY]);
1532
- web.setUniform(gl, loc("u_spatial3"), [spatial[3].freqX, spatial[3].freqY, spatial[3].periodX, spatial[3].periodY]);
1533
- }
1534
- let phase = 0;
1535
- function wrapPhase(value) {
1536
- const wrapped = value % MEDIUM_TIME_PERIOD;
1537
- return wrapped < 0 ? wrapped + MEDIUM_TIME_PERIOD : wrapped;
1538
- }
1539
- const WATER_RENORM_INTERVAL_S = 1;
1540
- let waterTargetTotal = 0;
1541
- let waterScale = 1;
1542
- let timeSinceRenorm = 0;
1543
- let framesSinceRenorm = 0;
1544
- function step(dt, params = {}) {
1545
- if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected || !shadow) {
1546
- return;
1547
- }
1548
- const p = { ...MEDIUM_DEFAULTS, ...params };
1549
- const spatial = computeMediumSpatialPeriods(gridW, gridH, p.curlFreq);
1550
- const clampedDt = Math.min(Math.max(dt, 0), 1 / 15);
1551
- phase = wrapPhase(phase + clampedDt * p.curlSpeed);
1552
- const back = front === 0 ? 1 : 0;
1553
- const gridSize = [gridW, gridH];
1554
- gl.viewport(0, 0, gridW, gridH);
1555
- gl.disable(gl.BLEND);
1556
- gl.bindFramebuffer(gl.FRAMEBUFFER, vaporForward.fbo);
1557
- gl.useProgram(vaporForwardProgram);
1558
- web.bindTextureAt(gl, 0, vapor[front].texture, vaporForwardProgram, "u_dye");
1559
- bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p, spatial);
1560
- web.drawFullscreenTriangle(gl);
1561
- gl.bindFramebuffer(gl.FRAMEBUFFER, condensateForward.fbo);
1562
- gl.useProgram(condensateForwardProgram);
1563
- web.bindTextureAt(gl, 0, condensate[front].texture, condensateForwardProgram, "u_dye");
1564
- bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p, spatial);
1565
- web.setUniform(gl, condensateForwardLoc("u_settleSpeed"), p.condensateSettleSpeed);
1566
- web.setUniform(gl, condensateForwardLoc("u_settleWiggle"), p.condensateSettleWiggle);
1567
- web.drawFullscreenTriangle(gl);
1568
- gl.bindFramebuffer(gl.FRAMEBUFFER, vaporCorrected.fbo);
1569
- gl.useProgram(vaporCorrectProgram);
1570
- web.bindTextureAt(gl, 0, vapor[front].texture, vaporCorrectProgram, "u_dye");
1571
- web.bindTextureAt(gl, 1, vaporForward.texture, vaporCorrectProgram, "u_forward");
1572
- web.setUniform(gl, vaporCorrectLoc("u_forwardSize"), gridSize);
1573
- bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p, spatial);
1574
- web.drawFullscreenTriangle(gl);
1575
- gl.bindFramebuffer(gl.FRAMEBUFFER, condensateCorrected.fbo);
1576
- gl.useProgram(condensateCorrectProgram);
1577
- web.bindTextureAt(gl, 0, condensate[front].texture, condensateCorrectProgram, "u_dye");
1578
- web.bindTextureAt(gl, 1, condensateForward.texture, condensateCorrectProgram, "u_forward");
1579
- web.setUniform(gl, condensateCorrectLoc("u_forwardSize"), gridSize);
1580
- bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p, spatial);
1581
- web.setUniform(gl, condensateCorrectLoc("u_settleSpeed"), p.condensateSettleSpeed);
1582
- web.setUniform(gl, condensateCorrectLoc("u_settleWiggle"), p.condensateSettleWiggle);
1583
- web.drawFullscreenTriangle(gl);
1584
- gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[back].fbo);
1585
- gl.useProgram(vaporReactProgram);
1586
- web.bindTextureAt(gl, 0, vaporCorrected.texture, vaporReactProgram, "u_transported");
1587
- web.bindTextureAt(gl, 1, condensateCorrected.texture, vaporReactProgram, "u_condensateTransported");
1588
- web.setUniform(gl, vaporReactLoc("u_transportedSize"), gridSize);
1589
- web.setUniform(gl, vaporReactLoc("u_condensateTransportedSize"), gridSize);
1590
- web.setUniform(gl, vaporReactLoc("u_resolution"), gridSize);
1591
- web.setUniform(gl, vaporReactLoc("u_dt"), clampedDt);
1592
- web.setUniform(gl, vaporReactLoc("u_condensationRate"), p.condensationRate);
1593
- web.setUniform(gl, vaporReactLoc("u_condensationFloor"), p.condensationFloor);
1594
- web.setUniform(gl, vaporReactLoc("u_evaporationRate"), p.evaporationRate);
1595
- web.setUniform(gl, vaporReactLoc("u_totalScale"), waterScale);
1596
- web.drawFullscreenTriangle(gl);
1597
- gl.bindFramebuffer(gl.FRAMEBUFFER, condensate[back].fbo);
1598
- gl.useProgram(condensateReactProgram);
1599
- web.bindTextureAt(gl, 0, condensateCorrected.texture, condensateReactProgram, "u_transported");
1600
- web.bindTextureAt(gl, 1, vaporCorrected.texture, condensateReactProgram, "u_vaporTransported");
1601
- web.bindTextureAt(gl, 2, condensate[front].texture, condensateReactProgram, "u_dye");
1602
- web.setUniform(gl, condensateReactLoc("u_transportedSize"), gridSize);
1603
- web.setUniform(gl, condensateReactLoc("u_vaporTransportedSize"), gridSize);
1604
- web.setUniform(gl, condensateReactLoc("u_dyeSize"), gridSize);
1605
- web.setUniform(gl, condensateReactLoc("u_resolution"), gridSize);
1606
- web.setUniform(gl, condensateReactLoc("u_dt"), clampedDt);
1607
- web.setUniform(gl, condensateReactLoc("u_condensationRate"), p.condensationRate);
1608
- web.setUniform(gl, condensateReactLoc("u_condensationFloor"), p.condensationFloor);
1609
- web.setUniform(gl, condensateReactLoc("u_evaporationRate"), p.evaporationRate);
1610
- web.setUniform(gl, condensateReactLoc("u_spreadRate"), p.condensateSpreadRate);
1611
- web.setUniform(gl, condensateReactLoc("u_totalScale"), waterScale);
1612
- web.drawFullscreenTriangle(gl);
1613
- gl.bindFramebuffer(gl.FRAMEBUFFER, track[back].fbo);
1614
- gl.useProgram(trackAdvectProgram);
1615
- web.bindTextureAt(gl, 0, track[front].texture, trackAdvectProgram, "u_dye");
1616
- bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p, spatial);
1617
- web.setUniform(gl, trackAdvectLoc("u_decay"), p.decay);
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);
1633
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1634
- front = back;
1635
- framesSinceRenorm += 1;
1636
- timeSinceRenorm += clampedDt;
1637
- if (waterTargetTotal > 0 && timeSinceRenorm >= WATER_RENORM_INTERVAL_S && framesSinceRenorm > 0) {
1638
- const current = readTarget(vapor[front]) + readTarget(condensate[front]);
1639
- if (current > 1e-6) {
1640
- const totalRatio = waterTargetTotal / current;
1641
- const perFrame = Math.pow(totalRatio, 1 / framesSinceRenorm);
1642
- waterScale = Math.min(1.05, Math.max(0.95, perFrame));
1643
- }
1644
- timeSinceRenorm = 0;
1645
- framesSinceRenorm = 0;
1646
- }
1647
- }
1648
- function emit(emissions) {
1649
- if (!track || emissions.length === 0) return;
1650
- const minDim = Math.min(gridW, gridH);
1651
- gl.bindFramebuffer(gl.FRAMEBUFFER, track[front].fbo);
1652
- gl.viewport(0, 0, gridW, gridH);
1653
- gl.enable(gl.BLEND);
1654
- gl.blendFunc(gl.ONE, gl.ONE);
1655
- gl.useProgram(emitProgram);
1656
- web.setUniform(gl, emitLoc("u_resolution"), [gridW, gridH]);
1657
- for (const e of emissions) {
1658
- const source = [e.source[0] * gridW, e.source[1] * gridH];
1659
- const length = e.lengthFrac * minDim;
1660
- const head = [
1661
- source[0] + Math.cos(e.angle) * length,
1662
- source[1] + Math.sin(e.angle) * length
1663
- ];
1664
- web.setUniform(gl, emitLoc("u_source"), source);
1665
- web.setUniform(gl, emitLoc("u_head"), head);
1666
- web.setUniform(gl, emitLoc("u_headWidth"), e.headWidthFrac * minDim);
1667
- web.setUniform(gl, emitLoc("u_tailWidth"), e.tailWidthFrac * minDim);
1668
- web.setUniform(gl, emitLoc("u_raggedAmp"), e.raggedFrac * minDim);
1669
- web.setUniform(gl, emitLoc("u_raggedFreq"), e.raggedFreq);
1670
- web.setUniform(gl, emitLoc("u_settle"), e.settleFrac * minDim);
1671
- web.setUniform(gl, emitLoc("u_tailDim"), e.tailDim);
1672
- web.setUniform(gl, emitLoc("u_intensity"), e.intensity);
1673
- web.setUniform(gl, emitLoc("u_seed"), e.seed);
1674
- web.drawFullscreenTriangle(gl);
1675
- }
1676
- gl.disable(gl.BLEND);
1677
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1678
- }
1679
- function stepTrackLayer(dt, emissions) {
1680
- liveTracks = trackLayer.step(dt, emissions);
1681
- }
1682
- function composite(contentWidth, contentHeight, params = {}) {
1683
- if (!vapor || !condensate || !track || !shadow) return;
1684
- const p = { ...MEDIUM_DEFAULTS, ...params };
1685
- gl.viewport(0, 0, contentWidth, contentHeight);
1686
- gl.disable(gl.BLEND);
1687
- gl.useProgram(compositeProgram);
1688
- web.bindTextureAt(gl, 0, vapor[front].texture, compositeProgram, "u_vapor");
1689
- web.bindTextureAt(gl, 1, condensate[front].texture, compositeProgram, "u_condensate");
1690
- web.bindTextureAt(gl, 2, track[front].texture, compositeProgram, "u_track");
1691
- web.bindTextureAt(gl, 3, shadow.texture, compositeProgram, "u_shadow");
1692
- web.setUniform(gl, compositeLoc("u_vaporSize"), [gridW, gridH]);
1693
- web.setUniform(gl, compositeLoc("u_condensateSize"), [gridW, gridH]);
1694
- web.setUniform(gl, compositeLoc("u_trackSize"), [gridW, gridH]);
1695
- web.setUniform(gl, compositeLoc("u_shadowSize"), [gridW, gridH]);
1696
- web.setUniform(gl, compositeLoc("u_resolution"), [contentWidth, contentHeight]);
1697
- web.setUniform(gl, compositeLoc("u_dyeScale"), [gridW / contentWidth, gridH / contentHeight]);
1698
- web.setUniform(gl, compositeLoc("u_lab0"), srgbToOklab(p.channelColors[0]));
1699
- web.setUniform(gl, compositeLoc("u_lab1"), srgbToOklab(p.channelColors[1]));
1700
- web.setUniform(gl, compositeLoc("u_lab2"), srgbToOklab(p.channelColors[2]));
1701
- web.setUniform(gl, compositeLoc("u_labBg"), srgbToOklab(p.baseColor));
1702
- web.setUniform(gl, compositeLoc("u_labCondensate"), srgbToOklab(p.condensateColor));
1703
- web.setUniform(gl, compositeLoc("u_baseWeight"), p.baseWeight);
1704
- web.setUniform(gl, compositeLoc("u_condensateTint"), p.condensateTint);
1705
- web.setUniform(gl, compositeLoc("u_condensateGain"), p.condensateGain);
1706
- web.setUniform(gl, compositeLoc("u_farScale"), p.depthFarScale);
1707
- web.setUniform(gl, compositeLoc("u_farOffset"), depthFarOffsetPx(p.depthFarOffsetFrac, gridW, gridH));
1708
- web.setUniform(gl, compositeLoc("u_farBlurRadius"), p.depthFarBlurRadius);
1709
- web.setUniform(gl, compositeLoc("u_farWeight"), p.depthFarWeight);
1710
- web.setUniform(gl, compositeLoc("u_gravityBoost"), p.gravityBottomBoost);
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);
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);
1718
- }
1719
- function readTarget(target) {
1720
- gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
1721
- let sum = 0;
1722
- if (format.type === gl.HALF_FLOAT) {
1723
- const buf = new Float32Array(gridW * gridH * 4);
1724
- gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
1725
- for (let i = 0; i < buf.length; i += 4) sum += buf[i] + buf[i + 1] + buf[i + 2];
1726
- } else {
1727
- const buf = new Uint8Array(gridW * gridH * 4);
1728
- gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
1729
- for (let i = 0; i < buf.length; i += 4) sum += (buf[i] + buf[i + 1] + buf[i + 2]) / 255;
1730
- }
1731
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1732
- return sum;
1733
- }
1734
- function readTotals() {
1735
- if (!vapor || !condensate || !track) return null;
1736
- return {
1737
- vapor: readTarget(vapor[front]),
1738
- condensate: readTarget(condensate[front]),
1739
- track: readTarget(track[front])
1740
- };
1741
- }
1742
- function readVaporGrid() {
1743
- if (!vapor) return null;
1744
- gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[front].fbo);
1745
- const cells = gridW * gridH;
1746
- const data = new Array(cells * 3);
1747
- if (format.type === gl.HALF_FLOAT) {
1748
- const buf = new Float32Array(cells * 4);
1749
- gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
1750
- for (let i = 0; i < cells; i += 1) {
1751
- data[i * 3] = buf[i * 4];
1752
- data[i * 3 + 1] = buf[i * 4 + 1];
1753
- data[i * 3 + 2] = buf[i * 4 + 2];
1754
- }
1755
- } else {
1756
- const buf = new Uint8Array(cells * 4);
1757
- gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
1758
- for (let i = 0; i < cells; i += 1) {
1759
- data[i * 3] = buf[i * 4] / 255;
1760
- data[i * 3 + 1] = buf[i * 4 + 1] / 255;
1761
- data[i * 3 + 2] = buf[i * 4 + 2] / 255;
1762
- }
1763
- }
1764
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1765
- return { cols: gridW, rows: gridH, data };
1766
- }
1767
- function readCondensateGrid() {
1768
- if (!condensate) return null;
1769
- gl.bindFramebuffer(gl.FRAMEBUFFER, condensate[front].fbo);
1770
- const cells = gridW * gridH;
1771
- const data = new Array(cells);
1772
- if (format.type === gl.HALF_FLOAT) {
1773
- const buf = new Float32Array(cells * 4);
1774
- gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
1775
- for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4];
1776
- } else {
1777
- const buf = new Uint8Array(cells * 4);
1778
- gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
1779
- for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4] / 255;
1780
- }
1781
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1782
- return { cols: gridW, rows: gridH, data };
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
- }
1801
- function destroy() {
1802
- destroyTargets();
1803
- gl.deleteProgram(vaporForwardProgram);
1804
- gl.deleteProgram(vaporCorrectProgram);
1805
- gl.deleteProgram(vaporReactProgram);
1806
- gl.deleteProgram(condensateForwardProgram);
1807
- gl.deleteProgram(condensateCorrectProgram);
1808
- gl.deleteProgram(condensateReactProgram);
1809
- gl.deleteProgram(trackAdvectProgram);
1810
- gl.deleteProgram(compositeProgram);
1811
- gl.deleteProgram(seedProgram);
1812
- gl.deleteProgram(emitProgram);
1813
- gl.deleteProgram(shadowProgram);
1814
- gl.deleteProgram(resampleProgram);
1815
- trackLayerProgram.destroy();
1816
- }
1817
- return {
1818
- ensureGrid,
1819
- step,
1820
- emit,
1821
- stepTrackLayer,
1822
- composite,
1823
- readTotals,
1824
- readVaporGrid,
1825
- readCondensateGrid,
1826
- readTrackGrid,
1827
- destroy
1828
- };
1829
- }
1830
- function createMediumBackdrop() {
1831
- let runtime = null;
1832
- let owner = null;
1833
- return {
1834
- pass: (frame) => (gl, target) => {
1835
- if (owner && owner !== gl) throw new Error("vireuikit: the medium is already bound to another WebGL context");
1836
- owner = gl;
1837
- runtime ??= createMediumRuntime(gl, web.FULLSCREEN_TRIANGLE_VERTEX_SOURCE);
1838
- runtime.ensureGrid(frame.gridWidth, frame.gridHeight);
1839
- runtime.step(frame.dt, frame.params);
1840
- runtime.emit(frame.emissions ?? []);
1841
- runtime.stepTrackLayer(frame.dt, frame.emissions ?? []);
1842
- gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
1843
- runtime.composite(target.width, target.height, frame.params);
1844
- },
1845
- readTotals: () => runtime?.readTotals() ?? null,
1846
- readVaporGrid: () => runtime?.readVaporGrid() ?? null,
1847
- readCondensateGrid: () => runtime?.readCondensateGrid() ?? null,
1848
- readTrackGrid: () => runtime?.readTrackGrid() ?? null,
1849
- destroy: () => {
1850
- runtime?.destroy();
1851
- runtime = null;
1852
- owner = null;
1853
- }
1854
- };
1855
- }
1856
-
1857
- exports.buildTrackLayerInstances = buildTrackLayerInstances;
1858
- exports.createMediumBackdrop = createMediumBackdrop;
1859
- exports.createMediumRuntime = createMediumRuntime;
1860
- exports.createTrackLayer = createTrackLayer;
1861
- exports.createTrackLayerProgram = createTrackLayerProgram;
1862
- exports.trackLayerFade = trackLayerFade;
1863
- exports.trackLayerSigmaMul = trackLayerSigmaMul;
1864
- //# sourceMappingURL=index.cjs.map
1865
- //# sourceMappingURL=index.cjs.map