vireuikit 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -15
- package/dist/{chunk-BV5P6MF7.js → chunk-VY22O2II.js} +485 -46
- package/dist/chunk-VY22O2II.js.map +1 -0
- package/dist/{dynamics-DelasYER.d.cts → dynamics-DqGk_f1n.d.cts} +170 -14
- package/dist/{dynamics-DelasYER.d.ts → dynamics-DqGk_f1n.d.ts} +170 -14
- package/dist/index.cjs +434 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.js +1 -1
- package/dist/web/index.cjs +984 -145
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +63 -2
- package/dist/web/index.d.ts +63 -2
- package/dist/web/index.js +462 -13
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BV5P6MF7.js.map +0 -1
package/dist/web/index.cjs
CHANGED
|
@@ -3,125 +3,6 @@
|
|
|
3
3
|
var vireglass = require('vireglass');
|
|
4
4
|
var web = require('vireglass/web');
|
|
5
5
|
|
|
6
|
-
// src/medium/oklab.ts
|
|
7
|
-
var LINEAR_TO_LMS = [
|
|
8
|
-
[0.4122214708, 0.5363325363, 0.0514459929],
|
|
9
|
-
[0.2119034982, 0.6806995451, 0.1073969566],
|
|
10
|
-
[0.0883024619, 0.2817188376, 0.6299787005]
|
|
11
|
-
];
|
|
12
|
-
var LMS_TO_OKLAB = [
|
|
13
|
-
[0.2104542553, 0.793617785, -0.0040720468],
|
|
14
|
-
[1.9779984951, -2.428592205, 0.4505937099],
|
|
15
|
-
[0.0259040371, 0.7827717662, -0.808675766]
|
|
16
|
-
];
|
|
17
|
-
var OKLAB_TO_LMS = [
|
|
18
|
-
[1, 0.3963377774, 0.2158037573],
|
|
19
|
-
[1, -0.1055613458, -0.0638541728],
|
|
20
|
-
[1, -0.0894841775, -1.291485548]
|
|
21
|
-
];
|
|
22
|
-
var LMS_TO_LINEAR = [
|
|
23
|
-
[4.0767416621, -3.3077115913, 0.2309699292],
|
|
24
|
-
[-1.2684380046, 2.6097574011, -0.3413193965],
|
|
25
|
-
[-0.0041960863, -0.7034186147, 1.707614701]
|
|
26
|
-
];
|
|
27
|
-
var glsl = (v) => Number.isInteger(v) ? v.toFixed(1) : String(v);
|
|
28
|
-
var row = (m, i, vars) => m[i].map((k, j) => `${glsl(k)} * ${vars[j]}`).join(" + ");
|
|
29
|
-
var VG_OKLAB_TO_SRGB = `
|
|
30
|
-
float3 vgLinearToSrgb(float3 c) {
|
|
31
|
-
float3 lo = c * 12.92;
|
|
32
|
-
float3 hi = 1.055 * pow(max(c, float3(0.0)), float3(1.0 / 2.4)) - 0.055;
|
|
33
|
-
return mix(lo, hi, step(float3(0.0031308), c));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
float3 vgOklabToLinear(float3 lab) {
|
|
37
|
-
float l_ = ${row(OKLAB_TO_LMS, 0, ["lab.x", "lab.y", "lab.z"])};
|
|
38
|
-
float m_ = ${row(OKLAB_TO_LMS, 1, ["lab.x", "lab.y", "lab.z"])};
|
|
39
|
-
float s_ = ${row(OKLAB_TO_LMS, 2, ["lab.x", "lab.y", "lab.z"])};
|
|
40
|
-
float l = l_ * l_ * l_;
|
|
41
|
-
float m = m_ * m_ * m_;
|
|
42
|
-
float s = s_ * s_ * s_;
|
|
43
|
-
return float3(
|
|
44
|
-
${row(LMS_TO_LINEAR, 0, ["l", "m", "s"])},
|
|
45
|
-
${row(LMS_TO_LINEAR, 1, ["l", "m", "s"])},
|
|
46
|
-
${row(LMS_TO_LINEAR, 2, ["l", "m", "s"])}
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
`;
|
|
50
|
-
var apply = (m, v) => [
|
|
51
|
-
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
|
|
52
|
-
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
|
|
53
|
-
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]
|
|
54
|
-
];
|
|
55
|
-
var srgbChannelToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
56
|
-
var linearChannelToSrgb = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * Math.pow(Math.max(c, 0), 1 / 2.4) - 0.055;
|
|
57
|
-
function srgbToOklab(rgb) {
|
|
58
|
-
const linear = rgb.map(srgbChannelToLinear);
|
|
59
|
-
const lms = apply(LINEAR_TO_LMS, linear);
|
|
60
|
-
return apply(LMS_TO_OKLAB, lms.map((v) => Math.cbrt(Math.max(v, 0))));
|
|
61
|
-
}
|
|
62
|
-
function oklabToSrgb(lab) {
|
|
63
|
-
const lms = apply(OKLAB_TO_LMS, lab).map((v) => v * v * v);
|
|
64
|
-
return apply(LMS_TO_LINEAR, lms).map(linearChannelToSrgb);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// src/medium/species.ts
|
|
68
|
-
var MEDIUM_SPECIES_LIGHTNESS = 0.44;
|
|
69
|
-
var MEDIUM_SPECIES_HUE_SPREAD = 32;
|
|
70
|
-
var clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
71
|
-
function characterOf(color) {
|
|
72
|
-
const [lightness, a, b] = srgbToOklab(color);
|
|
73
|
-
return {
|
|
74
|
-
hue: Math.atan2(b, a) * 180 / Math.PI,
|
|
75
|
-
chroma: Math.hypot(a, b),
|
|
76
|
-
lightness
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function speciesFamily(character, spreadDeg = MEDIUM_SPECIES_HUE_SPREAD) {
|
|
80
|
-
const { hue, chroma, lightness } = character;
|
|
81
|
-
const tone = (deg) => {
|
|
82
|
-
const rad = deg * Math.PI / 180;
|
|
83
|
-
return oklabToSrgb([lightness, chroma * Math.cos(rad), chroma * Math.sin(rad)]).map(
|
|
84
|
-
clamp01
|
|
85
|
-
);
|
|
86
|
-
};
|
|
87
|
-
return [tone(hue - spreadDeg), tone(hue), tone(hue + spreadDeg)];
|
|
88
|
-
}
|
|
89
|
-
function neutralSpecies(baseColor) {
|
|
90
|
-
const { hue, chroma } = characterOf(baseColor);
|
|
91
|
-
return speciesFamily({ hue, chroma, lightness: MEDIUM_SPECIES_LIGHTNESS });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/medium/params.ts
|
|
95
|
-
var BASE_COLOR = [0.07, 0.08, 0.1];
|
|
96
|
-
var MEDIUM_DEFAULTS = {
|
|
97
|
-
decay: 0.06,
|
|
98
|
-
curlFreq: 0.045,
|
|
99
|
-
curlSpeed: 0.12,
|
|
100
|
-
advectSpeed: 26,
|
|
101
|
-
turbulence: 1,
|
|
102
|
-
condensationRate: 0.18,
|
|
103
|
-
condensationFloor: 0.12,
|
|
104
|
-
evaporationRate: 0.2,
|
|
105
|
-
condensateSettleSpeed: 8,
|
|
106
|
-
condensateSpreadRate: 0.08,
|
|
107
|
-
condensateTint: 0.2,
|
|
108
|
-
condensateGain: 5,
|
|
109
|
-
condensateColor: [0.94, 0.95, 0.97],
|
|
110
|
-
channelColors: neutralSpecies(BASE_COLOR),
|
|
111
|
-
baseColor: BASE_COLOR,
|
|
112
|
-
baseWeight: 0.9,
|
|
113
|
-
depthFarScale: 1.6,
|
|
114
|
-
depthFarOffsetFrac: [0.47, 0.53],
|
|
115
|
-
depthFarBlurRadius: 5,
|
|
116
|
-
depthFarWeight: 0.5,
|
|
117
|
-
gravityVaporDrift: 0.6,
|
|
118
|
-
gravityBottomBoost: 2,
|
|
119
|
-
gravityBottomBoostStart: 0.75
|
|
120
|
-
};
|
|
121
|
-
function depthFarOffsetPx(offsetFrac, gridWidth, gridHeight) {
|
|
122
|
-
return [offsetFrac[0] * gridWidth, offsetFrac[1] * gridHeight];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
6
|
// src/medium/noise.ts
|
|
126
7
|
var VG_VALUE_NOISE = `
|
|
127
8
|
float vgHash21(float2 p) {
|
|
@@ -252,7 +133,343 @@ float2 vgCurlVelocity(float2 p, float phase) {
|
|
|
252
133
|
float py0 = vgPotential(p - float2(0.0, e), phase);
|
|
253
134
|
return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
|
|
254
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
|
+
}
|
|
255
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;
|
|
256
473
|
|
|
257
474
|
// src/medium/seed-shader.ts
|
|
258
475
|
var MEDIUM_SEED_SHADER = `
|
|
@@ -295,15 +512,25 @@ uniform float u_phase;
|
|
|
295
512
|
uniform float u_curlFreq;
|
|
296
513
|
uniform float u_advectSpeed;
|
|
297
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;
|
|
298
523
|
|
|
299
524
|
${VG_CURL_NOISE}
|
|
300
525
|
`;
|
|
301
526
|
var MEDIUM_ADVECT_VELOCITY = `
|
|
302
|
-
float2
|
|
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;
|
|
303
529
|
`;
|
|
304
530
|
var MEDIUM_ADVECT_VELOCITY_SETTLE = `
|
|
305
531
|
${MEDIUM_ADVECT_VELOCITY}
|
|
306
532
|
vel.y += u_settleSpeed;
|
|
533
|
+
vel.x += curl.x * u_advectSpeed * u_settleWiggle;
|
|
307
534
|
`;
|
|
308
535
|
var MEDIUM_ADVECT_VELOCITY_VAPOR = `
|
|
309
536
|
${MEDIUM_ADVECT_VELOCITY}
|
|
@@ -322,6 +549,7 @@ ${MEDIUM_ADVECT_VELOCITY_VAPOR}
|
|
|
322
549
|
var MEDIUM_CONDENSATE_FORWARD_SHADER = `
|
|
323
550
|
${MEDIUM_ADVECT_UNIFORMS}
|
|
324
551
|
uniform float u_settleSpeed;
|
|
552
|
+
uniform float u_settleWiggle;
|
|
325
553
|
|
|
326
554
|
half4 main(float2 xy) {
|
|
327
555
|
${MEDIUM_ADVECT_VELOCITY_SETTLE}
|
|
@@ -361,6 +589,7 @@ ${MEDIUM_MACCORMACK_CORRECT}
|
|
|
361
589
|
var MEDIUM_CONDENSATE_CORRECT_SHADER = `
|
|
362
590
|
${MEDIUM_ADVECT_UNIFORMS}
|
|
363
591
|
uniform float u_settleSpeed;
|
|
592
|
+
uniform float u_settleWiggle;
|
|
364
593
|
uniform shader u_forward;
|
|
365
594
|
|
|
366
595
|
half4 main(float2 xy) {
|
|
@@ -460,10 +689,68 @@ ${MEDIUM_ADVECT_VELOCITY}
|
|
|
460
689
|
`;
|
|
461
690
|
|
|
462
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
|
+
`;
|
|
463
749
|
var MEDIUM_COMPOSITE_SHADER = `
|
|
464
750
|
uniform shader u_vapor;
|
|
465
751
|
uniform shader u_condensate;
|
|
466
752
|
uniform shader u_track;
|
|
753
|
+
uniform shader u_shadow;
|
|
467
754
|
uniform float2 u_dyeScale;
|
|
468
755
|
uniform float3 u_lab0;
|
|
469
756
|
uniform float3 u_lab1;
|
|
@@ -479,27 +766,68 @@ uniform float u_farBlurRadius;
|
|
|
479
766
|
uniform float u_farWeight;
|
|
480
767
|
uniform float u_gravityBoost;
|
|
481
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;
|
|
482
789
|
|
|
483
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}
|
|
484
796
|
|
|
485
797
|
half4 main(float2 xy) {
|
|
486
798
|
float2 baseSrc = xy * u_dyeScale;
|
|
487
799
|
|
|
488
|
-
// Near plane \u2014
|
|
489
|
-
|
|
490
|
-
half4
|
|
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);
|
|
491
804
|
half4 track = u_track.eval(baseSrc);
|
|
492
805
|
|
|
493
|
-
// Far plane \u2014 the identical field, scaled and offset (see the file header).
|
|
494
|
-
//
|
|
495
|
-
//
|
|
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.
|
|
496
814
|
float2 farSrc = baseSrc * u_farScale + u_farOffset;
|
|
497
|
-
float2
|
|
498
|
-
float2
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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);
|
|
503
831
|
|
|
504
832
|
// Gravity's "denser layer at the bottom of the chamber" \u2014 a compositing-only boost (see
|
|
505
833
|
// MEDIUM_DEFAULTS.gravityBottomBoost): it scales how density already in the conserved buffers is
|
|
@@ -532,7 +860,34 @@ half4 main(float2 xy) {
|
|
|
532
860
|
float total = max(w0 + w1 + w2 + wBg + wCondensate, 1e-4);
|
|
533
861
|
float3 mixLab =
|
|
534
862
|
(u_labBg * wBg + u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2 + condensateLab * wCondensate) / total;
|
|
535
|
-
|
|
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);
|
|
536
891
|
float3 srgb = vgLinearToSrgb(rgbLinear);
|
|
537
892
|
return half4(half3(srgb), half(1.0));
|
|
538
893
|
}
|
|
@@ -550,7 +905,6 @@ uniform float u_settle;
|
|
|
550
905
|
uniform float u_tailDim;
|
|
551
906
|
uniform float u_intensity;
|
|
552
907
|
uniform float u_seed;
|
|
553
|
-
uniform float3 u_channelMask;
|
|
554
908
|
|
|
555
909
|
${VG_VALUE_NOISE}
|
|
556
910
|
|
|
@@ -573,10 +927,407 @@ half4 main(float2 xy) {
|
|
|
573
927
|
best = max(best, v);
|
|
574
928
|
}
|
|
575
929
|
|
|
576
|
-
float3 dye =
|
|
930
|
+
float3 dye = float3(best * u_intensity);
|
|
577
931
|
return half4(half3(dye), half(1.0));
|
|
578
932
|
}
|
|
579
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
|
|
580
1331
|
function pickDyeFormat(gl) {
|
|
581
1332
|
const hasFloat = gl.getExtension("EXT_color_buffer_float") !== null;
|
|
582
1333
|
return hasFloat ? { internalFormat: gl.RGBA16F, type: gl.HALF_FLOAT } : { internalFormat: gl.RGBA8, type: gl.UNSIGNED_BYTE };
|
|
@@ -594,6 +1345,10 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
594
1345
|
const seedProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_SEED_SHADER));
|
|
595
1346
|
const emitProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_EMIT_SHADER));
|
|
596
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 = [];
|
|
597
1352
|
const vaporForwardLoc = web.locationCache(gl, vaporForwardProgram);
|
|
598
1353
|
const vaporCorrectLoc = web.locationCache(gl, vaporCorrectProgram);
|
|
599
1354
|
const vaporReactLoc = web.locationCache(gl, vaporReactProgram);
|
|
@@ -605,6 +1360,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
605
1360
|
const seedLoc = web.locationCache(gl, seedProgram);
|
|
606
1361
|
const emitLoc = web.locationCache(gl, emitProgram);
|
|
607
1362
|
const resampleLoc = web.locationCache(gl, resampleProgram);
|
|
1363
|
+
const shadowLoc = web.locationCache(gl, shadowProgram);
|
|
608
1364
|
let gridW = 0;
|
|
609
1365
|
let gridH = 0;
|
|
610
1366
|
let vapor = null;
|
|
@@ -614,6 +1370,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
614
1370
|
let vaporCorrected = null;
|
|
615
1371
|
let condensateForward = null;
|
|
616
1372
|
let condensateCorrected = null;
|
|
1373
|
+
let shadow = null;
|
|
617
1374
|
let front = 0;
|
|
618
1375
|
function makeTarget(w, h) {
|
|
619
1376
|
const texture = web.createTexture(gl, {
|
|
@@ -657,6 +1414,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
657
1414
|
destroyTarget(vaporCorrected);
|
|
658
1415
|
destroyTarget(condensateForward);
|
|
659
1416
|
destroyTarget(condensateCorrected);
|
|
1417
|
+
destroyTarget(shadow);
|
|
660
1418
|
vapor = null;
|
|
661
1419
|
condensate = null;
|
|
662
1420
|
track = null;
|
|
@@ -664,6 +1422,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
664
1422
|
vaporCorrected = null;
|
|
665
1423
|
condensateForward = null;
|
|
666
1424
|
condensateCorrected = null;
|
|
1425
|
+
shadow = null;
|
|
667
1426
|
}
|
|
668
1427
|
function seed(w, h) {
|
|
669
1428
|
if (!vapor || !condensate || !track) return;
|
|
@@ -717,6 +1476,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
717
1476
|
vaporCorrected = makeTarget(w, h);
|
|
718
1477
|
condensateForward = makeTarget(w, h);
|
|
719
1478
|
condensateCorrected = makeTarget(w, h);
|
|
1479
|
+
shadow = makeTarget(w, h);
|
|
720
1480
|
front = 0;
|
|
721
1481
|
const areaRatio = w * h / (oldW * oldH);
|
|
722
1482
|
waterTargetTotal *= areaRatio;
|
|
@@ -735,6 +1495,7 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
735
1495
|
vaporCorrected = makeTarget(w, h);
|
|
736
1496
|
condensateForward = makeTarget(w, h);
|
|
737
1497
|
condensateCorrected = makeTarget(w, h);
|
|
1498
|
+
shadow = makeTarget(w, h);
|
|
738
1499
|
front = 0;
|
|
739
1500
|
seed(w, h);
|
|
740
1501
|
waterTargetTotal = readTarget(vapor[front]) + readTarget(condensate[front]);
|
|
@@ -745,7 +1506,18 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
745
1506
|
}
|
|
746
1507
|
resizeGrid(w, h);
|
|
747
1508
|
}
|
|
748
|
-
function
|
|
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) {
|
|
749
1521
|
web.setUniform(gl, loc("u_dyeSize"), [gridW, gridH]);
|
|
750
1522
|
web.setUniform(gl, loc("u_resolution"), [gridW, gridH]);
|
|
751
1523
|
web.setUniform(gl, loc("u_dt"), dt);
|
|
@@ -754,6 +1526,10 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
754
1526
|
web.setUniform(gl, loc("u_advectSpeed"), p.advectSpeed);
|
|
755
1527
|
web.setUniform(gl, loc("u_turbulence"), p.turbulence);
|
|
756
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]);
|
|
757
1533
|
}
|
|
758
1534
|
let phase = 0;
|
|
759
1535
|
function wrapPhase(value) {
|
|
@@ -766,10 +1542,11 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
766
1542
|
let timeSinceRenorm = 0;
|
|
767
1543
|
let framesSinceRenorm = 0;
|
|
768
1544
|
function step(dt, params = {}) {
|
|
769
|
-
if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected) {
|
|
1545
|
+
if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected || !shadow) {
|
|
770
1546
|
return;
|
|
771
1547
|
}
|
|
772
1548
|
const p = { ...MEDIUM_DEFAULTS, ...params };
|
|
1549
|
+
const spatial = computeMediumSpatialPeriods(gridW, gridH, p.curlFreq);
|
|
773
1550
|
const clampedDt = Math.min(Math.max(dt, 0), 1 / 15);
|
|
774
1551
|
phase = wrapPhase(phase + clampedDt * p.curlSpeed);
|
|
775
1552
|
const back = front === 0 ? 1 : 0;
|
|
@@ -779,28 +1556,30 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
779
1556
|
gl.bindFramebuffer(gl.FRAMEBUFFER, vaporForward.fbo);
|
|
780
1557
|
gl.useProgram(vaporForwardProgram);
|
|
781
1558
|
web.bindTextureAt(gl, 0, vapor[front].texture, vaporForwardProgram, "u_dye");
|
|
782
|
-
bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p);
|
|
1559
|
+
bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p, spatial);
|
|
783
1560
|
web.drawFullscreenTriangle(gl);
|
|
784
1561
|
gl.bindFramebuffer(gl.FRAMEBUFFER, condensateForward.fbo);
|
|
785
1562
|
gl.useProgram(condensateForwardProgram);
|
|
786
1563
|
web.bindTextureAt(gl, 0, condensate[front].texture, condensateForwardProgram, "u_dye");
|
|
787
|
-
bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p);
|
|
1564
|
+
bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p, spatial);
|
|
788
1565
|
web.setUniform(gl, condensateForwardLoc("u_settleSpeed"), p.condensateSettleSpeed);
|
|
1566
|
+
web.setUniform(gl, condensateForwardLoc("u_settleWiggle"), p.condensateSettleWiggle);
|
|
789
1567
|
web.drawFullscreenTriangle(gl);
|
|
790
1568
|
gl.bindFramebuffer(gl.FRAMEBUFFER, vaporCorrected.fbo);
|
|
791
1569
|
gl.useProgram(vaporCorrectProgram);
|
|
792
1570
|
web.bindTextureAt(gl, 0, vapor[front].texture, vaporCorrectProgram, "u_dye");
|
|
793
1571
|
web.bindTextureAt(gl, 1, vaporForward.texture, vaporCorrectProgram, "u_forward");
|
|
794
1572
|
web.setUniform(gl, vaporCorrectLoc("u_forwardSize"), gridSize);
|
|
795
|
-
bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p);
|
|
1573
|
+
bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p, spatial);
|
|
796
1574
|
web.drawFullscreenTriangle(gl);
|
|
797
1575
|
gl.bindFramebuffer(gl.FRAMEBUFFER, condensateCorrected.fbo);
|
|
798
1576
|
gl.useProgram(condensateCorrectProgram);
|
|
799
1577
|
web.bindTextureAt(gl, 0, condensate[front].texture, condensateCorrectProgram, "u_dye");
|
|
800
1578
|
web.bindTextureAt(gl, 1, condensateForward.texture, condensateCorrectProgram, "u_forward");
|
|
801
1579
|
web.setUniform(gl, condensateCorrectLoc("u_forwardSize"), gridSize);
|
|
802
|
-
bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p);
|
|
1580
|
+
bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p, spatial);
|
|
803
1581
|
web.setUniform(gl, condensateCorrectLoc("u_settleSpeed"), p.condensateSettleSpeed);
|
|
1582
|
+
web.setUniform(gl, condensateCorrectLoc("u_settleWiggle"), p.condensateSettleWiggle);
|
|
804
1583
|
web.drawFullscreenTriangle(gl);
|
|
805
1584
|
gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[back].fbo);
|
|
806
1585
|
gl.useProgram(vaporReactProgram);
|
|
@@ -834,9 +1613,23 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
834
1613
|
gl.bindFramebuffer(gl.FRAMEBUFFER, track[back].fbo);
|
|
835
1614
|
gl.useProgram(trackAdvectProgram);
|
|
836
1615
|
web.bindTextureAt(gl, 0, track[front].texture, trackAdvectProgram, "u_dye");
|
|
837
|
-
bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p);
|
|
1616
|
+
bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p, spatial);
|
|
838
1617
|
web.setUniform(gl, trackAdvectLoc("u_decay"), p.decay);
|
|
839
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);
|
|
840
1633
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
841
1634
|
front = back;
|
|
842
1635
|
framesSinceRenorm += 1;
|
|
@@ -878,14 +1671,16 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
878
1671
|
web.setUniform(gl, emitLoc("u_tailDim"), e.tailDim);
|
|
879
1672
|
web.setUniform(gl, emitLoc("u_intensity"), e.intensity);
|
|
880
1673
|
web.setUniform(gl, emitLoc("u_seed"), e.seed);
|
|
881
|
-
web.setUniform(gl, emitLoc("u_channelMask"), e.channel);
|
|
882
1674
|
web.drawFullscreenTriangle(gl);
|
|
883
1675
|
}
|
|
884
1676
|
gl.disable(gl.BLEND);
|
|
885
1677
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
886
1678
|
}
|
|
1679
|
+
function stepTrackLayer(dt, emissions) {
|
|
1680
|
+
liveTracks = trackLayer.step(dt, emissions);
|
|
1681
|
+
}
|
|
887
1682
|
function composite(contentWidth, contentHeight, params = {}) {
|
|
888
|
-
if (!vapor || !condensate || !track) return;
|
|
1683
|
+
if (!vapor || !condensate || !track || !shadow) return;
|
|
889
1684
|
const p = { ...MEDIUM_DEFAULTS, ...params };
|
|
890
1685
|
gl.viewport(0, 0, contentWidth, contentHeight);
|
|
891
1686
|
gl.disable(gl.BLEND);
|
|
@@ -893,9 +1688,11 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
893
1688
|
web.bindTextureAt(gl, 0, vapor[front].texture, compositeProgram, "u_vapor");
|
|
894
1689
|
web.bindTextureAt(gl, 1, condensate[front].texture, compositeProgram, "u_condensate");
|
|
895
1690
|
web.bindTextureAt(gl, 2, track[front].texture, compositeProgram, "u_track");
|
|
1691
|
+
web.bindTextureAt(gl, 3, shadow.texture, compositeProgram, "u_shadow");
|
|
896
1692
|
web.setUniform(gl, compositeLoc("u_vaporSize"), [gridW, gridH]);
|
|
897
1693
|
web.setUniform(gl, compositeLoc("u_condensateSize"), [gridW, gridH]);
|
|
898
1694
|
web.setUniform(gl, compositeLoc("u_trackSize"), [gridW, gridH]);
|
|
1695
|
+
web.setUniform(gl, compositeLoc("u_shadowSize"), [gridW, gridH]);
|
|
899
1696
|
web.setUniform(gl, compositeLoc("u_resolution"), [contentWidth, contentHeight]);
|
|
900
1697
|
web.setUniform(gl, compositeLoc("u_dyeScale"), [gridW / contentWidth, gridH / contentHeight]);
|
|
901
1698
|
web.setUniform(gl, compositeLoc("u_lab0"), srgbToOklab(p.channelColors[0]));
|
|
@@ -912,7 +1709,12 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
912
1709
|
web.setUniform(gl, compositeLoc("u_farWeight"), p.depthFarWeight);
|
|
913
1710
|
web.setUniform(gl, compositeLoc("u_gravityBoost"), p.gravityBottomBoost);
|
|
914
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);
|
|
915
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);
|
|
916
1718
|
}
|
|
917
1719
|
function readTarget(target) {
|
|
918
1720
|
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
|
|
@@ -979,6 +1781,23 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
979
1781
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
980
1782
|
return { cols: gridW, rows: gridH, data };
|
|
981
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
|
+
}
|
|
982
1801
|
function destroy() {
|
|
983
1802
|
destroyTargets();
|
|
984
1803
|
gl.deleteProgram(vaporForwardProgram);
|
|
@@ -991,9 +1810,22 @@ function createMediumRuntime(gl, vertexSource) {
|
|
|
991
1810
|
gl.deleteProgram(compositeProgram);
|
|
992
1811
|
gl.deleteProgram(seedProgram);
|
|
993
1812
|
gl.deleteProgram(emitProgram);
|
|
1813
|
+
gl.deleteProgram(shadowProgram);
|
|
994
1814
|
gl.deleteProgram(resampleProgram);
|
|
1815
|
+
trackLayerProgram.destroy();
|
|
995
1816
|
}
|
|
996
|
-
return {
|
|
1817
|
+
return {
|
|
1818
|
+
ensureGrid,
|
|
1819
|
+
step,
|
|
1820
|
+
emit,
|
|
1821
|
+
stepTrackLayer,
|
|
1822
|
+
composite,
|
|
1823
|
+
readTotals,
|
|
1824
|
+
readVaporGrid,
|
|
1825
|
+
readCondensateGrid,
|
|
1826
|
+
readTrackGrid,
|
|
1827
|
+
destroy
|
|
1828
|
+
};
|
|
997
1829
|
}
|
|
998
1830
|
function createMediumBackdrop() {
|
|
999
1831
|
let runtime = null;
|
|
@@ -1006,12 +1838,14 @@ function createMediumBackdrop() {
|
|
|
1006
1838
|
runtime.ensureGrid(frame.gridWidth, frame.gridHeight);
|
|
1007
1839
|
runtime.step(frame.dt, frame.params);
|
|
1008
1840
|
runtime.emit(frame.emissions ?? []);
|
|
1841
|
+
runtime.stepTrackLayer(frame.dt, frame.emissions ?? []);
|
|
1009
1842
|
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
1010
1843
|
runtime.composite(target.width, target.height, frame.params);
|
|
1011
1844
|
},
|
|
1012
1845
|
readTotals: () => runtime?.readTotals() ?? null,
|
|
1013
1846
|
readVaporGrid: () => runtime?.readVaporGrid() ?? null,
|
|
1014
1847
|
readCondensateGrid: () => runtime?.readCondensateGrid() ?? null,
|
|
1848
|
+
readTrackGrid: () => runtime?.readTrackGrid() ?? null,
|
|
1015
1849
|
destroy: () => {
|
|
1016
1850
|
runtime?.destroy();
|
|
1017
1851
|
runtime = null;
|
|
@@ -1020,7 +1854,12 @@ function createMediumBackdrop() {
|
|
|
1020
1854
|
};
|
|
1021
1855
|
}
|
|
1022
1856
|
|
|
1857
|
+
exports.buildTrackLayerInstances = buildTrackLayerInstances;
|
|
1023
1858
|
exports.createMediumBackdrop = createMediumBackdrop;
|
|
1024
1859
|
exports.createMediumRuntime = createMediumRuntime;
|
|
1860
|
+
exports.createTrackLayer = createTrackLayer;
|
|
1861
|
+
exports.createTrackLayerProgram = createTrackLayerProgram;
|
|
1862
|
+
exports.trackLayerFade = trackLayerFade;
|
|
1863
|
+
exports.trackLayerSigmaMul = trackLayerSigmaMul;
|
|
1025
1864
|
//# sourceMappingURL=index.cjs.map
|
|
1026
1865
|
//# sourceMappingURL=index.cjs.map
|