@3driseai/3drise-viewer 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AnimatedObject.d.ts.map +1 -1
- package/dist/components/AnimatedObject.js +3 -2
- package/dist/components/CreatedObject.d.ts.map +1 -1
- package/dist/components/CreatedObject.js +18 -14
- package/dist/components/ModelMaterialPassContext.d.ts +7 -0
- package/dist/components/ModelMaterialPassContext.d.ts.map +1 -0
- package/dist/components/ModelMaterialPassContext.js +7 -0
- package/dist/components/ModelPrimitive.d.ts.map +1 -1
- package/dist/components/ModelPrimitive.js +7 -1
- package/dist/components/ObjectNode.d.ts.map +1 -1
- package/dist/components/ObjectNode.js +10 -6
- package/dist/components/ParticlesGenerator.d.ts.map +1 -1
- package/dist/components/ParticlesGenerator.js +11 -5
- package/dist/components/particles/BioluminescentWakeParticles.d.ts +5 -2
- package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -1
- package/dist/components/particles/BioluminescentWakeParticles.js +28 -6
- package/dist/components/particles/BrownianDustParticles.d.ts +5 -2
- package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -1
- package/dist/components/particles/BrownianDustParticles.js +28 -6
- package/dist/components/particles/ChladniSandParticles.d.ts +15 -0
- package/dist/components/particles/ChladniSandParticles.d.ts.map +1 -0
- package/dist/components/particles/ChladniSandParticles.js +912 -0
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts +5 -2
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -1
- package/dist/components/particles/ElectronOrbitalsParticles.js +28 -6
- package/dist/components/particles/FaradayPowderParticles.d.ts +15 -0
- package/dist/components/particles/FaradayPowderParticles.d.ts.map +1 -0
- package/dist/components/particles/FaradayPowderParticles.js +783 -0
- package/dist/components/particles/GrinderSparksParticles.d.ts +5 -2
- package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -1
- package/dist/components/particles/GrinderSparksParticles.js +37 -13
- package/dist/components/particles/NeonRainParticles.d.ts +15 -0
- package/dist/components/particles/NeonRainParticles.d.ts.map +1 -0
- package/dist/components/particles/NeonRainParticles.js +716 -0
- package/dist/components/particles/TidalStreamParticles.d.ts +5 -2
- package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -1
- package/dist/components/particles/TidalStreamParticles.js +28 -6
- package/dist/components/particles/index.d.ts +6 -0
- package/dist/components/particles/index.d.ts.map +1 -1
- package/dist/components/particles/index.js +3 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/useConfigLive.d.ts +54 -0
- package/dist/hooks/useConfigLive.d.ts.map +1 -0
- package/dist/hooks/useConfigLive.js +100 -0
- package/dist/hooks/useMaterialApplication.d.ts.map +1 -1
- package/dist/hooks/useMaterialApplication.js +1 -0
- package/dist/hooks/useNodeOverrides.d.ts +13 -0
- package/dist/hooks/useNodeOverrides.d.ts.map +1 -0
- package/dist/hooks/useNodeOverrides.js +254 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/utils/configAnimations.d.ts +43 -0
- package/dist/utils/configAnimations.d.ts.map +1 -0
- package/dist/utils/configAnimations.js +73 -0
- package/dist/utils/particleTrails.d.ts.map +1 -1
- package/dist/utils/particleTrails.js +17 -8
- package/package.json +2 -2
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import * as THREE from 'three';
|
|
4
|
+
import { useFrame } from '@react-three/fiber';
|
|
5
|
+
import { GENERATIVE_PARTICLES_ANIMATABLE } from '@3driseai/3drise-core';
|
|
6
|
+
import { applyConfigKeyframeAnimations } from '../../utils/configAnimations.js';
|
|
7
|
+
import { createPointerUniforms, stepPointerField, usePointerField } from '../../utils/pointerField.js';
|
|
8
|
+
import { createTrailUniforms, applyTrailConfig, createCpuTrail } from '../../utils/particleTrails.js';
|
|
9
|
+
import { useObjectReady } from '../../hooks/useObjectReady.js';
|
|
10
|
+
import { useConfigLiveHandle } from '../../hooks/useConfigLive.js';
|
|
11
|
+
/**
|
|
12
|
+
* Neon Rain — Night rain lit by neon: Marshall–Palmer drops at terminal velocity, gusting sheets, splash crowns and rim drips, each drop lit like a tiny lens; the cursor is an umbrella, a gust or a headlight. Use for city, noir and cinematic scenes.
|
|
13
|
+
*/
|
|
14
|
+
// Neon Rain — rain at night lit by off-screen neon and a streetlamp, filmed at high speed.
|
|
15
|
+
// The object is the rain alone: drops, splash crowns and drips. The lights are not drawn; they only
|
|
16
|
+
// exist as the light the drops scatter.
|
|
17
|
+
// Movement: raindrops with Marshall–Palmer sizes (N(D) ~ exp(-Lambda D), Lambda = 4.1 R^-0.21 /mm)
|
|
18
|
+
// falling at their Atlas terminal velocity v(D) = 9.65 - 10.3 exp(-0.6 D) m/s. Each drop
|
|
19
|
+
// relaxes to the local air velocity with its own drag time tau = v_t / g, so small drops
|
|
20
|
+
// follow the gusts and big ones cut through. The wind is a steady breeze plus travelling
|
|
21
|
+
// gust fronts that sweep sheets of rain across. On the ground plane the splash
|
|
22
|
+
// parameter K = We^1/2 Re^1/4 decides: K > 57.7 throws a crown of secondary droplets
|
|
23
|
+
// (Mundo's threshold), smaller drops just deposit.
|
|
24
|
+
// Colour: drops are tiny water lenses lit by two neon lights and a sodium streetlamp: forward
|
|
25
|
+
// refraction (Henyey–Greenstein lobe), Fresnel reflection, and the primary rainbow caustic
|
|
26
|
+
// at 138 degrees with its spectral split. Out-of-focus drops become bokeh discs (thin lens).
|
|
27
|
+
// Mouse (pointerMode):
|
|
28
|
+
// umbrella — an invisible umbrella: drops hitting the canopy splash off it (outlining its dome),
|
|
29
|
+
// water runs to the rim and drips in heavy streams, leaving a dry column below.
|
|
30
|
+
// gust — the cursor is a gust of wind: a moving-body flow with a wake that bends and swirls
|
|
31
|
+
// the rain sheets (small drops follow, big drops cut through).
|
|
32
|
+
// headlight — an oncoming headlight at the back of the street, placed by the cursor and aimed at
|
|
33
|
+
// the viewer: drops in the beam flare in their forward-scattering lobe.
|
|
34
|
+
// Trails (trailMode): streak = shutter streaks, comet = wet threads, echo = splash afterglow.
|
|
35
|
+
//
|
|
36
|
+
// Generated from the approved lab module (particle lab, batch 3). Sim units = object-local / size.
|
|
37
|
+
// ---------------------------------------------------------------------------------------------
|
|
38
|
+
// Engine features
|
|
39
|
+
// ---------------------------------------------------------------------------------------------
|
|
40
|
+
const QUALITY_SCALE = { low: 0.5, medium: 1, high: 1.6 };
|
|
41
|
+
/** mulberry32 — seeded PRNG, returns a function giving floats in [0,1). */
|
|
42
|
+
function mulberry32(seed) {
|
|
43
|
+
let a = seed >>> 0;
|
|
44
|
+
return function () {
|
|
45
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
46
|
+
let t = a;
|
|
47
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
48
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
49
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Standard normal from a uniform PRNG (Box–Muller, one value per call). */
|
|
53
|
+
function gauss(rng) {
|
|
54
|
+
const u = Math.max(rng(), 1e-9);
|
|
55
|
+
const v = rng();
|
|
56
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(6.283185307 * v);
|
|
57
|
+
}
|
|
58
|
+
/** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
|
|
59
|
+
function attachViewH(obj, u) {
|
|
60
|
+
const scratch = new THREE.Vector2();
|
|
61
|
+
obj.onBeforeRender = (renderer) => {
|
|
62
|
+
renderer.getDrawingBufferSize(scratch);
|
|
63
|
+
u.uViewH.value = scratch.y;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function colorUniform(hex) {
|
|
67
|
+
return { value: new THREE.Color(hex) };
|
|
68
|
+
}
|
|
69
|
+
function qualityScale(q) { return QUALITY_SCALE[q] || 1; }
|
|
70
|
+
function seedMix(base, seed) {
|
|
71
|
+
let h = (base ^ Math.imul((Math.round(seed) | 0) + 0x9e37, 0x85ebca6b)) >>> 0;
|
|
72
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
|
|
73
|
+
return (h ^ (h >>> 16)) >>> 0;
|
|
74
|
+
}
|
|
75
|
+
function createFixedStep(H, maxSteps) {
|
|
76
|
+
let acc = 0;
|
|
77
|
+
return {
|
|
78
|
+
H,
|
|
79
|
+
run(d, step) {
|
|
80
|
+
acc += Math.max(d, 0);
|
|
81
|
+
let k = 0;
|
|
82
|
+
while (acc >= H && k < maxSteps) {
|
|
83
|
+
step(H);
|
|
84
|
+
acc -= H;
|
|
85
|
+
k++;
|
|
86
|
+
}
|
|
87
|
+
if (k === maxSteps)
|
|
88
|
+
acc = Math.min(acc, H);
|
|
89
|
+
return acc / H;
|
|
90
|
+
},
|
|
91
|
+
reset() { acc = 0; },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function createPickProxy(center, radius) {
|
|
95
|
+
const geo = new THREE.SphereGeometry(1, 16, 10);
|
|
96
|
+
const mat = new THREE.MeshBasicMaterial({ visible: false });
|
|
97
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
98
|
+
mesh.name = 'pickProxy';
|
|
99
|
+
mesh.position.set(center[0], center[1], center[2]);
|
|
100
|
+
mesh.scale.setScalar(radius);
|
|
101
|
+
return { mesh, dispose() { geo.dispose(); mat.dispose(); } };
|
|
102
|
+
}
|
|
103
|
+
const _frustum = new THREE.Frustum();
|
|
104
|
+
const _pm = new THREE.Matrix4();
|
|
105
|
+
const _sphere = new THREE.Sphere();
|
|
106
|
+
function inView(object, camera, center, radius, size) {
|
|
107
|
+
if (!camera || !camera.projectionMatrix)
|
|
108
|
+
return true;
|
|
109
|
+
object.updateWorldMatrix(true, false);
|
|
110
|
+
_sphere.center.set(center[0], center[1], center[2]).multiplyScalar(size).applyMatrix4(object.matrixWorld);
|
|
111
|
+
_sphere.radius = radius * size * object.matrixWorld.getMaxScaleOnAxis();
|
|
112
|
+
_pm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
113
|
+
_frustum.setFromProjectionMatrix(_pm);
|
|
114
|
+
return _frustum.intersectsSphere(_sphere);
|
|
115
|
+
}
|
|
116
|
+
const POINTER_MODES = ['umbrella', 'gust', 'headlight'];
|
|
117
|
+
// ---- world (sim units = metres), laid out round the object origin ----
|
|
118
|
+
const GROUND = -1.0; // road / puddle height
|
|
119
|
+
const TOP = 1.0; // rain enters here
|
|
120
|
+
const XH = 1.3, ZH = 0.85; // half extents of the rain volume
|
|
121
|
+
const ZS = -1.05; // plane of the (off-screen) neon lights, behind the rain
|
|
122
|
+
const SLOWMO = 0.3; // physics seconds per shown second (a high-speed camera)
|
|
123
|
+
const H_STEP = 1 / 60;
|
|
124
|
+
const GRAV = 9.81;
|
|
125
|
+
const TR_MODE = { streak: 0.04, comet: 0.06, echo: 0.08 }; // physics seconds per trail unit
|
|
126
|
+
const TRAIL_GAIN = { streak: 3, comet: 3.5, echo: 4.5 };
|
|
127
|
+
const BOUNDS = { center: [0, 0, 0], radius: 1.75 };
|
|
128
|
+
// neon light sources behind the rain (never drawn): segments [x0, y0, x1, y1, colour index], and a ring [cx, cy, r, colour]
|
|
129
|
+
const SEGS = [[-0.9, -0.35, -0.9, 0.65, 0]];
|
|
130
|
+
const RING = [0.78, 0.2, 0.3, 1];
|
|
131
|
+
const LAMP = [-0.45, 1.35, -0.55];
|
|
132
|
+
// point-light samples of the tubes (for lighting the drops): x, y, colour index, power
|
|
133
|
+
function tubeLights() {
|
|
134
|
+
const L = [];
|
|
135
|
+
for (const s of SEGS) {
|
|
136
|
+
const k = Math.max(2, Math.round(Math.hypot(s[2] - s[0], s[3] - s[1]) / 0.33));
|
|
137
|
+
for (let i = 0; i < k; i++) {
|
|
138
|
+
const t = (i + 0.5) / k;
|
|
139
|
+
L.push([s[0] + (s[2] - s[0]) * t, s[1] + (s[3] - s[1]) * t, s[4], 1 / k * Math.hypot(s[2] - s[0], s[3] - s[1])]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (let i = 0; i < 5; i++) {
|
|
143
|
+
const a = (i / 5) * 6.2831853 + 0.3;
|
|
144
|
+
L.push([RING[0] + RING[2] * Math.cos(a), RING[1] + RING[2] * Math.sin(a), RING[3], 1.0 / 5]); // both lights carry the same total power
|
|
145
|
+
}
|
|
146
|
+
return L;
|
|
147
|
+
}
|
|
148
|
+
const LIGHTS = tubeLights();
|
|
149
|
+
function createUniforms() {
|
|
150
|
+
return {
|
|
151
|
+
uTime: { value: 0 },
|
|
152
|
+
uPhysT: { value: 0 },
|
|
153
|
+
uSize: { value: 1 },
|
|
154
|
+
uViewH: { value: 800 },
|
|
155
|
+
uIntensity: { value: 1 },
|
|
156
|
+
uSpeed: { value: 1 },
|
|
157
|
+
uOpacity: { value: 0.9 },
|
|
158
|
+
uColor: colorUniform('#ffffff'),
|
|
159
|
+
uParticleCount: { value: 0 },
|
|
160
|
+
uRainRate: { value: 18 },
|
|
161
|
+
uWindSpeed: { value: 1.6 },
|
|
162
|
+
uGustiness: { value: 0.6 },
|
|
163
|
+
uNeonColor2: colorUniform('#ffffff'),
|
|
164
|
+
uLampColor: colorUniform('#ffffff'),
|
|
165
|
+
uNeonLight: { value: 1 },
|
|
166
|
+
uAperture: { value: 0.5 },
|
|
167
|
+
uDropSize: { value: 1 },
|
|
168
|
+
uPointerMode: { value: 0 },
|
|
169
|
+
uPointerStrength: { value: 1 },
|
|
170
|
+
uPointerRadius: { value: 1 },
|
|
171
|
+
uPointerColor: colorUniform('#ffffff'),
|
|
172
|
+
uFocus: { value: 3.3 },
|
|
173
|
+
uCamLocal: { value: new THREE.Vector3(0, 0, 3) },
|
|
174
|
+
uHeadPos: { value: new THREE.Vector3() },
|
|
175
|
+
uHeadDir: { value: new THREE.Vector3(0, 0, -1) },
|
|
176
|
+
uHead: { value: 0 }, // headlight power (0 = off)
|
|
177
|
+
uTrailGain: { value: 1 },
|
|
178
|
+
...createPointerUniforms(),
|
|
179
|
+
...createTrailUniforms(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function applyConfig(u, c, size) {
|
|
183
|
+
u.uSize.value = size;
|
|
184
|
+
u.uIntensity.value = c.intensity;
|
|
185
|
+
u.uSpeed.value = c.speed;
|
|
186
|
+
u.uOpacity.value = c.opacity;
|
|
187
|
+
u.uColor.value.set(c.color);
|
|
188
|
+
u.uParticleCount.value = c.particleCount;
|
|
189
|
+
u.uRainRate.value = c.rainRate;
|
|
190
|
+
u.uWindSpeed.value = c.windSpeed;
|
|
191
|
+
u.uGustiness.value = c.gustiness;
|
|
192
|
+
u.uNeonColor2.value.set(c.neonColor2);
|
|
193
|
+
u.uLampColor.value.set(c.lampColor);
|
|
194
|
+
u.uNeonLight.value = c.neonLight;
|
|
195
|
+
u.uAperture.value = c.aperture;
|
|
196
|
+
u.uDropSize.value = c.dropSize;
|
|
197
|
+
u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
|
|
198
|
+
u.uPointerStrength.value = c.pointerStrength;
|
|
199
|
+
u.uPointerRadius.value = c.pointerRadius;
|
|
200
|
+
u.uPointerColor.value.set(c.pointerColor);
|
|
201
|
+
applyTrailConfig(u, c);
|
|
202
|
+
}
|
|
203
|
+
const BASE_U = 'uniform float uSize, uViewH, uIntensity, uOpacity;';
|
|
204
|
+
const f3 = (v) => v.toFixed(4);
|
|
205
|
+
// ---- light on a water drop at p seen from the eye: refraction lobe + Fresnel + rainbow ----
|
|
206
|
+
const DROP_LIGHT_GLSL = /* glsl */ `
|
|
207
|
+
uniform vec3 uColor, uNeonColor2, uLampColor, uPointerColor;
|
|
208
|
+
uniform float uNeonLight, uHead;
|
|
209
|
+
uniform vec3 uHeadPos, uHeadDir;
|
|
210
|
+
uniform vec3 uCamLocal;
|
|
211
|
+
// scattering by a water sphere; cosT = cos(scattering angle), 1 = straight through
|
|
212
|
+
vec3 dropPhase(float cosT) {
|
|
213
|
+
float g = 0.75;
|
|
214
|
+
float hg = (1.0 - g * g) / pow(max(1.0 + g * g - 2.0 * g * cosT, 1e-4), 1.5) * 0.08; // refraction lobe
|
|
215
|
+
float refl = 0.06; // external reflection
|
|
216
|
+
// primary rainbow caustic near 138 deg, red outside, violet inside (Airy-ish widths)
|
|
217
|
+
float th = degrees(acos(clamp(cosT, -1.0, 1.0)));
|
|
218
|
+
vec3 bow = vec3(exp(-pow((th - 137.8) / 0.9, 2.0)), exp(-pow((th - 138.7) / 0.9, 2.0)), exp(-pow((th - 139.6) / 0.9, 2.0)));
|
|
219
|
+
bow += 0.25 * smoothstep(138.0, 150.0, th) * (1.0 - smoothstep(150.0, 170.0, th)); // brighter sky inside the bow
|
|
220
|
+
return vec3(hg + refl) + bow * 0.9;
|
|
221
|
+
}
|
|
222
|
+
vec3 pointLight(vec3 p, vec3 e, vec3 L, float pw) {
|
|
223
|
+
vec3 d = p - L;
|
|
224
|
+
float r2 = dot(d, d);
|
|
225
|
+
return vec3(pw / (r2 + 0.25)) * dropPhase(dot(normalize(d), e));
|
|
226
|
+
}
|
|
227
|
+
vec3 dropLight(vec3 p) {
|
|
228
|
+
vec3 e = normalize(uCamLocal - p);
|
|
229
|
+
vec3 acc = vec3(0.0);
|
|
230
|
+
${LIGHTS.map((l) => ` acc += ${['uColor', 'uNeonColor2'][l[2]]} * pointLight(p, e, vec3(${f3(l[0])}, ${f3(l[1] - 0.25)}, ${f3(ZS)}), ${f3(l[3] * 7.0)}) * uNeonLight;`).join('\n')}
|
|
231
|
+
acc += uLampColor * pointLight(p, e, vec3(${LAMP.map(f3).join(', ')}), 3.0);
|
|
232
|
+
if (uHead > 0.001) {
|
|
233
|
+
vec3 d = p - uHeadPos;
|
|
234
|
+
float r2 = dot(d, d);
|
|
235
|
+
vec3 dn = d * inversesqrt(max(r2, 1e-6));
|
|
236
|
+
float cone = smoothstep(0.7, 0.93, dot(dn, uHeadDir));
|
|
237
|
+
acc += uPointerColor * (uHead * 5.0 * cone / (r2 + 0.05)) * dropPhase(dot(dn, e));
|
|
238
|
+
}
|
|
239
|
+
acc += vec3(0.05, 0.07, 0.12) * 0.8; // city sky glow
|
|
240
|
+
return acc;
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
// ---- drops (rain, splash droplets, rim drips) ----
|
|
244
|
+
const dropVertex = /* glsl */ `
|
|
245
|
+
${BASE_U}
|
|
246
|
+
${DROP_LIGHT_GLSL}
|
|
247
|
+
uniform float uAperture, uFocus, uDropSize;
|
|
248
|
+
attribute float aD; // diameter in mm, 0 = not in flight
|
|
249
|
+
attribute float aKind; // 0 rain, 1 splash droplet, 2 rim drip, 3 canopy spray
|
|
250
|
+
varying vec3 vCol;
|
|
251
|
+
varying float vA;
|
|
252
|
+
varying float vBokeh;
|
|
253
|
+
void main() {
|
|
254
|
+
vec3 p = position;
|
|
255
|
+
vec4 mv = modelViewMatrix * vec4(p * uSize, 1.0);
|
|
256
|
+
float dist = -mv.z;
|
|
257
|
+
float alive = step(0.01, aD);
|
|
258
|
+
// fades: entering at the top, at the sides of the volume
|
|
259
|
+
float fade = smoothstep(${f3(TOP)}, ${f3(TOP - 0.2)}, p.y) * smoothstep(${f3(XH)}, ${f3(XH - 0.2)}, abs(p.x)) * smoothstep(${f3(ZH + 0.1)}, ${f3(ZH - 0.1)}, abs(p.z));
|
|
260
|
+
vec3 L = dropLight(p);
|
|
261
|
+
// a drop's light scales with its cross-section, gently (the sprites are much bigger than drops)
|
|
262
|
+
float s = sqrt(clamp(aD, 0.2, 5.0));
|
|
263
|
+
float lum = dot(L, vec3(0.3, 0.5, 0.2));
|
|
264
|
+
float basePx = uDropSize * uSize * uViewH * (0.0035 + 0.002 * s) * (1.0 + 0.6 * min(lum, 2.5)) / dist;
|
|
265
|
+
float coc = uAperture * uViewH * 0.05 * abs(dist - uFocus) / max(dist, 0.05);
|
|
266
|
+
float px = sqrt(basePx * basePx + coc * coc);
|
|
267
|
+
float spread = clamp((basePx * basePx) / (px * px), 0.02, 1.0);
|
|
268
|
+
vBokeh = smoothstep(4.0, 14.0, coc);
|
|
269
|
+
vCol = (L * (0.6 + 0.4 * s) + uPointerColor * (step(2.5, aKind) * 1.4 + step(1.5, aKind) * (1.0 - step(2.5, aKind)) * 0.5)) * uIntensity;
|
|
270
|
+
vA = uOpacity * alive * fade * mix(1.0, spread * 2.5, vBokeh);
|
|
271
|
+
gl_PointSize = clamp(px, 1.0, 72.0) * step(0.001, vA);
|
|
272
|
+
gl_Position = projectionMatrix * mv;
|
|
273
|
+
}
|
|
274
|
+
`;
|
|
275
|
+
const dropFragment = /* glsl */ `
|
|
276
|
+
varying vec3 vCol;
|
|
277
|
+
varying float vA;
|
|
278
|
+
varying float vBokeh;
|
|
279
|
+
void main() {
|
|
280
|
+
vec2 d = gl_PointCoord * 2.0 - 1.0;
|
|
281
|
+
float r2 = dot(d, d);
|
|
282
|
+
if (r2 > 1.0) discard;
|
|
283
|
+
float core = (exp(-r2 * 7.0) + 0.2 * exp(-r2 * 2.0)) * (1.0 - r2);
|
|
284
|
+
float r = sqrt(r2);
|
|
285
|
+
float disc = (1.0 - smoothstep(0.8, 1.0, r)) * (0.65 + 0.35 * smoothstep(0.5, 0.95, r));
|
|
286
|
+
float a = mix(core, disc * 0.45, vBokeh) * vA;
|
|
287
|
+
if (a < 0.002) discard;
|
|
288
|
+
gl_FragColor = vec4(vCol, a);
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
function build(u, c, size) {
|
|
292
|
+
const q = qualityScale(c.quality);
|
|
293
|
+
const nr = Math.max(200, Math.round(c.particleCount * q));
|
|
294
|
+
const ns = Math.round(nr * 0.55);
|
|
295
|
+
const n = nr + ns;
|
|
296
|
+
const rng = mulberry32(seedMix(0x7a1f, c.seed));
|
|
297
|
+
const pos = new Float32Array(n * 3), vel = new Float32Array(n * 3);
|
|
298
|
+
const Dm = new Float32Array(n); // diameter (mm); 0 = not in flight
|
|
299
|
+
const vt = new Float32Array(n); // terminal velocity (m/s)
|
|
300
|
+
const kind = new Uint8Array(n); // 0 rain, 1 splash droplet, 2 rim drip
|
|
301
|
+
const K = new Float32Array(n); // trail brightness weight
|
|
302
|
+
const kindF = new Float32Array(n); // kind, as a vertex attribute
|
|
303
|
+
let trail = null;
|
|
304
|
+
const atlas = (D) => Math.max(0.3, 9.65 - 10.3 * Math.exp(-0.6 * D));
|
|
305
|
+
function sampleD() {
|
|
306
|
+
const R = Math.max(u.uRainRate.value, 0.5);
|
|
307
|
+
const lam = 4.1 * Math.pow(R, -0.21);
|
|
308
|
+
let D = 0;
|
|
309
|
+
do {
|
|
310
|
+
D = -Math.log(Math.max(rng(), 1e-9)) / lam;
|
|
311
|
+
} while (D < 0.3 || D > 5.5);
|
|
312
|
+
return D;
|
|
313
|
+
}
|
|
314
|
+
// wind: breeze + travelling gust fronts + two slow eddies (divergence-free in x-y)
|
|
315
|
+
let physT = 0;
|
|
316
|
+
const wind = new Float32Array(3);
|
|
317
|
+
function windAt(x, y, z) {
|
|
318
|
+
const U = u.uWindSpeed.value, G = u.uGustiness.value;
|
|
319
|
+
const xi = x - U * physT * 0.8;
|
|
320
|
+
const front = Math.sin(0.9 * xi + 1.3) * Math.sin(0.31 * physT + 0.7 * z) + 0.5 * Math.sin(2.1 * xi - 0.4 * physT + 1.9);
|
|
321
|
+
const ex = Math.sin(2.3 * y + 0.9 * physT) * Math.cos(1.7 * x), ey = -Math.cos(2.3 * y + 0.9 * physT) * Math.sin(1.7 * x) * (1.7 / 2.3);
|
|
322
|
+
wind[0] = U * (1 + 0.55 * G * front) + 0.6 * G * ex;
|
|
323
|
+
wind[1] = 0.6 * G * ey;
|
|
324
|
+
wind[2] = 0.35 * G * Math.sin(1.1 * xi + 0.5 * physT);
|
|
325
|
+
}
|
|
326
|
+
function spawnRain(i, anywhere) {
|
|
327
|
+
const j = i * 3;
|
|
328
|
+
const D = sampleD();
|
|
329
|
+
Dm[i] = D;
|
|
330
|
+
vt[i] = atlas(D);
|
|
331
|
+
kind[i] = 0;
|
|
332
|
+
kindF[i] = 0;
|
|
333
|
+
const U = u.uWindSpeed.value;
|
|
334
|
+
const fall = (TOP - GROUND) / vt[i];
|
|
335
|
+
// spawn upwind of the volume so the slanted rain fills it; rain sheets: accept by a travelling band
|
|
336
|
+
let x = 0, zz = 0;
|
|
337
|
+
for (let tries = 0; tries < 4; tries++) {
|
|
338
|
+
const drift = Math.max(-0.45, Math.min(0.45, U * fall * 0.6));
|
|
339
|
+
x = -XH + rng() * 2 * XH - drift * rng() * (anywhere ? 0 : 1);
|
|
340
|
+
zz = (rng() * 2 - 1) * ZH;
|
|
341
|
+
const band = 0.55 + 0.45 * Math.sin(1.4 * (x - U * physT * 0.8) + 0.9 * Math.sin(0.6 * zz + 0.2 * physT));
|
|
342
|
+
if (rng() < band)
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
pos[j] = x;
|
|
346
|
+
pos[j + 2] = zz;
|
|
347
|
+
pos[j + 1] = anywhere ? GROUND + rng() * (TOP - GROUND) : TOP + 0.02 * rng();
|
|
348
|
+
vel[j] = U;
|
|
349
|
+
vel[j + 1] = -vt[i];
|
|
350
|
+
vel[j + 2] = 0;
|
|
351
|
+
K[i] = Math.sqrt(D);
|
|
352
|
+
if (trail)
|
|
353
|
+
trail.reset(i);
|
|
354
|
+
}
|
|
355
|
+
let sHead = 0;
|
|
356
|
+
function spawnSplash(x, y, z, vx, vy, vz, D, kd) {
|
|
357
|
+
const i = nr + (sHead % ns);
|
|
358
|
+
sHead++;
|
|
359
|
+
const j = i * 3;
|
|
360
|
+
pos[j] = x;
|
|
361
|
+
pos[j + 1] = y;
|
|
362
|
+
pos[j + 2] = z;
|
|
363
|
+
vel[j] = vx;
|
|
364
|
+
vel[j + 1] = vy;
|
|
365
|
+
vel[j + 2] = vz;
|
|
366
|
+
Dm[i] = D;
|
|
367
|
+
vt[i] = atlas(D);
|
|
368
|
+
kind[i] = kd;
|
|
369
|
+
kindF[i] = kd;
|
|
370
|
+
K[i] = Math.sqrt(D) * (kd === 1 || kd === 3 ? 1.4 : 1);
|
|
371
|
+
if (trail)
|
|
372
|
+
trail.reset(i);
|
|
373
|
+
}
|
|
374
|
+
for (let i = 0; i < nr; i++)
|
|
375
|
+
spawnRain(i, true);
|
|
376
|
+
const hp = new THREE.Vector3(), hv = new THREE.Vector3();
|
|
377
|
+
let emitAcc = 0, dripAcc = 0;
|
|
378
|
+
const hr = mulberry32(seedMix(0x5b1a, c.seed));
|
|
379
|
+
function step(hShown) {
|
|
380
|
+
const h = hShown * SLOWMO;
|
|
381
|
+
physT += h;
|
|
382
|
+
u.uPhysT.value = physT;
|
|
383
|
+
const mode = u.uPointerMode.value;
|
|
384
|
+
const S = u.uPointerStrength.value, act = u.uPointerActive.value, Rr = u.uPointerRadius.value;
|
|
385
|
+
hp.copy(u.uPointer.value);
|
|
386
|
+
hv.copy(u.uPointerVel.value);
|
|
387
|
+
const useUmb = mode === 0 && S > 0.01 && act > 0.02;
|
|
388
|
+
const useGust = mode === 1 && S > 0.01 && act > 0.01;
|
|
389
|
+
// umbrella: a dome of radius RU at the cursor (hand height), canopy apex a little above the rim
|
|
390
|
+
const RU = 0.42 * Rr * Math.min(Math.max(S, 0.3), 2);
|
|
391
|
+
const ux = hp.x, uy = Math.max(Math.min(hp.y, TOP - 0.1), GROUND + 0.4), uz = Math.max(Math.min(hp.z, ZH), -ZH);
|
|
392
|
+
// gust: a moving body in the air, velocity in physics time (a hand sweep is a strong gust)
|
|
393
|
+
const GS = 0.3 * Rr;
|
|
394
|
+
const AIR = 3.0 * S * act / SLOWMO;
|
|
395
|
+
const bvx = hv.x * AIR, bvy = hv.y * AIR, bvz = hv.z * AIR;
|
|
396
|
+
const bsp = Math.hypot(bvx, bvy, bvz), bn = bsp > 1e-4 ? 1 / bsp : 0;
|
|
397
|
+
const gustOn = useGust && bsp > 0.05;
|
|
398
|
+
const tauMin = 0.02;
|
|
399
|
+
// emission keeps the pool's live share tied to the rain rate
|
|
400
|
+
const liveShare = Math.min(1, 0.3 + u.uRainRate.value / 30);
|
|
401
|
+
emitAcc += (liveShare * nr * h) / ((TOP - GROUND) / 5.0);
|
|
402
|
+
let intercepted = 0;
|
|
403
|
+
for (let i = 0; i < n; i++) {
|
|
404
|
+
if (Dm[i] === 0) {
|
|
405
|
+
if (i < nr && emitAcc >= 1) {
|
|
406
|
+
emitAcc -= 1;
|
|
407
|
+
spawnRain(i, false);
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const j = i * 3;
|
|
412
|
+
let x = pos[j], y = pos[j + 1], z = pos[j + 2];
|
|
413
|
+
let vx = vel[j], vy = vel[j + 1], vz = vel[j + 2];
|
|
414
|
+
windAt(x, y, z);
|
|
415
|
+
let ax = wind[0], ay = wind[1], az = wind[2];
|
|
416
|
+
if (gustOn) {
|
|
417
|
+
const qx = x - hp.x, qy = y - hp.y, qz = z - hp.z;
|
|
418
|
+
const r2 = qx * qx + qy * qy + qz * qz;
|
|
419
|
+
if (r2 < 49 * GS * GS) {
|
|
420
|
+
const r = Math.sqrt(r2) + 1e-6;
|
|
421
|
+
if (r > GS) {
|
|
422
|
+
const k3 = (GS * GS * GS) / (2 * r2 * r);
|
|
423
|
+
const vr = (bvx * qx + bvy * qy + bvz * qz) / r;
|
|
424
|
+
ax += k3 * (3 * vr * qx / r - bvx);
|
|
425
|
+
ay += k3 * (3 * vr * qy / r - bvy);
|
|
426
|
+
az += k3 * (3 * vr * qz / r - bvz);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
ax += bvx;
|
|
430
|
+
ay += bvy;
|
|
431
|
+
az += bvz;
|
|
432
|
+
}
|
|
433
|
+
const behind = -(qx * bvx + qy * bvy + qz * bvz) * bn;
|
|
434
|
+
if (behind > 0) {
|
|
435
|
+
const perp2 = r2 - behind * behind;
|
|
436
|
+
const wk = Math.exp(-perp2 / (GS * GS * 1.5) - behind / (6 * GS));
|
|
437
|
+
// the wake rolls up: a swirl about the gust direction
|
|
438
|
+
ax += bvx * wk * 0.7 + (qy * bvz - qz * bvy) * bn * wk * 2.0;
|
|
439
|
+
ay += bvy * wk * 0.7 + (qz * bvx - qx * bvz) * bn * wk * 2.0;
|
|
440
|
+
az += bvz * wk * 0.7 + (qx * bvy - qy * bvx) * bn * wk * 2.0;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// drag relaxation toward the air + terminal fall: tau = v_t / g (exact exponential update)
|
|
445
|
+
const tau = Math.max(vt[i] / GRAV, tauMin);
|
|
446
|
+
const dcy = Math.exp(-h / tau);
|
|
447
|
+
vx = ax + (vx - ax) * dcy;
|
|
448
|
+
vz = az + (vz - az) * dcy;
|
|
449
|
+
vy = (ay - vt[i]) + (vy - (ay - vt[i])) * dcy;
|
|
450
|
+
const py = y;
|
|
451
|
+
x += vx * h;
|
|
452
|
+
y += vy * h;
|
|
453
|
+
z += vz * h;
|
|
454
|
+
// umbrella canopy
|
|
455
|
+
if (useUmb && (kind[i] === 0 || kind[i] === 2) && py > uy) {
|
|
456
|
+
const dx = x - ux, dz = z - uz;
|
|
457
|
+
const rr2 = dx * dx + dz * dz;
|
|
458
|
+
if (rr2 < RU * RU) {
|
|
459
|
+
const cy = uy + 0.16 * RU * (1 - rr2 / (RU * RU));
|
|
460
|
+
if (y <= cy) {
|
|
461
|
+
const rr = Math.sqrt(rr2) + 1e-4;
|
|
462
|
+
intercepted += Dm[i] * Dm[i] * Dm[i];
|
|
463
|
+
// the drop shatters on the fabric: small droplets fly off along the dome's slope
|
|
464
|
+
const nSpray = Dm[i] > 0.6 ? 2 : 1;
|
|
465
|
+
for (let k = 0; k < nSpray; k++) {
|
|
466
|
+
const out = 0.6 + 1.4 * hr(), a = (hr() - 0.5) * 1.2;
|
|
467
|
+
const ox = dx / rr * Math.cos(a) - dz / rr * Math.sin(a), oz = dz / rr * Math.cos(a) + dx / rr * Math.sin(a);
|
|
468
|
+
spawnSplash(x, cy + 0.004, z, ox * out + vx * 0.2, 0.5 + 1.0 * hr(), oz * out + vz * 0.2, 0.5 + 0.5 * hr(), 3);
|
|
469
|
+
}
|
|
470
|
+
Dm[i] = 0;
|
|
471
|
+
pos[j] = x;
|
|
472
|
+
pos[j + 1] = cy;
|
|
473
|
+
pos[j + 2] = z;
|
|
474
|
+
vel[j] = vel[j + 1] = vel[j + 2] = 0;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// the road: splash (K = We^1/2 Re^1/4 > 57.7) or deposit
|
|
480
|
+
if (y <= GROUND) {
|
|
481
|
+
if (Math.abs(x) < XH + 0.05 && Math.abs(z) < ZH + 0.15 && (kind[i] === 0 || kind[i] === 2)) {
|
|
482
|
+
const v = Math.hypot(vx, vy, vz), D = Dm[i] * 1e-3;
|
|
483
|
+
const We = (1000 * v * v * D) / 0.072, Re = (v * D) / 1e-6;
|
|
484
|
+
const Ks = Math.sqrt(We) * Math.pow(Re, 0.25);
|
|
485
|
+
if (Ks > 57.7) {
|
|
486
|
+
const m = 2 + Math.min(5, Math.floor((Ks - 57.7) / 90));
|
|
487
|
+
const vs = 0.22 * v;
|
|
488
|
+
for (let k = 0; k < m; k++) {
|
|
489
|
+
if (hr() > 0.7)
|
|
490
|
+
continue;
|
|
491
|
+
const a = 6.2831853 * hr(), el = 0.6 + 0.5 * hr();
|
|
492
|
+
const sp = vs * (0.6 + 0.8 * hr());
|
|
493
|
+
spawnSplash(x, GROUND + 0.002, z, Math.cos(a) * Math.cos(el) * sp, Math.sin(el) * sp, Math.sin(a) * Math.cos(el) * sp, Dm[i] * (0.2 + 0.15 * hr()), 1);
|
|
494
|
+
}
|
|
495
|
+
// Worthington jet: one droplet straight up
|
|
496
|
+
if (Dm[i] > 2 && hr() < 0.5)
|
|
497
|
+
spawnSplash(x, GROUND + 0.003, z, vx * 0.05, 0.3 * v, vz * 0.05, Dm[i] * 0.45, 1);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
Dm[i] = 0;
|
|
501
|
+
pos[j] = x;
|
|
502
|
+
pos[j + 1] = GROUND;
|
|
503
|
+
pos[j + 2] = z;
|
|
504
|
+
vel[j] = vel[j + 1] = vel[j + 2] = 0;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (Math.abs(x) > XH + 0.45 || Math.abs(z) > ZH + 0.5 || y > TOP + 0.5) {
|
|
508
|
+
// left the street: parked on the road (hidden) until re-emitted
|
|
509
|
+
Dm[i] = 0;
|
|
510
|
+
vel[j] = vel[j + 1] = vel[j + 2] = 0;
|
|
511
|
+
pos[j] = 0;
|
|
512
|
+
pos[j + 1] = GROUND;
|
|
513
|
+
pos[j + 2] = 0;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
pos[j] = x;
|
|
517
|
+
pos[j + 1] = y;
|
|
518
|
+
pos[j + 2] = z;
|
|
519
|
+
vel[j] = vx;
|
|
520
|
+
vel[j + 1] = vy;
|
|
521
|
+
vel[j + 2] = vz;
|
|
522
|
+
}
|
|
523
|
+
if (emitAcc > 50)
|
|
524
|
+
emitAcc = 50;
|
|
525
|
+
// rim drips: intercepted water runs to the rim and leaves as heavy drops
|
|
526
|
+
if (useUmb) {
|
|
527
|
+
dripAcc += intercepted / 8;
|
|
528
|
+
while (dripAcc >= 1) {
|
|
529
|
+
dripAcc -= 1;
|
|
530
|
+
const a = 6.2831853 * hr();
|
|
531
|
+
spawnSplash(ux + RU * 1.01 * Math.cos(a), uy - 0.005, uz + RU * 1.01 * Math.sin(a), 0, -0.2, 0, 3.2 + 1.2 * hr(), 2);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (dripAcc > 20)
|
|
535
|
+
dripAcc = 20;
|
|
536
|
+
}
|
|
537
|
+
const stepper = createFixedStep(H_STEP, 4);
|
|
538
|
+
for (let s = 0; s < 90; s++)
|
|
539
|
+
step(H_STEP);
|
|
540
|
+
// ---- geometry ----
|
|
541
|
+
const geo = new THREE.BufferGeometry();
|
|
542
|
+
const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
|
|
543
|
+
const dAttr = new THREE.BufferAttribute(Dm, 1).setUsage(THREE.DynamicDrawUsage);
|
|
544
|
+
geo.setAttribute('position', posAttr);
|
|
545
|
+
geo.setAttribute('aD', dAttr);
|
|
546
|
+
const kAttr = new THREE.BufferAttribute(kindF, 1).setUsage(THREE.DynamicDrawUsage);
|
|
547
|
+
geo.setAttribute('aKind', kAttr);
|
|
548
|
+
const mat = new THREE.ShaderMaterial({
|
|
549
|
+
uniforms: u, vertexShader: dropVertex, fragmentShader: dropFragment,
|
|
550
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
551
|
+
});
|
|
552
|
+
const drops = new THREE.Points(geo, mat);
|
|
553
|
+
drops.frustumCulled = false;
|
|
554
|
+
attachViewH(drops, u);
|
|
555
|
+
const object = new THREE.Group();
|
|
556
|
+
const proxy = createPickProxy(BOUNDS.center, BOUNDS.radius);
|
|
557
|
+
object.add(drops, proxy.mesh);
|
|
558
|
+
const anchor = new THREE.Vector3(0, 0, 0.25);
|
|
559
|
+
const anchorHead = new THREE.Vector3(0, 0, -0.55); // the headlight sits at the back of the street
|
|
560
|
+
const focusPt = new THREE.Vector3();
|
|
561
|
+
const camDir = new THREE.Vector3();
|
|
562
|
+
// ---- trails: every sample lit by the same lights as the drops ----
|
|
563
|
+
const trailGLSL = /* glsl */ `
|
|
564
|
+
${DROP_LIGHT_GLSL}
|
|
565
|
+
uniform float uTrailGain;
|
|
566
|
+
vec3 trailShade(vec3 p, float f) {
|
|
567
|
+
float fade = smoothstep(${f3(TOP)}, ${f3(TOP - 0.2)}, p.y) * step(${f3(GROUND)}, p.y);
|
|
568
|
+
return dropLight(p) * (0.5 + 0.5 * TRAIL_K) * 0.55 * uTrailGain * fade;
|
|
569
|
+
}
|
|
570
|
+
vec3 trailView(vec3 mv, out float mag) { mag = 1.0; return mv; }
|
|
571
|
+
`;
|
|
572
|
+
const materials = [mat];
|
|
573
|
+
let trailKey = '', trailTR = TR_MODE.streak;
|
|
574
|
+
function buildTrails(mode, share) {
|
|
575
|
+
trailTR = TR_MODE[mode] || TR_MODE.streak;
|
|
576
|
+
const key = mode + ':' + share;
|
|
577
|
+
if (key === trailKey)
|
|
578
|
+
return;
|
|
579
|
+
trailKey = key;
|
|
580
|
+
if (trail) {
|
|
581
|
+
object.remove(trail.object);
|
|
582
|
+
trail.dispose();
|
|
583
|
+
trail = null;
|
|
584
|
+
}
|
|
585
|
+
materials.length = 1;
|
|
586
|
+
u.uTrailGain.value = TRAIL_GAIN[mode] || 1;
|
|
587
|
+
trail = createCpuTrail(mode, { u, n, pos, vel, k: K, glsl: trailGLSL, sizePx: 0.0055, spacing: 0.004, sub: 8 });
|
|
588
|
+
if (!trail)
|
|
589
|
+
return;
|
|
590
|
+
object.add(trail.object);
|
|
591
|
+
materials.push(...trail.materials);
|
|
592
|
+
}
|
|
593
|
+
buildTrails(c.trailMode, c.trailShare);
|
|
594
|
+
function upload() { posAttr.needsUpdate = true; dAttr.needsUpdate = true; kAttr.needsUpdate = true; }
|
|
595
|
+
return {
|
|
596
|
+
object,
|
|
597
|
+
materials,
|
|
598
|
+
apply(cfg) { if (cfg)
|
|
599
|
+
buildTrails(cfg.trailMode, cfg.trailShare); },
|
|
600
|
+
update(dt, camera, pointer) {
|
|
601
|
+
const rdt = Math.min(Math.max(dt, 0), 1 / 20);
|
|
602
|
+
if (camera && !inView(object, camera, BOUNDS.center, BOUNDS.radius, u.uSize.value))
|
|
603
|
+
return;
|
|
604
|
+
const d = rdt * u.uSpeed.value;
|
|
605
|
+
u.uTime.value += d;
|
|
606
|
+
if (camera) {
|
|
607
|
+
object.localToWorld(focusPt.set(0, 0, 0));
|
|
608
|
+
u.uFocus.value = camera.position.distanceTo(focusPt);
|
|
609
|
+
object.worldToLocal(u.uCamLocal.value.copy(camera.position)).divideScalar(u.uSize.value);
|
|
610
|
+
if (pointer)
|
|
611
|
+
stepPointerField(u, pointer, camera, object, u.uSize.value, rdt, u.uPointerMode.value === 2 ? anchorHead : anchor);
|
|
612
|
+
// headlight: an oncoming car at the back of the street, aimed at the viewer and tipped down,
|
|
613
|
+
// so the drops between it and the eye flare in the forward-scattering lobe
|
|
614
|
+
const on = u.uPointerMode.value === 2 ? u.uPointerStrength.value * u.uPointerActive.value : 0;
|
|
615
|
+
u.uHead.value = on;
|
|
616
|
+
if (on > 0) {
|
|
617
|
+
camDir.copy(u.uCamLocal.value).sub(u.uPointer.value).normalize();
|
|
618
|
+
camDir.y -= 0.12;
|
|
619
|
+
camDir.normalize();
|
|
620
|
+
u.uHeadDir.value.copy(camDir);
|
|
621
|
+
u.uHeadPos.value.copy(u.uPointer.value);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
u.uVelScale.value = trailTR;
|
|
625
|
+
let simD = 0;
|
|
626
|
+
stepper.run(d, (hh) => { step(hh); simD += hh; });
|
|
627
|
+
if (simD > 0) {
|
|
628
|
+
upload();
|
|
629
|
+
if (trail)
|
|
630
|
+
trail.update((simD * SLOWMO) / trailTR);
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
seek(t) { for (let s = 0; s < Math.round(t / H_STEP); s++)
|
|
634
|
+
step(H_STEP); upload(); },
|
|
635
|
+
dispose() {
|
|
636
|
+
geo.dispose();
|
|
637
|
+
mat.dispose();
|
|
638
|
+
proxy.dispose();
|
|
639
|
+
if (trail)
|
|
640
|
+
trail.dispose();
|
|
641
|
+
},
|
|
642
|
+
_state: { pos, vel, Dm, kind },
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
|
|
646
|
+
const DEFAULTS = {
|
|
647
|
+
intensity: 1,
|
|
648
|
+
speed: 1,
|
|
649
|
+
opacity: 0.9,
|
|
650
|
+
size: 1,
|
|
651
|
+
color: '#ff2f9a',
|
|
652
|
+
particleCount: 10000,
|
|
653
|
+
rainRate: 18,
|
|
654
|
+
windSpeed: 0.8,
|
|
655
|
+
gustiness: 0.6,
|
|
656
|
+
neonColor2: '#22dcff',
|
|
657
|
+
lampColor: '#ffae52',
|
|
658
|
+
neonLight: 1,
|
|
659
|
+
aperture: 0.5,
|
|
660
|
+
dropSize: 1,
|
|
661
|
+
seed: 1,
|
|
662
|
+
quality: 'medium',
|
|
663
|
+
pointerMode: 'umbrella',
|
|
664
|
+
pointerStrength: 1,
|
|
665
|
+
pointerRadius: 1,
|
|
666
|
+
pointerColor: '#eaf2ff',
|
|
667
|
+
trailMode: 'streak',
|
|
668
|
+
trailLength: 1.2,
|
|
669
|
+
trailOpacity: 0.7,
|
|
670
|
+
trailColor: '#9fb8ff',
|
|
671
|
+
trailShare: 0.6,
|
|
672
|
+
};
|
|
673
|
+
/** Knobs that rebuild the simulation: they change on commit, never through the live path. */
|
|
674
|
+
const STRUCTURAL = ['particleCount', 'seed', 'quality'];
|
|
675
|
+
/** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
|
|
676
|
+
const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.neonRain.map((p) => p.value));
|
|
677
|
+
const NeonRainParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
|
|
678
|
+
const rootRef = useRef(null);
|
|
679
|
+
useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
|
|
680
|
+
const s = size > 0 ? size : 1;
|
|
681
|
+
const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
|
|
682
|
+
// uniforms outlive the objects, so keyframe animations survive a rebuild
|
|
683
|
+
const uniforms = useMemo(() => createUniforms(), []);
|
|
684
|
+
// rebuilt only when a structural knob changes (trails rebuild inside apply())
|
|
685
|
+
const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.seed, cfg.quality]);
|
|
686
|
+
useEffect(() => () => instance.dispose(), [instance]);
|
|
687
|
+
useLayoutEffect(() => {
|
|
688
|
+
applyConfig(uniforms, cfg, s);
|
|
689
|
+
instance.apply(cfg);
|
|
690
|
+
}, [instance, uniforms, cfg, s]);
|
|
691
|
+
// Slider drags and keyframe animations are both overrides on the committed config.
|
|
692
|
+
// This is the one place they are merged and pushed, so they compose instead of
|
|
693
|
+
// overwriting each other; the animation wins, because it is the active driver.
|
|
694
|
+
const keyframes = animations?.particles?.animations;
|
|
695
|
+
const animOverride = useRef({});
|
|
696
|
+
const animFrom = useRef({});
|
|
697
|
+
const animStart = useRef(0);
|
|
698
|
+
// Editing a keyframe restarts its cycle rather than dropping the user mid-way.
|
|
699
|
+
useEffect(() => { animStart.current = performance.now(); animFrom.current = {}; }, [keyframes]);
|
|
700
|
+
const push = useCallback((overrides) => {
|
|
701
|
+
const merged = { ...cfg, ...overrides, ...animOverride.current };
|
|
702
|
+
const z = merged.size;
|
|
703
|
+
applyConfig(uniforms, merged, z && z > 0 ? z : s);
|
|
704
|
+
}, [cfg, uniforms, s]);
|
|
705
|
+
const live = useConfigLiveHandle(rootRef, id, 'particles', cfg, STRUCTURAL, push);
|
|
706
|
+
const pointer = usePointerField();
|
|
707
|
+
useFrame((state, delta) => {
|
|
708
|
+
instance.update(Math.min(delta, 0.1), state.camera, pointer);
|
|
709
|
+
if (keyframes?.length) {
|
|
710
|
+
applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
|
|
711
|
+
push(live.current);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
|
|
715
|
+
};
|
|
716
|
+
export default NeonRainParticles;
|