@3driseai/3drise-viewer 0.1.16 → 0.1.18
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 +4 -3
- package/dist/components/ChladniGrid.d.ts +15 -0
- package/dist/components/ChladniGrid.d.ts.map +1 -0
- package/dist/components/ChladniGrid.js +62 -0
- package/dist/components/CustomPrimitive.d.ts +2 -0
- package/dist/components/CustomPrimitive.d.ts.map +1 -1
- package/dist/components/CustomPrimitive.js +2 -2
- package/dist/components/EffectsGenerator.d.ts.map +1 -1
- package/dist/components/EffectsGenerator.js +26 -2
- package/dist/components/FlowNetGrid.d.ts +15 -0
- package/dist/components/FlowNetGrid.d.ts.map +1 -0
- package/dist/components/FlowNetGrid.js +61 -0
- package/dist/components/Galaxy.d.ts.map +1 -1
- package/dist/components/Galaxy.js +2 -0
- package/dist/components/GirihGrid.d.ts +15 -0
- package/dist/components/GirihGrid.d.ts.map +1 -0
- package/dist/components/GirihGrid.js +62 -0
- package/dist/components/GravWaveGrid.d.ts +15 -0
- package/dist/components/GravWaveGrid.d.ts.map +1 -0
- package/dist/components/GravWaveGrid.js +62 -0
- package/dist/components/GridsGenerator.d.ts +2 -2
- package/dist/components/GridsGenerator.d.ts.map +1 -1
- package/dist/components/GridsGenerator.js +21 -0
- package/dist/components/HyperbolicGrid.d.ts +15 -0
- package/dist/components/HyperbolicGrid.d.ts.map +1 -0
- package/dist/components/HyperbolicGrid.js +61 -0
- package/dist/components/LayoutInstances.d.ts +13 -0
- package/dist/components/LayoutInstances.d.ts.map +1 -0
- package/dist/components/LayoutInstances.js +39 -0
- package/dist/components/ModelPrimitive.d.ts +2 -0
- package/dist/components/ModelPrimitive.d.ts.map +1 -1
- package/dist/components/ModelPrimitive.js +3 -3
- package/dist/components/ObjectNode.d.ts.map +1 -1
- package/dist/components/ObjectNode.js +44 -9
- package/dist/components/ParticlesGenerator.d.ts +10 -0
- package/dist/components/ParticlesGenerator.d.ts.map +1 -0
- package/dist/components/ParticlesGenerator.js +23 -0
- package/dist/components/ReactionDiffusionGrid.d.ts +15 -0
- package/dist/components/ReactionDiffusionGrid.d.ts.map +1 -0
- package/dist/components/ReactionDiffusionGrid.js +376 -0
- package/dist/components/WallpaperGrid.d.ts +15 -0
- package/dist/components/WallpaperGrid.d.ts.map +1 -0
- package/dist/components/WallpaperGrid.js +555 -0
- package/dist/components/cursor/CursorController.d.ts.map +1 -1
- package/dist/components/effects/BlackHoleEffect.d.ts +3 -3
- package/dist/components/effects/BlackHoleEffect.d.ts.map +1 -1
- package/dist/components/effects/BlackHoleEffect.js +293 -163
- package/dist/components/effects/DandelionSeedsEffect.d.ts +13 -0
- package/dist/components/effects/DandelionSeedsEffect.d.ts.map +1 -0
- package/dist/components/effects/DandelionSeedsEffect.js +510 -0
- package/dist/components/effects/FallingLeavesEffect.d.ts +13 -0
- package/dist/components/effects/FallingLeavesEffect.d.ts.map +1 -0
- package/dist/components/effects/FallingLeavesEffect.js +446 -0
- package/dist/components/effects/FieldLinesEffect.d.ts +11 -0
- package/dist/components/effects/FieldLinesEffect.d.ts.map +1 -0
- package/dist/components/effects/FieldLinesEffect.js +260 -0
- package/dist/components/effects/FireflySwarmEffect.d.ts +13 -0
- package/dist/components/effects/FireflySwarmEffect.d.ts.map +1 -0
- package/dist/components/effects/FireflySwarmEffect.js +408 -0
- package/dist/components/effects/GalaxyEffect.d.ts +13 -0
- package/dist/components/effects/GalaxyEffect.d.ts.map +1 -0
- package/dist/components/effects/GalaxyEffect.js +374 -0
- package/dist/components/effects/HarmonicShellEffect.d.ts +11 -0
- package/dist/components/effects/HarmonicShellEffect.d.ts.map +1 -0
- package/dist/components/effects/HarmonicShellEffect.js +209 -0
- package/dist/components/effects/IceCrystalsEffect.d.ts +3 -3
- package/dist/components/effects/IceCrystalsEffect.d.ts.map +1 -1
- package/dist/components/effects/IceCrystalsEffect.js +286 -166
- package/dist/components/effects/IceHaloEffect.d.ts +13 -0
- package/dist/components/effects/IceHaloEffect.d.ts.map +1 -0
- package/dist/components/effects/IceHaloEffect.js +306 -0
- package/dist/components/effects/LichtenbergEffect.d.ts +13 -0
- package/dist/components/effects/LichtenbergEffect.d.ts.map +1 -0
- package/dist/components/effects/LichtenbergEffect.js +517 -0
- package/dist/components/effects/MoleculesEffect.d.ts +3 -3
- package/dist/components/effects/MoleculesEffect.d.ts.map +1 -1
- package/dist/components/effects/MoleculesEffect.js +322 -164
- package/dist/components/effects/MurmurationEffect.d.ts +13 -0
- package/dist/components/effects/MurmurationEffect.d.ts.map +1 -0
- package/dist/components/effects/MurmurationEffect.js +539 -0
- package/dist/components/effects/PlanetaryRingsEffect.d.ts +13 -0
- package/dist/components/effects/PlanetaryRingsEffect.d.ts.map +1 -0
- package/dist/components/effects/PlanetaryRingsEffect.js +458 -0
- package/dist/components/effects/PortalEffect.d.ts +3 -3
- package/dist/components/effects/PortalEffect.d.ts.map +1 -1
- package/dist/components/effects/PortalEffect.js +240 -163
- package/dist/components/effects/ShockwaveEffect.d.ts +5 -3
- package/dist/components/effects/ShockwaveEffect.d.ts.map +1 -1
- package/dist/components/effects/ShockwaveEffect.js +538 -142
- package/dist/components/effects/SmokePlumeEffect.d.ts +3 -3
- package/dist/components/effects/SmokePlumeEffect.d.ts.map +1 -1
- package/dist/components/effects/SmokePlumeEffect.js +180 -116
- package/dist/components/effects/SmokeRingEffect.d.ts +3 -3
- package/dist/components/effects/SmokeRingEffect.d.ts.map +1 -1
- package/dist/components/effects/SmokeRingEffect.js +260 -135
- package/dist/components/effects/StrangeAttractorEffect.d.ts +11 -0
- package/dist/components/effects/StrangeAttractorEffect.d.ts.map +1 -0
- package/dist/components/effects/StrangeAttractorEffect.js +229 -0
- package/dist/components/effects/VolumetricFogEffect.d.ts +5 -3
- package/dist/components/effects/VolumetricFogEffect.d.ts.map +1 -1
- package/dist/components/effects/VolumetricFogEffect.js +211 -93
- package/dist/components/effects/WaterCausticsEffect.d.ts +13 -0
- package/dist/components/effects/WaterCausticsEffect.d.ts.map +1 -0
- package/dist/components/effects/WaterCausticsEffect.js +308 -0
- package/dist/components/effects/index.d.ts +9 -0
- package/dist/components/effects/index.d.ts.map +1 -1
- package/dist/components/effects/index.js +9 -0
- package/dist/components/index.d.ts +19 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +10 -0
- package/dist/components/particles/BioluminescentWakeParticles.d.ts +12 -0
- package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -0
- package/dist/components/particles/BioluminescentWakeParticles.js +1123 -0
- package/dist/components/particles/BrownianDustParticles.d.ts +12 -0
- package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -0
- package/dist/components/particles/BrownianDustParticles.js +757 -0
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts +12 -0
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -0
- package/dist/components/particles/ElectronOrbitalsParticles.js +944 -0
- package/dist/components/particles/GrinderSparksParticles.d.ts +12 -0
- package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -0
- package/dist/components/particles/GrinderSparksParticles.js +744 -0
- package/dist/components/particles/TidalStreamParticles.d.ts +12 -0
- package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -0
- package/dist/components/particles/TidalStreamParticles.js +788 -0
- package/dist/components/particles/index.d.ts +11 -0
- package/dist/components/particles/index.d.ts.map +1 -0
- package/dist/components/particles/index.js +6 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/hooks/useModelObject.d.ts +6 -1
- package/dist/hooks/useModelObject.d.ts.map +1 -1
- package/dist/hooks/useModelObject.js +19 -6
- package/dist/types/gridConfigs.d.ts +98 -0
- package/dist/types/gridConfigs.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/particleTrails.d.ts +70 -0
- package/dist/utils/particleTrails.d.ts.map +1 -0
- package/dist/utils/particleTrails.js +369 -0
- package/dist/utils/pointerField.d.ts +28 -0
- package/dist/utils/pointerField.d.ts.map +1 -0
- package/dist/utils/pointerField.js +103 -0
- package/package.json +2 -2
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import * as THREE from 'three';
|
|
4
|
+
import { useFrame } from '@react-three/fiber';
|
|
5
|
+
import { applyGenerativeAnimations } from '../../utils/handleGenerativeEffectsAnimations.js';
|
|
6
|
+
import { createPointerUniforms, stepPointerField, usePointerField } from '../../utils/pointerField.js';
|
|
7
|
+
import { createTrailUniforms, applyTrailConfig, createCpuTrail } from '../../utils/particleTrails.js';
|
|
8
|
+
import { useObjectReady } from '../../hooks/useObjectReady.js';
|
|
9
|
+
/**
|
|
10
|
+
* Grinder Sparks — Slow-motion grinder sparks: drag-limited ballistics, radiative cooling on the blackbody scale, carbon bursts, floor bounces and reflections; the cursor moves the grinder, aims the spray or sweeps the air. Use for industrial and cinematic scenes.
|
|
11
|
+
*/
|
|
12
|
+
// Grinder Sparks — an angle grinder biting steel, filmed in slow motion.
|
|
13
|
+
// Movement: each spark is a burning steel grain thrown tangentially off the wheel rim. Quadratic
|
|
14
|
+
// air drag a = -k |v| v with k = 3 rho_air C_d / (4 rho_steel d) (small grains stop sooner),
|
|
15
|
+
// gravity, and floor bounces with restitution and skid friction. Carbon in the steel burns
|
|
16
|
+
// to CO inside the grain and bursts it: the spark forks into a star of short, brighter
|
|
17
|
+
// sparklets that can fork again — the "spark test" that tells steels apart (`carbon`).
|
|
18
|
+
// Colour: temperature on the Planck locus. While a grain burns, oxidation holds it near its
|
|
19
|
+
// launch temperature; after burn-out it cools by radiation (eps sigma T^4) and convection
|
|
20
|
+
// (Nu = 2), sliding from white-yellow to orange to cherry red; visible radiance
|
|
21
|
+
// ~ exp(-c2 / (lambda T)) sets brightness, so it fades out around 1000 K. The floor is lit
|
|
22
|
+
// by the contact point (cos / d^2) and mirrors the sparks; the cut glows and cools behind
|
|
23
|
+
// the wheel.
|
|
24
|
+
// Mouse (pointerMode) — the cursor moves the grinder or the sparks; nothing is drawn at the cursor:
|
|
25
|
+
// wheel — the grinder follows the cursor: the contact point moves along a camera-facing plane,
|
|
26
|
+
// the spark rate follows Preston's law (removal ~ pressure x speed), and the cut behind
|
|
27
|
+
// it glows and cools.
|
|
28
|
+
// aim — the grinder stays put and turns: the spray fan swings toward the cursor, and pushing
|
|
29
|
+
// harder (a faster swing) throws more sparks.
|
|
30
|
+
// sweep — the cursor's motion stirs the air: a moving-body potential flow plus a trailing wake
|
|
31
|
+
// (in the slowed-down physics time) drags the sparks into swirls, and burning grains in the
|
|
32
|
+
// wake get extra oxygen and flare hotter.
|
|
33
|
+
// Trails (trailMode): streak = long exposure (the classic grinder photo), comet = tracer arcs,
|
|
34
|
+
// echo = smoulder. Trails take the spark's temperature (TRAIL_K) so they cool with it.
|
|
35
|
+
//
|
|
36
|
+
// Generated from the approved lab module (particle lab, batch 2). Sim units = object-local / size.
|
|
37
|
+
/** mulberry32 — seeded PRNG, returns a function giving floats in [0,1). */
|
|
38
|
+
function mulberry32(seed) {
|
|
39
|
+
let a = seed >>> 0;
|
|
40
|
+
return function () {
|
|
41
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
42
|
+
let t = a;
|
|
43
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
44
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
45
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Standard normal from a uniform PRNG (Box–Muller, one value per call). */
|
|
49
|
+
function gauss(rng) {
|
|
50
|
+
const u = Math.max(rng(), 1e-9);
|
|
51
|
+
const v = rng();
|
|
52
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(6.283185307 * v);
|
|
53
|
+
}
|
|
54
|
+
/** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
|
|
55
|
+
function attachViewH(obj, u) {
|
|
56
|
+
const scratch = new THREE.Vector2();
|
|
57
|
+
obj.onBeforeRender = (renderer) => {
|
|
58
|
+
renderer.getDrawingBufferSize(scratch);
|
|
59
|
+
u.uViewH.value = scratch.y;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function colorUniform(hex) {
|
|
63
|
+
return { value: new THREE.Color(hex) };
|
|
64
|
+
}
|
|
65
|
+
/** Tanner Helland blackbody fit, t in kelvin -> linear-ish RGB. */
|
|
66
|
+
const BLACKBODY_GLSL = /* glsl */ `
|
|
67
|
+
vec3 blackbody(float tK) {
|
|
68
|
+
float t = clamp(tK, 1000.0, 40000.0) / 100.0;
|
|
69
|
+
float r = t <= 66.0 ? 1.0 : clamp(1.29293618606 * pow(t - 60.0, -0.1332047592), 0.0, 1.0);
|
|
70
|
+
float g = t <= 66.0 ? clamp(0.39008157876 * log(t) - 0.63184144378, 0.0, 1.0)
|
|
71
|
+
: clamp(1.12989086089 * pow(t - 60.0, -0.0755148492), 0.0, 1.0);
|
|
72
|
+
float b = t >= 66.0 ? 1.0 : (t <= 19.0 ? 0.0 : clamp(0.54320678911 * log(t - 10.0) - 1.19625408914, 0.0, 1.0));
|
|
73
|
+
return pow(vec3(r, g, b), vec3(2.2));
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
/** Quality levels scale counts, raymarch samples and trail caps. Structural. */
|
|
77
|
+
const QUALITY_SCALE = { low: 0.5, medium: 1, high: 1.6 };
|
|
78
|
+
function qualityScale(q) { return QUALITY_SCALE[q] || 1; }
|
|
79
|
+
/** Mix the object's own base seed with the `seed` knob into a mulberry32 seed. */
|
|
80
|
+
function seedMix(base, seed) {
|
|
81
|
+
let h = (base ^ Math.imul((Math.round(seed) | 0) + 0x9e37, 0x85ebca6b)) >>> 0;
|
|
82
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
|
|
83
|
+
return (h ^ (h >>> 16)) >>> 0;
|
|
84
|
+
}
|
|
85
|
+
/** Fixed-step clock: the same motion at 30, 60 or 144 Hz. `run(d, step)` advances d sim-seconds
|
|
86
|
+
* in steps of exactly H (at most maxSteps per call; a longer backlog is dropped, not caught up). */
|
|
87
|
+
function createFixedStep(H, maxSteps) {
|
|
88
|
+
let acc = 0;
|
|
89
|
+
return {
|
|
90
|
+
H,
|
|
91
|
+
run(d, step) {
|
|
92
|
+
acc += Math.max(d, 0);
|
|
93
|
+
let k = 0;
|
|
94
|
+
while (acc >= H && k < maxSteps) {
|
|
95
|
+
step(H);
|
|
96
|
+
acc -= H;
|
|
97
|
+
k++;
|
|
98
|
+
}
|
|
99
|
+
if (k === maxSteps)
|
|
100
|
+
acc = Math.min(acc, H);
|
|
101
|
+
return acc / H; // leftover fraction, for render interpolation
|
|
102
|
+
},
|
|
103
|
+
reset() { acc = 0; },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Selection bounds: an invisible sphere the editor can pick, frame and size (never rendered,
|
|
107
|
+
* still raycastable and counted by Box3.setFromObject). */
|
|
108
|
+
function createPickProxy(center, radius) {
|
|
109
|
+
const geo = new THREE.SphereGeometry(1, 16, 10);
|
|
110
|
+
const mat = new THREE.MeshBasicMaterial({ visible: false });
|
|
111
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
112
|
+
mesh.name = 'pickProxy';
|
|
113
|
+
mesh.position.set(center[0], center[1], center[2]);
|
|
114
|
+
mesh.scale.setScalar(radius);
|
|
115
|
+
return { mesh, dispose() { geo.dispose(); mat.dispose(); } };
|
|
116
|
+
}
|
|
117
|
+
const _frustum = new THREE.Frustum();
|
|
118
|
+
const _pm = new THREE.Matrix4();
|
|
119
|
+
const _sphere = new THREE.Sphere();
|
|
120
|
+
/** Off-screen test: false when the object's bounding sphere is outside the camera frustum. */
|
|
121
|
+
function inView(object, camera, center, radius, size) {
|
|
122
|
+
if (!camera || !camera.projectionMatrix)
|
|
123
|
+
return true;
|
|
124
|
+
object.updateWorldMatrix(true, false);
|
|
125
|
+
_sphere.center.set(center[0], center[1], center[2]).multiplyScalar(size).applyMatrix4(object.matrixWorld);
|
|
126
|
+
_sphere.radius = radius * size * object.matrixWorld.getMaxScaleOnAxis();
|
|
127
|
+
_pm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
128
|
+
_frustum.setFromProjectionMatrix(_pm);
|
|
129
|
+
return _frustum.intersectsSphere(_sphere);
|
|
130
|
+
}
|
|
131
|
+
const POINTER_MODES = ['wheel', 'aim', 'sweep'];
|
|
132
|
+
const SLOWMO = 0.2; // physics seconds per shown second (a high-speed camera)
|
|
133
|
+
const H_STEP = 1 / 120; // shown-time step (physics step = H_STEP * SLOWMO)
|
|
134
|
+
const TR = 0.006; // physics seconds per trail unit (the shutter) for streaks
|
|
135
|
+
const TR_MODE = { streak: TR, comet: 0.03, echo: 0.04 }; // arcs and smoulder look back further
|
|
136
|
+
const C0 = [-0.8, 0.95, 0]; // contact point at rest (m)
|
|
137
|
+
const SPRAY_MID = [0.1, 0.6, 0];
|
|
138
|
+
const BOUNDS = { center: [0.2, 0.7, 0], radius: 2.3 };
|
|
139
|
+
const RHO_C = 7800 * 600; // steel density x heat capacity
|
|
140
|
+
const SIGMA = 5.67e-8, EPS = 0.8, T_AIR = 300;
|
|
141
|
+
const EL0 = -0.26; // rest elevation of the fan (rad)
|
|
142
|
+
const COOL = 0.22; // effective cooling scale (grains keep oxide shells hot)
|
|
143
|
+
const NK = 96; // cut heat-trail points
|
|
144
|
+
function createUniforms() {
|
|
145
|
+
return {
|
|
146
|
+
uTime: { value: 0 },
|
|
147
|
+
uSize: { value: 1 },
|
|
148
|
+
uViewH: { value: 800 },
|
|
149
|
+
uIntensity: { value: 1 },
|
|
150
|
+
uSpeed: { value: 1 },
|
|
151
|
+
uOpacity: { value: 0.9 },
|
|
152
|
+
uColor: colorUniform('#ffffff'),
|
|
153
|
+
uParticleCount: { value: 0 },
|
|
154
|
+
uSparkRate: { value: 1 },
|
|
155
|
+
uWheelSpeed: { value: 45 },
|
|
156
|
+
uSprayAngle: { value: 14 },
|
|
157
|
+
uCarbon: { value: 0.5 },
|
|
158
|
+
uTemperature: { value: 2000 },
|
|
159
|
+
uExposure: { value: 1 },
|
|
160
|
+
uFloorReflect: { value: 0.35 },
|
|
161
|
+
uEmberColor: colorUniform('#ffffff'),
|
|
162
|
+
uPointerMode: { value: 0 },
|
|
163
|
+
uPointerStrength: { value: 1 },
|
|
164
|
+
uPointerRadius: { value: 1 },
|
|
165
|
+
uPointerColor: colorUniform('#ffffff'),
|
|
166
|
+
uContact: { value: new THREE.Vector4(C0[0], C0[1], C0[2], 0) }, // xyz, glow
|
|
167
|
+
...createPointerUniforms(),
|
|
168
|
+
...createTrailUniforms(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function applyConfig(u, c, size) {
|
|
172
|
+
u.uSize.value = size;
|
|
173
|
+
u.uIntensity.value = c.intensity;
|
|
174
|
+
u.uSpeed.value = c.speed;
|
|
175
|
+
u.uOpacity.value = c.opacity;
|
|
176
|
+
u.uColor.value.set(c.color);
|
|
177
|
+
u.uParticleCount.value = c.particleCount;
|
|
178
|
+
u.uSparkRate.value = c.sparkRate;
|
|
179
|
+
u.uWheelSpeed.value = c.wheelSpeed;
|
|
180
|
+
u.uSprayAngle.value = c.sprayAngle;
|
|
181
|
+
u.uCarbon.value = c.carbon;
|
|
182
|
+
u.uTemperature.value = c.temperature;
|
|
183
|
+
u.uExposure.value = c.exposure;
|
|
184
|
+
u.uFloorReflect.value = c.floorReflect;
|
|
185
|
+
u.uEmberColor.value.set(c.emberColor);
|
|
186
|
+
u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
|
|
187
|
+
u.uPointerStrength.value = c.pointerStrength;
|
|
188
|
+
u.uPointerRadius.value = c.pointerRadius;
|
|
189
|
+
u.uPointerColor.value.set(c.pointerColor);
|
|
190
|
+
applyTrailConfig(u, c);
|
|
191
|
+
}
|
|
192
|
+
const BASE_U = 'uniform float uSize, uViewH, uIntensity, uOpacity;';
|
|
193
|
+
// Spark light: blackbody colour, visible-band radiance ~ exp(-c / T) normalised at 2000 K.
|
|
194
|
+
const HEAT_GLSL = /* glsl */ `
|
|
195
|
+
${BLACKBODY_GLSL}
|
|
196
|
+
uniform vec3 uColor, uEmberColor;
|
|
197
|
+
uniform float uExposure;
|
|
198
|
+
vec3 heatLight(float T) {
|
|
199
|
+
if (T < 600.0) return vec3(0.0);
|
|
200
|
+
float b = exp(-5500.0 / uExposure * (1.0 / T - 1.0 / 2000.0));
|
|
201
|
+
vec3 c = blackbody(T);
|
|
202
|
+
c /= max(max(c.r, c.g), max(c.b, 1e-3));
|
|
203
|
+
c = mix(c, c * uColor, smoothstep(1700.0, 2300.0, T)); // hottest sparks take the tint
|
|
204
|
+
c = mix(c, uEmberColor, 0.55 * smoothstep(1350.0, 950.0, T)); // dull red embers take the ember tint
|
|
205
|
+
// film response: bright sparks saturate toward white like an overexposed photo
|
|
206
|
+
vec3 E = c * b * 2.4;
|
|
207
|
+
return (vec3(1.0) - exp(-E)) * (1.0 + 0.6 * smoothstep(1.2, 3.0, b));
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
function sparkVertex(mirror) {
|
|
211
|
+
return /* glsl */ `
|
|
212
|
+
${BASE_U}
|
|
213
|
+
${HEAT_GLSL}
|
|
214
|
+
uniform float uFloorReflect;
|
|
215
|
+
attribute float aT;
|
|
216
|
+
attribute float aD;
|
|
217
|
+
varying vec3 vCol;
|
|
218
|
+
varying float vA;
|
|
219
|
+
void main() {
|
|
220
|
+
vec3 p = position;
|
|
221
|
+
${mirror ? 'p.y = -p.y;' : ''}
|
|
222
|
+
vec3 L = heatLight(aT);
|
|
223
|
+
vec4 mv = modelViewMatrix * vec4(p * uSize, 1.0);
|
|
224
|
+
float lum = dot(L, vec3(0.3, 0.5, 0.2));
|
|
225
|
+
float px = uSize * uViewH * (0.0045 + 0.004 * aD) * (1.0 + 0.8 * min(lum, 2.0)) / -mv.z;
|
|
226
|
+
${mirror ? 'px *= 1.6; float fade = uFloorReflect * 0.45 * exp(-position.y * 1.2) * smoothstep(3.0, 1.5, length(position.xz - vec2(0.4, 0.0)));' : 'float fade = 1.0;'}
|
|
227
|
+
vCol = L * uIntensity;
|
|
228
|
+
vA = uOpacity * fade * step(600.0, aT);
|
|
229
|
+
gl_PointSize = clamp(px, 1.0, 40.0) * step(0.001, vA);
|
|
230
|
+
gl_Position = projectionMatrix * mv;
|
|
231
|
+
}
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
const sparkFragment = /* glsl */ `
|
|
235
|
+
varying vec3 vCol;
|
|
236
|
+
varying float vA;
|
|
237
|
+
void main() {
|
|
238
|
+
vec2 d = gl_PointCoord * 2.0 - 1.0;
|
|
239
|
+
float r2 = dot(d, d);
|
|
240
|
+
if (r2 > 1.0) discard;
|
|
241
|
+
float a = (exp(-r2 * 8.0) + 0.3 * exp(-r2 * 2.2)) * (1.0 - r2) * vA;
|
|
242
|
+
if (a < 0.002) discard;
|
|
243
|
+
gl_FragColor = vec4(vCol, a);
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
// Floor: light from the contact point (Lambert, cos / d^2) — the pool the sparks rain into.
|
|
247
|
+
const floorVertex = /* glsl */ `
|
|
248
|
+
uniform float uSize;
|
|
249
|
+
varying vec3 vP;
|
|
250
|
+
void main() {
|
|
251
|
+
vP = position;
|
|
252
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position * uSize, 1.0);
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
const floorFragment = /* glsl */ `
|
|
256
|
+
${BASE_U}
|
|
257
|
+
${BLACKBODY_GLSL}
|
|
258
|
+
uniform vec4 uContact;
|
|
259
|
+
uniform vec3 uColor;
|
|
260
|
+
varying vec3 vP;
|
|
261
|
+
void main() {
|
|
262
|
+
vec3 d = uContact.xyz - vP;
|
|
263
|
+
float r2 = dot(d, d);
|
|
264
|
+
float E = uContact.w * max(d.y, 0.0) / pow(r2 + 0.02, 1.5);
|
|
265
|
+
vec3 c = blackbody(2100.0); c /= max(max(c.r, c.g), c.b);
|
|
266
|
+
float edge = smoothstep(2.8, 1.8, length(vP.xz - vec2(0.3, 0.0)));
|
|
267
|
+
vec3 rgb = c * uColor * E * 0.14 * uIntensity * edge;
|
|
268
|
+
float I = max(max(rgb.r, rgb.g), rgb.b);
|
|
269
|
+
if (I < 0.001) discard;
|
|
270
|
+
I = min(I, 1.0);
|
|
271
|
+
gl_FragColor = vec4(rgb / max(I, 1e-4), I * uOpacity);
|
|
272
|
+
}
|
|
273
|
+
`;
|
|
274
|
+
// Glyphs: the contact glow and the cooling cut behind the wheel (nothing is drawn at the cursor).
|
|
275
|
+
const glyphVertex = /* glsl */ `
|
|
276
|
+
${BASE_U}
|
|
277
|
+
${HEAT_GLSL}
|
|
278
|
+
attribute vec3 aG; // kind, temperature, strength
|
|
279
|
+
varying vec3 vCol;
|
|
280
|
+
varying float vA;
|
|
281
|
+
varying float vKind;
|
|
282
|
+
void main() {
|
|
283
|
+
vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
|
|
284
|
+
float kind = aG.x;
|
|
285
|
+
float s;
|
|
286
|
+
if (kind < 0.5) { s = 0.05 + 0.05 * aG.z; vCol = heatLight(2300.0); } // contact glow
|
|
287
|
+
else { s = 0.018; vCol = heatLight(aG.y); } // the cooling cut
|
|
288
|
+
vA = aG.z * uOpacity;
|
|
289
|
+
vKind = kind;
|
|
290
|
+
vCol *= uIntensity;
|
|
291
|
+
gl_PointSize = clamp(uSize * uViewH * s / -mv.z, 0.0, 200.0) * step(0.001, vA);
|
|
292
|
+
gl_Position = projectionMatrix * mv;
|
|
293
|
+
}
|
|
294
|
+
`;
|
|
295
|
+
const glyphFragment = /* glsl */ `
|
|
296
|
+
varying vec3 vCol;
|
|
297
|
+
varying float vA;
|
|
298
|
+
varying float vKind;
|
|
299
|
+
void main() {
|
|
300
|
+
vec2 d = gl_PointCoord * 2.0 - 1.0;
|
|
301
|
+
float r2 = dot(d, d);
|
|
302
|
+
if (r2 > 1.0) discard;
|
|
303
|
+
float a = vKind < 0.5 ? exp(-r2 * 40.0) * 1.5 + exp(-r2 * 5.0) * 0.25 : exp(-r2 * 6.0) * 0.9;
|
|
304
|
+
a *= (1.0 - r2) * vA;
|
|
305
|
+
if (a < 0.003) discard;
|
|
306
|
+
gl_FragColor = vec4(vCol, a);
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
function build(u, c, size) {
|
|
310
|
+
const q = qualityScale(c.quality);
|
|
311
|
+
const n = Math.max(64, Math.round(c.particleCount * q));
|
|
312
|
+
const rng = mulberry32(seedMix(0x5a4c, c.seed));
|
|
313
|
+
const pos = new Float32Array(n * 3), vel = new Float32Array(n * 3);
|
|
314
|
+
let trail = null, trailKey = '', trailTR = TR;
|
|
315
|
+
const T = new Float32Array(n); // temperature (K); 0 = not glowing
|
|
316
|
+
const Dm = new Float32Array(n); // grain diameter (m)
|
|
317
|
+
const aD = new Float32Array(n); // size seed for rendering
|
|
318
|
+
const burn = new Float32Array(n); // remaining burn time (physics s)
|
|
319
|
+
const burst = new Float32Array(n); // physics age of the carbon burst (1e9 = none)
|
|
320
|
+
const age = new Float32Array(n);
|
|
321
|
+
const gen = new Uint8Array(n);
|
|
322
|
+
const st = new Uint8Array(n); // 0 free, 1 flying, 2 holding (dead)
|
|
323
|
+
const hold = new Float32Array(n);
|
|
324
|
+
const free = new Int32Array(n);
|
|
325
|
+
let nFree = n;
|
|
326
|
+
for (let i = 0; i < n; i++)
|
|
327
|
+
free[i] = n - 1 - i;
|
|
328
|
+
// contact point (moves in wheel mode), cut heat trail
|
|
329
|
+
const cp = new THREE.Vector3(...C0);
|
|
330
|
+
const KP = new Float32Array(NK * 3), KB = new Float32Array(NK);
|
|
331
|
+
for (let k = 0; k < NK; k++)
|
|
332
|
+
KB[k] = -1e9;
|
|
333
|
+
let kHead = 0, kClock = 0;
|
|
334
|
+
const kLast = new THREE.Vector3(...C0);
|
|
335
|
+
let emitAcc = 0, glow = 1, physT = 0, strokeRate = 0;
|
|
336
|
+
const hp = new THREE.Vector3(), hv = new THREE.Vector3();
|
|
337
|
+
let aimAz = 0, aimEl = EL0;
|
|
338
|
+
function spawn(i, x, y, z, vx, vy, vz, T0, d, g) {
|
|
339
|
+
st[i] = 1;
|
|
340
|
+
age[i] = 0;
|
|
341
|
+
gen[i] = g;
|
|
342
|
+
pos[i * 3] = x;
|
|
343
|
+
pos[i * 3 + 1] = y;
|
|
344
|
+
pos[i * 3 + 2] = z;
|
|
345
|
+
vel[i * 3] = vx;
|
|
346
|
+
vel[i * 3 + 1] = vy;
|
|
347
|
+
vel[i * 3 + 2] = vz;
|
|
348
|
+
T[i] = T0;
|
|
349
|
+
Dm[i] = d;
|
|
350
|
+
// burn time grows with grain size (diffusion-limited oxidation)
|
|
351
|
+
burn[i] = g === 0 ? 0.22 * Math.pow(d / 1.2e-4, 1.5) * (0.7 + 0.6 * rng()) : 0.012 + 0.03 * rng();
|
|
352
|
+
const pBurst = g === 0 ? 0.85 * u.uCarbon.value : g === 1 ? 0.35 * u.uCarbon.value : 0;
|
|
353
|
+
burst[i] = rng() < pBurst ? burn[i] * (0.35 + 0.5 * rng()) : 1e9;
|
|
354
|
+
aD[i] = Math.min(d / 2.5e-4, 1.5);
|
|
355
|
+
if (trail)
|
|
356
|
+
trail.reset(i);
|
|
357
|
+
}
|
|
358
|
+
function step(hShown) {
|
|
359
|
+
const h = hShown * SLOWMO; // physics seconds
|
|
360
|
+
physT += h;
|
|
361
|
+
const mode = u.uPointerMode.value;
|
|
362
|
+
const S = u.uPointerStrength.value, act = u.uPointerActive.value, Rr = u.uPointerRadius.value;
|
|
363
|
+
hp.copy(u.uPointer.value);
|
|
364
|
+
hv.copy(u.uPointerVel.value);
|
|
365
|
+
const useWheel = mode === 0 && S > 0.01;
|
|
366
|
+
const useAim = mode === 1 && S > 0.01;
|
|
367
|
+
const useSweep = mode === 2 && S > 0.01 && act > 0.01;
|
|
368
|
+
// ---- contact point and spark rate (Preston: removal ~ pressure x relative speed) ----
|
|
369
|
+
let target = C0;
|
|
370
|
+
if (useWheel && act > 0.05)
|
|
371
|
+
target = [hp.x, Math.min(Math.max(hp.y, 0.25), 1.6), hp.z];
|
|
372
|
+
const lagK = Math.min(1, hShown * 10);
|
|
373
|
+
cp.x += (target[0] - cp.x) * lagK;
|
|
374
|
+
cp.y += (target[1] - cp.y) * lagK;
|
|
375
|
+
cp.z += (target[2] - cp.z) * lagK;
|
|
376
|
+
const stroke = useWheel || useAim ? hv.length() * act : 0;
|
|
377
|
+
strokeRate += (stroke - strokeRate) * Math.min(1, hShown * 4);
|
|
378
|
+
const pressure = useWheel || useAim ? 0.6 + 0.5 * S : 0.8;
|
|
379
|
+
// aim: the fan direction swings toward the cursor (azimuth about y, elevation), back to rest when idle
|
|
380
|
+
let tEl = EL0, tAz = 0;
|
|
381
|
+
if (useAim && act > 0.05) {
|
|
382
|
+
const dx = hp.x - cp.x, dy = hp.y - cp.y, dz = hp.z - cp.z;
|
|
383
|
+
const dl = Math.hypot(dx, dy, dz);
|
|
384
|
+
if (dl > 0.05) {
|
|
385
|
+
tEl = Math.max(-1.2, Math.min(0.9, Math.asin(dy / dl)));
|
|
386
|
+
tAz = Math.atan2(dz, dx);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
let dAz = tAz - aimAz;
|
|
390
|
+
while (dAz > Math.PI)
|
|
391
|
+
dAz -= 2 * Math.PI;
|
|
392
|
+
while (dAz < -Math.PI)
|
|
393
|
+
dAz += 2 * Math.PI;
|
|
394
|
+
const aimK = Math.min(1, hShown * 5);
|
|
395
|
+
aimAz += dAz * aimK;
|
|
396
|
+
aimEl += (tEl - aimEl) * aimK;
|
|
397
|
+
const rate = 5000 * u.uSparkRate.value * pressure * (1 + 1.2 * strokeRate / (strokeRate + 0.6));
|
|
398
|
+
glow += (rate / 5000 - glow) * Math.min(1, hShown * 6);
|
|
399
|
+
u.uContact.value.set(cp.x, cp.y, cp.z, glow);
|
|
400
|
+
// cut heat trail: a point every ~1 cm of contact travel
|
|
401
|
+
kClock += hShown;
|
|
402
|
+
if (cp.distanceTo(kLast) > 0.012 || kClock > 0.5) {
|
|
403
|
+
KP[kHead * 3] = cp.x;
|
|
404
|
+
KP[kHead * 3 + 1] = cp.y;
|
|
405
|
+
KP[kHead * 3 + 2] = cp.z;
|
|
406
|
+
KB[kHead] = physT;
|
|
407
|
+
kHead = (kHead + 1) % NK;
|
|
408
|
+
kLast.copy(cp);
|
|
409
|
+
kClock = 0;
|
|
410
|
+
}
|
|
411
|
+
// ---- emission: tangential fan off the rim (the wheel turns in the x-y plane) ----
|
|
412
|
+
emitAcc += rate * h;
|
|
413
|
+
const vRim = u.uWheelSpeed.value;
|
|
414
|
+
const spread = (u.uSprayAngle.value * Math.PI) / 180;
|
|
415
|
+
const T0 = u.uTemperature.value;
|
|
416
|
+
while (emitAcc >= 1 && nFree > 0) {
|
|
417
|
+
emitAcc -= 1;
|
|
418
|
+
const i = free[--nFree];
|
|
419
|
+
const ang = aimEl + spread * gauss(rng) * 0.6; // the fan spreads in the wheel plane
|
|
420
|
+
const yaw = aimAz + 0.05 * gauss(rng); // thin across it
|
|
421
|
+
const sp = vRim * (0.55 + 0.45 * rng());
|
|
422
|
+
const d = 1.2e-4 * Math.exp(0.45 * gauss(rng));
|
|
423
|
+
const ca = Math.cos(ang), sa = Math.sin(ang);
|
|
424
|
+
spawn(i, cp.x + 0.004 * gauss(rng), cp.y + 0.004 * gauss(rng), cp.z + 0.004 * gauss(rng), sp * ca * Math.cos(yaw), sp * sa, sp * ca * Math.sin(yaw), T0 * (0.96 + 0.08 * rng()), d, 0);
|
|
425
|
+
}
|
|
426
|
+
if (emitAcc > 1)
|
|
427
|
+
emitAcc = 1;
|
|
428
|
+
// ---- air sweep: the cursor is a moving body in the air (velocity in physics time) ----
|
|
429
|
+
const mx = hp.x, my = hp.y, mz = hp.z;
|
|
430
|
+
const RS = 0.2 * Rr;
|
|
431
|
+
const AIR = 2.2 * S * act / SLOWMO; // a fast sweep (a hand or a board fanning the air)
|
|
432
|
+
const bvx = hv.x * AIR, bvy = hv.y * AIR, bvz = hv.z * AIR;
|
|
433
|
+
const bsp = Math.hypot(bvx, bvy, bvz), bn = bsp > 1e-4 ? 1 / bsp : 0;
|
|
434
|
+
const sweepOn = useSweep && bsp > 0.05;
|
|
435
|
+
for (let i = 0; i < n; i++) {
|
|
436
|
+
const s = st[i];
|
|
437
|
+
if (s === 0)
|
|
438
|
+
continue;
|
|
439
|
+
if (s === 2) { // dead: park until the trails forget it
|
|
440
|
+
hold[i] -= h;
|
|
441
|
+
if (hold[i] <= 0) {
|
|
442
|
+
st[i] = 0;
|
|
443
|
+
free[nFree++] = i;
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const j = i * 3;
|
|
448
|
+
const d = Dm[i];
|
|
449
|
+
let t = T[i];
|
|
450
|
+
age[i] += h;
|
|
451
|
+
let x = pos[j], y = pos[j + 1], z = pos[j + 2];
|
|
452
|
+
let vx = vel[j], vy = vel[j + 1], vz = vel[j + 2];
|
|
453
|
+
// air: still, unless the sweep stirs it (moving-sphere dipole outside, carried inside, plus a wake)
|
|
454
|
+
let ax = 0, ay = 0, az = 0, flare = 0;
|
|
455
|
+
if (sweepOn) {
|
|
456
|
+
const qx = x - mx, qy = y - my, qz = z - mz;
|
|
457
|
+
const r2 = qx * qx + qy * qy + qz * qz;
|
|
458
|
+
if (r2 < 64 * RS * RS) {
|
|
459
|
+
const r = Math.sqrt(r2) + 1e-6;
|
|
460
|
+
if (r > RS) {
|
|
461
|
+
const k3 = (RS * RS * RS) / (2 * r2 * r);
|
|
462
|
+
const vr = (bvx * qx + bvy * qy + bvz * qz) / r;
|
|
463
|
+
ax = k3 * (3 * vr * qx / r - bvx);
|
|
464
|
+
ay = k3 * (3 * vr * qy / r - bvy);
|
|
465
|
+
az = k3 * (3 * vr * qz / r - bvz);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
ax = bvx;
|
|
469
|
+
ay = bvy;
|
|
470
|
+
az = bvz;
|
|
471
|
+
}
|
|
472
|
+
const behind = -(qx * bvx + qy * bvy + qz * bvz) * bn;
|
|
473
|
+
if (behind > 0) {
|
|
474
|
+
const perp2 = r2 - behind * behind;
|
|
475
|
+
const wk = Math.exp(-perp2 / (RS * RS * 1.5) - behind / (8 * RS));
|
|
476
|
+
ax += bvx * wk * 0.7;
|
|
477
|
+
ay += bvy * wk * 0.7;
|
|
478
|
+
az += bvz * wk * 0.7;
|
|
479
|
+
flare = wk; // fresh air feeds the burning grains
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// quadratic drag relative to the air, semi-implicit
|
|
484
|
+
const k = 1.8 / (31200 * d);
|
|
485
|
+
const rx = vx - ax, ry = vy - ay, rz = vz - az;
|
|
486
|
+
const rv = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
487
|
+
const damp = 1 / (1 + k * rv * h);
|
|
488
|
+
vx = ax + rx * damp;
|
|
489
|
+
vy = ay + ry * damp - 9.81 * h;
|
|
490
|
+
vz = az + rz * damp;
|
|
491
|
+
x += vx * h;
|
|
492
|
+
y += vy * h;
|
|
493
|
+
z += vz * h;
|
|
494
|
+
// floor: bounce with restitution, skid friction; conduction cools grains that touch it
|
|
495
|
+
let cool = 1;
|
|
496
|
+
if (y < 0) {
|
|
497
|
+
y = -y * 0.25;
|
|
498
|
+
vy = -vy * 0.25;
|
|
499
|
+
vx *= 0.5;
|
|
500
|
+
vz *= 0.5;
|
|
501
|
+
vz += (rng() - 0.5) * Math.abs(vx) * 0.4;
|
|
502
|
+
cool = 8;
|
|
503
|
+
}
|
|
504
|
+
else if (y < 0.01)
|
|
505
|
+
cool = 8; // lying on the floor: conduction drains it fast
|
|
506
|
+
// temperature: oxidation holds it hot while burning; then radiation + convection
|
|
507
|
+
if (burn[i] > 0) {
|
|
508
|
+
burn[i] -= h;
|
|
509
|
+
t += (T0 * 1.03 + 260 * flare - t) * Math.min(1, h / 0.02);
|
|
510
|
+
if (flare > 0.2)
|
|
511
|
+
burn[i] += h * flare * 0.8; // oxygen keeps it burning longer
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
const t2 = t * t;
|
|
515
|
+
t -= h * cool * COOL * (6 / (RHO_C * d)) * (EPS * SIGMA * (t2 * t2 - 8.1e9) + (0.052 / d) * (t - T_AIR));
|
|
516
|
+
}
|
|
517
|
+
// carbon burst: the grain forks into a star of sparklets
|
|
518
|
+
if (age[i] >= burst[i]) {
|
|
519
|
+
burst[i] = 1e9;
|
|
520
|
+
const kids = 3 + Math.floor(rng() * 4);
|
|
521
|
+
for (let q = 0; q < kids && nFree > 0; q++) {
|
|
522
|
+
const cI = free[--nFree];
|
|
523
|
+
let dx = gauss(rng), dy = gauss(rng), dz = gauss(rng);
|
|
524
|
+
const dl = Math.hypot(dx, dy, dz) || 1;
|
|
525
|
+
const sp = 2 + 4 * rng();
|
|
526
|
+
dx = dx / dl * sp;
|
|
527
|
+
dy = dy / dl * sp;
|
|
528
|
+
dz = dz / dl * sp;
|
|
529
|
+
spawn(cI, x, y, z, vx * 0.6 + dx, vy * 0.6 + dy, vz * 0.6 + dz, Math.max(t, T0) + 220, d * 0.45, gen[i] + 1);
|
|
530
|
+
}
|
|
531
|
+
Dm[i] = d * 0.75;
|
|
532
|
+
t += 120;
|
|
533
|
+
}
|
|
534
|
+
T[i] = t;
|
|
535
|
+
pos[j] = x;
|
|
536
|
+
pos[j + 1] = y;
|
|
537
|
+
pos[j + 2] = z;
|
|
538
|
+
vel[j] = vx;
|
|
539
|
+
vel[j + 1] = vy;
|
|
540
|
+
vel[j + 2] = vz;
|
|
541
|
+
if (t < 700 || age[i] > 6 || x > 3.5 || x < -2.5 || Math.abs(z) > 3 || y > 3.5) {
|
|
542
|
+
st[i] = 2;
|
|
543
|
+
hold[i] = 0.05;
|
|
544
|
+
T[i] = 0;
|
|
545
|
+
vel[j] = 0;
|
|
546
|
+
vel[j + 1] = 0;
|
|
547
|
+
vel[j + 2] = 0;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const stepper = createFixedStep(H_STEP, 6);
|
|
552
|
+
for (let s = 0; s < 240; s++)
|
|
553
|
+
step(H_STEP); // two shown seconds: the spray is established
|
|
554
|
+
// ---- geometry ----
|
|
555
|
+
const geo = new THREE.BufferGeometry();
|
|
556
|
+
const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
|
|
557
|
+
const tAttr = new THREE.BufferAttribute(T, 1).setUsage(THREE.DynamicDrawUsage);
|
|
558
|
+
const dAttr = new THREE.BufferAttribute(aD, 1).setUsage(THREE.DynamicDrawUsage);
|
|
559
|
+
geo.setAttribute('position', posAttr);
|
|
560
|
+
geo.setAttribute('aT', tAttr);
|
|
561
|
+
geo.setAttribute('aD', dAttr);
|
|
562
|
+
const mkSpark = (mirror) => new THREE.ShaderMaterial({
|
|
563
|
+
uniforms: u, vertexShader: sparkVertex(mirror), fragmentShader: sparkFragment,
|
|
564
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
565
|
+
});
|
|
566
|
+
const mat = mkSpark(false), rmat = mkSpark(true);
|
|
567
|
+
const sparks = new THREE.Points(geo, mat);
|
|
568
|
+
sparks.frustumCulled = false;
|
|
569
|
+
attachViewH(sparks, u);
|
|
570
|
+
const mirror = new THREE.Points(geo, rmat);
|
|
571
|
+
mirror.frustumCulled = false;
|
|
572
|
+
const fgeo = new THREE.CircleGeometry(2.8, 64);
|
|
573
|
+
fgeo.rotateX(-Math.PI / 2);
|
|
574
|
+
fgeo.translate(0.3, 0.002, 0);
|
|
575
|
+
const fmat = new THREE.ShaderMaterial({
|
|
576
|
+
uniforms: u, vertexShader: floorVertex, fragmentShader: floorFragment,
|
|
577
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
578
|
+
});
|
|
579
|
+
const floor = new THREE.Mesh(fgeo, fmat);
|
|
580
|
+
floor.frustumCulled = false;
|
|
581
|
+
const NG = 1 + NK;
|
|
582
|
+
const gp = new Float32Array(NG * 3), ga = new Float32Array(NG * 3);
|
|
583
|
+
const gpAttr = new THREE.BufferAttribute(gp, 3).setUsage(THREE.DynamicDrawUsage);
|
|
584
|
+
const gaAttr = new THREE.BufferAttribute(ga, 3).setUsage(THREE.DynamicDrawUsage);
|
|
585
|
+
const ggeo = new THREE.BufferGeometry();
|
|
586
|
+
ggeo.setAttribute('position', gpAttr);
|
|
587
|
+
ggeo.setAttribute('aG', gaAttr);
|
|
588
|
+
const gmat = new THREE.ShaderMaterial({
|
|
589
|
+
uniforms: u, vertexShader: glyphVertex, fragmentShader: glyphFragment,
|
|
590
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
591
|
+
});
|
|
592
|
+
const glyphs = new THREE.Points(ggeo, gmat);
|
|
593
|
+
glyphs.frustumCulled = false;
|
|
594
|
+
const object = new THREE.Group();
|
|
595
|
+
const proxy = createPickProxy(BOUNDS.center, BOUNDS.radius);
|
|
596
|
+
object.add(floor, mirror, sparks, glyphs, proxy.mesh);
|
|
597
|
+
const anchorWheel = new THREE.Vector3(...C0), anchorSpray = new THREE.Vector3(...SPRAY_MID);
|
|
598
|
+
function upload() {
|
|
599
|
+
posAttr.needsUpdate = true;
|
|
600
|
+
tAttr.needsUpdate = true;
|
|
601
|
+
dAttr.needsUpdate = true;
|
|
602
|
+
const mode = u.uPointerMode.value;
|
|
603
|
+
gp[0] = cp.x;
|
|
604
|
+
gp[1] = cp.y;
|
|
605
|
+
gp[2] = cp.z;
|
|
606
|
+
ga[0] = 0;
|
|
607
|
+
ga[1] = 2300;
|
|
608
|
+
ga[2] = Math.min(glow, 2.5);
|
|
609
|
+
for (let k = 0; k < NK; k++) {
|
|
610
|
+
const g = 1 + k;
|
|
611
|
+
gp[g * 3] = KP[k * 3];
|
|
612
|
+
gp[g * 3 + 1] = KP[k * 3 + 1];
|
|
613
|
+
gp[g * 3 + 2] = KP[k * 3 + 2];
|
|
614
|
+
const a = physT - KB[k];
|
|
615
|
+
const tk = T_AIR + (1350 - T_AIR) * Math.exp(-a / 0.35); // the cut cools behind the wheel
|
|
616
|
+
ga[g * 3] = 1;
|
|
617
|
+
ga[g * 3 + 1] = tk;
|
|
618
|
+
ga[g * 3 + 2] = a < 4 && mode === 0 ? 1 : 0;
|
|
619
|
+
}
|
|
620
|
+
gpAttr.needsUpdate = true;
|
|
621
|
+
gaAttr.needsUpdate = true;
|
|
622
|
+
}
|
|
623
|
+
// ---- trails: coloured by the spark's temperature ----
|
|
624
|
+
const trailGLSL = /* glsl */ `
|
|
625
|
+
${HEAT_GLSL}
|
|
626
|
+
vec3 trailShade(vec3 p, float f) { return heatLight(TRAIL_K) * 2.2; }
|
|
627
|
+
vec3 trailView(vec3 mv, out float mag) { mag = 1.0; return mv; }
|
|
628
|
+
`;
|
|
629
|
+
const materials = [mat, rmat, fmat, gmat];
|
|
630
|
+
trailKey = '';
|
|
631
|
+
trailTR = TR;
|
|
632
|
+
function buildTrails(mode, share) {
|
|
633
|
+
trailTR = TR_MODE[mode] || TR;
|
|
634
|
+
const key = mode + ':' + share;
|
|
635
|
+
if (key === trailKey)
|
|
636
|
+
return;
|
|
637
|
+
trailKey = key;
|
|
638
|
+
if (trail) {
|
|
639
|
+
object.remove(trail.object);
|
|
640
|
+
trail.dispose();
|
|
641
|
+
trail = null;
|
|
642
|
+
}
|
|
643
|
+
materials.length = 4;
|
|
644
|
+
trail = createCpuTrail(mode, { u, n, pos, vel, k: T, glsl: trailGLSL, sizePx: 0.0055, spacing: 0.004, sub: 12 });
|
|
645
|
+
if (!trail)
|
|
646
|
+
return;
|
|
647
|
+
object.add(trail.object);
|
|
648
|
+
materials.push(...trail.materials);
|
|
649
|
+
}
|
|
650
|
+
buildTrails(c.trailMode, c.trailShare);
|
|
651
|
+
upload();
|
|
652
|
+
return {
|
|
653
|
+
object,
|
|
654
|
+
materials,
|
|
655
|
+
apply(cfg) { if (cfg)
|
|
656
|
+
buildTrails(cfg.trailMode, cfg.trailShare); },
|
|
657
|
+
update(dt, camera, pointer) {
|
|
658
|
+
const rdt = Math.min(Math.max(dt, 0), 1 / 20);
|
|
659
|
+
if (camera && !inView(object, camera, BOUNDS.center, BOUNDS.radius, u.uSize.value))
|
|
660
|
+
return;
|
|
661
|
+
const d = rdt * u.uSpeed.value;
|
|
662
|
+
u.uTime.value += d;
|
|
663
|
+
if (camera && pointer) {
|
|
664
|
+
const anchor = u.uPointerMode.value === 0 ? anchorWheel : anchorSpray;
|
|
665
|
+
stepPointerField(u, pointer, camera, object, u.uSize.value, rdt, anchor);
|
|
666
|
+
}
|
|
667
|
+
u.uVelScale.value = trailTR; // trails cover trailTR x trailLength physics seconds
|
|
668
|
+
let simD = 0;
|
|
669
|
+
stepper.run(d, (hh) => { step(hh); simD += hh; });
|
|
670
|
+
if (simD > 0) {
|
|
671
|
+
upload();
|
|
672
|
+
if (trail)
|
|
673
|
+
trail.update((simD * SLOWMO) / trailTR);
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
seek(t) { for (let s = 0; s < Math.round(t / H_STEP); s++)
|
|
677
|
+
step(H_STEP); upload(); },
|
|
678
|
+
dispose() {
|
|
679
|
+
geo.dispose();
|
|
680
|
+
mat.dispose();
|
|
681
|
+
rmat.dispose();
|
|
682
|
+
fgeo.dispose();
|
|
683
|
+
fmat.dispose();
|
|
684
|
+
ggeo.dispose();
|
|
685
|
+
gmat.dispose();
|
|
686
|
+
proxy.dispose();
|
|
687
|
+
if (trail)
|
|
688
|
+
trail.dispose();
|
|
689
|
+
},
|
|
690
|
+
_state: { pos, vel, T },
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
|
|
694
|
+
const DEFAULTS = {
|
|
695
|
+
intensity: 1,
|
|
696
|
+
speed: 1,
|
|
697
|
+
opacity: 0.9,
|
|
698
|
+
size: 1,
|
|
699
|
+
color: '#fff1d6',
|
|
700
|
+
particleCount: 12000,
|
|
701
|
+
sparkRate: 1,
|
|
702
|
+
wheelSpeed: 35,
|
|
703
|
+
sprayAngle: 14,
|
|
704
|
+
carbon: 0.5,
|
|
705
|
+
temperature: 2000,
|
|
706
|
+
exposure: 1,
|
|
707
|
+
floorReflect: 0.35,
|
|
708
|
+
emberColor: '#ff3a12',
|
|
709
|
+
seed: 1,
|
|
710
|
+
quality: 'medium',
|
|
711
|
+
pointerMode: 'wheel',
|
|
712
|
+
pointerStrength: 1,
|
|
713
|
+
pointerRadius: 1,
|
|
714
|
+
pointerColor: '#ffd9a0',
|
|
715
|
+
trailMode: 'comet',
|
|
716
|
+
trailLength: 0.9,
|
|
717
|
+
trailOpacity: 0.9,
|
|
718
|
+
trailColor: '#ff7a2e',
|
|
719
|
+
trailShare: 1,
|
|
720
|
+
};
|
|
721
|
+
const GrinderSparksParticles = ({ size, config, animations, onObjectReady, onObjectDispose }) => {
|
|
722
|
+
const rootRef = useRef(null);
|
|
723
|
+
const animCache = useRef({});
|
|
724
|
+
useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
|
|
725
|
+
const s = size > 0 ? size : 1;
|
|
726
|
+
const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
|
|
727
|
+
// uniforms outlive the objects, so keyframe animations survive a rebuild
|
|
728
|
+
const uniforms = useMemo(() => createUniforms(), []);
|
|
729
|
+
// rebuilt only when a structural knob changes (trails rebuild inside apply())
|
|
730
|
+
const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.seed, cfg.quality]);
|
|
731
|
+
useEffect(() => () => instance.dispose(), [instance]);
|
|
732
|
+
useLayoutEffect(() => {
|
|
733
|
+
applyConfig(uniforms, cfg, s);
|
|
734
|
+
instance.apply(cfg);
|
|
735
|
+
}, [instance, uniforms, cfg, s]);
|
|
736
|
+
const pointer = usePointerField();
|
|
737
|
+
useFrame((state, delta) => {
|
|
738
|
+
instance.update(Math.min(delta, 0.1), state.camera, pointer);
|
|
739
|
+
if (animations?.length)
|
|
740
|
+
applyGenerativeAnimations(instance.materials, animations, animCache.current, Date.now(), false);
|
|
741
|
+
});
|
|
742
|
+
return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
|
|
743
|
+
};
|
|
744
|
+
export default GrinderSparksParticles;
|