@3driseai/3drise-viewer 0.1.17 → 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/dist/components/Galaxy.d.ts.map +1 -1
- package/dist/components/Galaxy.js +2 -0
- package/dist/components/ObjectNode.d.ts.map +1 -1
- package/dist/components/ObjectNode.js +9 -2
- 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/effects/ShockwaveEffect.js +2 -2
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -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/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,788 @@
|
|
|
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
|
+
* Tidal Stream — A spiral galaxy shredding dwarf satellites into tidal streams: symplectic orbits, density-wave arms, Doppler-shifted blackbody stars; the cursor is a subhalo, black hole or spectroscope. Use for space scenes.
|
|
11
|
+
*/
|
|
12
|
+
// Tidal Stream — a spiral galaxy cannibalising its dwarf satellites.
|
|
13
|
+
// Movement: (1) Host disc + bulge on circular orbits in the halo's rotation curve, with a
|
|
14
|
+
// Lin–Shu density wave: epicyclic displacement dR = eps R cos(psi) crowds stars into
|
|
15
|
+
// log-spiral arms that rotate at a fixed pattern speed. Analytic, in the vertex shader.
|
|
16
|
+
// (2) Up to three dwarf satellites on different orbits (long arcs, radial shells, a polar
|
|
17
|
+
// ring), each a Plummer cluster whose stars are integrated with kick-drift-kick leapfrog
|
|
18
|
+
// in halo + own-satellite potential and torn into leading/trailing tidal tails.
|
|
19
|
+
// Colour: every star is a blackbody. Disc: old stars 3.4–6.2 kK, young stars born at the arm
|
|
20
|
+
// crest that fade downstream, H-alpha knots at the crest, rotation Doppler. Streams: the
|
|
21
|
+
// observed temperature is Doppler-shifted, T_obs = T (1 + beta).
|
|
22
|
+
// Mouse (pointerMode):
|
|
23
|
+
// subhalo — a dark-matter clump: a softened point mass in the integration (sweeps carve
|
|
24
|
+
// stream gaps, fling dwarfs) + point-lens optics (Einstein ring behind the cursor).
|
|
25
|
+
// blackHole — a spinning black hole: weaker pull plus Lense–Thirring frame dragging (the
|
|
26
|
+
// velocity rotates about the spin axis, applied as an exact rotation), stars near it
|
|
27
|
+
// gravitationally redshifted and dimmed by sqrt(1 - rs/r), a lensed view and a
|
|
28
|
+
// Doppler-beamed photon ring.
|
|
29
|
+
// spectroscope — an instrument, not a mass: a loupe that magnifies the sky under the cursor
|
|
30
|
+
// (conformal-style map, x M at the centre, compressing into a rim) and multiplies
|
|
31
|
+
// the Doppler shift inside it, so hidden velocity structure pops out.
|
|
32
|
+
// Trails (trailMode): streak = long-exposure star trails, comet = history tails, echo = stardust
|
|
33
|
+
// afterglow. Stream stars use the shared CPU trails; disc stars trail analytically by
|
|
34
|
+
// evaluating their orbit at earlier times, so arcs are exact.
|
|
35
|
+
//
|
|
36
|
+
// Generated from the approved lab module (particle lab, round 3). 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
|
+
/** Soft sprite: gaussian core + wide halo, alpha-discarded at the rim. */
|
|
55
|
+
const SPRITE_GLSL = /* glsl */ `
|
|
56
|
+
float spriteAlpha(vec2 pc) {
|
|
57
|
+
vec2 d = pc * 2.0 - 1.0;
|
|
58
|
+
float r2 = dot(d, d);
|
|
59
|
+
if (r2 > 1.0) return 0.0;
|
|
60
|
+
float core = exp(-r2 * 9.0);
|
|
61
|
+
float halo = exp(-r2 * 3.2) * 0.35;
|
|
62
|
+
return (core + halo) * (1.0 - r2);
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
/** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
|
|
66
|
+
function attachViewH(obj, u) {
|
|
67
|
+
const scratch = new THREE.Vector2();
|
|
68
|
+
obj.onBeforeRender = (renderer) => {
|
|
69
|
+
renderer.getDrawingBufferSize(scratch);
|
|
70
|
+
u.uViewH.value = scratch.y;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function colorUniform(hex) {
|
|
74
|
+
return { value: new THREE.Color(hex) };
|
|
75
|
+
}
|
|
76
|
+
/** Tanner Helland blackbody fit, t in kelvin -> linear-ish RGB. */
|
|
77
|
+
const BLACKBODY_GLSL = /* glsl */ `
|
|
78
|
+
vec3 blackbody(float tK) {
|
|
79
|
+
float t = clamp(tK, 1000.0, 40000.0) / 100.0;
|
|
80
|
+
float r = t <= 66.0 ? 1.0 : clamp(1.29293618606 * pow(t - 60.0, -0.1332047592), 0.0, 1.0);
|
|
81
|
+
float g = t <= 66.0 ? clamp(0.39008157876 * log(t) - 0.63184144378, 0.0, 1.0)
|
|
82
|
+
: clamp(1.12989086089 * pow(t - 60.0, -0.0755148492), 0.0, 1.0);
|
|
83
|
+
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));
|
|
84
|
+
return pow(vec3(r, g, b), vec3(2.2));
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
const POINTER_MODES = ['subhalo', 'blackHole', 'spectroscope'];
|
|
88
|
+
const RC = 0.12; // halo core radius (v0 = 1)
|
|
89
|
+
const Q = 0.9; // vertical flattening of the halo
|
|
90
|
+
const PLUMMER_B = 0.03; // dwarf scale radius
|
|
91
|
+
const H_MAX = 0.008; // largest runtime leapfrog step
|
|
92
|
+
const H_WARM = 0.016; // pre-warm step (dwarf crossing time ~1.3 -> >80 steps per internal orbit)
|
|
93
|
+
const TIME_SCALE = 0.15; // sim time per second: a stream orbit takes ~40 s, the inner disc ~13 s
|
|
94
|
+
const PATTERN_SPEED = 1.1; // density-wave pattern speed (corotation just outside the disc)
|
|
95
|
+
const PITCH = 0.32; // tan(pitch angle) of the log spirals (~18 deg)
|
|
96
|
+
const WAVE_EPS = 0.07; // epicyclic amplitude of the density wave
|
|
97
|
+
const PERTURBER_MASS = 0.12;
|
|
98
|
+
const HOLE_MASS = 0.07;
|
|
99
|
+
const HOLE_SPIN = 0.018; // Lense–Thirring J (sim units) at strength 1
|
|
100
|
+
const DISC_TRAIL_SHARE = 0.35; // disc stars trail at this fraction of trailShare
|
|
101
|
+
// orbit families: [apocentre radius, inclination, node angle, eccentricity offset, mass factor]
|
|
102
|
+
const ORBITS = [
|
|
103
|
+
[1.05, 0.28, 0.0, 0.0, 1.0], // main dwarf: long arcs
|
|
104
|
+
[1.15, 1.05, 2.1, 0.42, 0.7], // near-radial: shells / umbrella
|
|
105
|
+
[0.85, 1.35, 4.0, -0.35, 0.8], // near-circular, polar ring
|
|
106
|
+
];
|
|
107
|
+
function createUniforms() {
|
|
108
|
+
return {
|
|
109
|
+
uTime: { value: 0 },
|
|
110
|
+
uSimT: { value: 0 },
|
|
111
|
+
uSize: { value: 1 },
|
|
112
|
+
uViewH: { value: 800 },
|
|
113
|
+
uIntensity: { value: 1 },
|
|
114
|
+
uSpeed: { value: 1 },
|
|
115
|
+
uOpacity: { value: 0.9 },
|
|
116
|
+
uColor: colorUniform('#ffffff'),
|
|
117
|
+
uParticleCount: { value: 0 },
|
|
118
|
+
uSatelliteCount: { value: 2 },
|
|
119
|
+
uSatelliteMass: { value: 0.0005 },
|
|
120
|
+
uOrbitEccentricity: { value: 0.5 },
|
|
121
|
+
uStreamAge: { value: 2.2 },
|
|
122
|
+
uDopplerScale: { value: 2 },
|
|
123
|
+
uRedshiftColor: colorUniform('#ffffff'),
|
|
124
|
+
uDiscStars: { value: 0 },
|
|
125
|
+
uArmCount: { value: 2 },
|
|
126
|
+
uNebulaColor: colorUniform('#ffffff'),
|
|
127
|
+
uStarSize: { value: 1 },
|
|
128
|
+
uPointerMode: { value: 0 },
|
|
129
|
+
uPointerStrength: { value: 1 },
|
|
130
|
+
uPointerRadius: { value: 1 },
|
|
131
|
+
uPointerColor: colorUniform('#ffffff'),
|
|
132
|
+
...createPointerUniforms(),
|
|
133
|
+
...createTrailUniforms(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function applyConfig(u, c, size) {
|
|
137
|
+
u.uSize.value = size;
|
|
138
|
+
u.uIntensity.value = c.intensity;
|
|
139
|
+
u.uSpeed.value = c.speed;
|
|
140
|
+
u.uOpacity.value = c.opacity;
|
|
141
|
+
u.uColor.value.set(c.color);
|
|
142
|
+
u.uParticleCount.value = c.particleCount;
|
|
143
|
+
u.uSatelliteCount.value = c.satelliteCount;
|
|
144
|
+
u.uSatelliteMass.value = c.satelliteMass;
|
|
145
|
+
u.uOrbitEccentricity.value = c.orbitEccentricity;
|
|
146
|
+
u.uStreamAge.value = c.streamAge;
|
|
147
|
+
u.uDopplerScale.value = c.dopplerScale;
|
|
148
|
+
u.uRedshiftColor.value.set(c.redshiftColor);
|
|
149
|
+
u.uDiscStars.value = c.discStars;
|
|
150
|
+
u.uArmCount.value = c.armCount;
|
|
151
|
+
u.uNebulaColor.value.set(c.nebulaColor);
|
|
152
|
+
u.uStarSize.value = c.starSize;
|
|
153
|
+
u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
|
|
154
|
+
u.uPointerStrength.value = c.pointerStrength;
|
|
155
|
+
u.uPointerRadius.value = c.pointerRadius;
|
|
156
|
+
u.uPointerColor.value.set(c.pointerColor);
|
|
157
|
+
applyTrailConfig(u, c);
|
|
158
|
+
}
|
|
159
|
+
// Cursor optics, shared by every material (needs uSize declared by the including shader).
|
|
160
|
+
const POINTER_GLSL = /* glsl */ `
|
|
161
|
+
uniform vec3 uPointer, uPointerColor;
|
|
162
|
+
uniform float uPointerActive, uPointerStrength, uPointerRadius, uPointerMode;
|
|
163
|
+
float pointerOn() { return uPointerActive * step(0.001, uPointerStrength); }
|
|
164
|
+
vec3 pointerViewPos() { return (viewMatrix * modelMatrix * vec4(uPointer * uSize, 1.0)).xyz; }
|
|
165
|
+
// 1 inside the spectroscope loupe (angular), 0 outside or in other modes
|
|
166
|
+
float loupeWeight(vec3 mv) {
|
|
167
|
+
if (uPointerMode < 1.5) return 0.0;
|
|
168
|
+
vec3 pv = pointerViewPos();
|
|
169
|
+
float R = 0.16 * uPointerRadius;
|
|
170
|
+
float b = length(mv.xy / -mv.z - pv.xy / -pv.z);
|
|
171
|
+
return pointerOn() * (1.0 - smoothstep(R * 0.8, R, b));
|
|
172
|
+
}
|
|
173
|
+
// gravitational redshift / dimming near the black hole: sqrt(1 - rs / r)
|
|
174
|
+
float horizonFactor(vec3 pSim) {
|
|
175
|
+
if (uPointerMode < 0.5 || uPointerMode > 1.5) return 1.0;
|
|
176
|
+
float rs = 0.05 * uPointerRadius * uPointerStrength * uPointerActive;
|
|
177
|
+
return sqrt(clamp(1.0 - rs / max(length(pSim - uPointer), 1e-4), 0.0, 1.0));
|
|
178
|
+
}
|
|
179
|
+
// View-space warp of a source: point-lens major image (subhalo, black hole) or loupe (spectroscope)
|
|
180
|
+
vec3 pointerView(vec3 mv, out float mag) {
|
|
181
|
+
mag = 1.0;
|
|
182
|
+
float act = pointerOn();
|
|
183
|
+
if (act < 0.001) return mv;
|
|
184
|
+
vec3 pv = pointerViewPos();
|
|
185
|
+
float Dl = -pv.z, Ds = -mv.z;
|
|
186
|
+
if (Dl <= 0.0) return mv;
|
|
187
|
+
vec2 c = pv.xy / Dl;
|
|
188
|
+
vec2 th = mv.xy / Ds - c;
|
|
189
|
+
float b = length(th);
|
|
190
|
+
vec2 dir = b > 1e-6 ? th / b : vec2(1.0, 0.0);
|
|
191
|
+
if (uPointerMode > 1.5) {
|
|
192
|
+
float R = 0.16 * uPointerRadius;
|
|
193
|
+
if (b >= R) return mv;
|
|
194
|
+
float M = 1.0 + 1.3 * min(uPointerStrength, 2.5) * act;
|
|
195
|
+
float x = b / R;
|
|
196
|
+
float bp = R * (M * x) / (1.0 + (M - 1.0) * x); // slope M at the centre, rim fixed
|
|
197
|
+
mag = 1.0 + 0.25 * (M - 1.0);
|
|
198
|
+
return vec3((c + dir * bp) * Ds, mv.z);
|
|
199
|
+
}
|
|
200
|
+
if (Ds <= Dl) return mv; // only sources behind the lens
|
|
201
|
+
float thetaE = (uPointerMode > 0.5 ? 0.06 : 0.085) * uPointerRadius * sqrt(max(uPointerStrength, 0.0)) * act;
|
|
202
|
+
thetaE *= sqrt(clamp((Ds - Dl) / Ds * 6.0, 0.0, 1.0));
|
|
203
|
+
float bp = 0.5 * (b + sqrt(b * b + 4.0 * thetaE * thetaE));
|
|
204
|
+
float uu = b / max(thetaE, 1e-6);
|
|
205
|
+
mag = clamp((uu * uu + 2.0) / (max(uu, 1e-3) * sqrt(uu * uu + 4.0)), 1.0, 5.0);
|
|
206
|
+
return vec3((c + dir * bp) * Ds, mv.z);
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
const streamVertex = /* glsl */ `
|
|
210
|
+
uniform float uSize, uViewH, uIntensity, uOpacity, uDopplerScale, uStarSize;
|
|
211
|
+
uniform vec3 uColor, uRedshiftColor;
|
|
212
|
+
attribute vec3 aVel;
|
|
213
|
+
attribute vec2 aSeed; // x: luminosity seed, y: temperature seed
|
|
214
|
+
varying vec3 vCol;
|
|
215
|
+
varying float vA;
|
|
216
|
+
varying float vSoft;
|
|
217
|
+
${BLACKBODY_GLSL}
|
|
218
|
+
${POINTER_GLSL}
|
|
219
|
+
void main() {
|
|
220
|
+
vec4 wp = modelMatrix * vec4(position * uSize, 1.0);
|
|
221
|
+
vec3 mv0 = (viewMatrix * wp).xyz;
|
|
222
|
+
vec3 toCam = normalize(cameraPosition - wp.xyz);
|
|
223
|
+
float vr = dot(mat3(modelMatrix) * aVel, toCam); // + approaching
|
|
224
|
+
float boost = 1.0 + 3.0 * loupeWeight(mv0); // spectroscope exaggerates Doppler
|
|
225
|
+
float beta = clamp(vr * uDopplerScale * 0.35 * boost, -0.75, 0.75);
|
|
226
|
+
float hz = horizonFactor(position);
|
|
227
|
+
float T = mix(3600.0, 7800.0, aSeed.y);
|
|
228
|
+
vec3 col = blackbody(T * (1.0 + beta) * hz);
|
|
229
|
+
float s = clamp(beta / 0.7, -1.0, 1.0);
|
|
230
|
+
col *= s > 0.0 ? mix(vec3(1.0), uColor * 1.4, s) : mix(vec3(1.0), uRedshiftColor * 1.4, -s);
|
|
231
|
+
float lum = (0.3 + 2.2 * pow(aSeed.x, 9.0)) * hz * hz;
|
|
232
|
+
float mag;
|
|
233
|
+
vec3 mv = pointerView(mv0, mag);
|
|
234
|
+
vCol = col * lum * uIntensity * mag;
|
|
235
|
+
vA = uOpacity;
|
|
236
|
+
vSoft = 0.0;
|
|
237
|
+
gl_PointSize = clamp(uStarSize * uSize * uViewH * 0.0095 * (0.7 + 1.0 * pow(aSeed.x, 5.0)) * sqrt(mag) / -mv.z, 1.0, 48.0);
|
|
238
|
+
gl_Position = projectionMatrix * vec4(mv, 1.0);
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
// Disc star at time t (analytic): position, blackbody colour, luminosity, softness, velocity.
|
|
242
|
+
const DISC_GLSL = /* glsl */ `
|
|
243
|
+
uniform float uArmCount;
|
|
244
|
+
uniform vec3 uNebulaColor;
|
|
245
|
+
attribute vec4 aOrb; // R0, theta0, height, luminosity seed
|
|
246
|
+
attribute vec4 aPop; // population (0 old, 1 young, 2 nebula, 3 bulge), temperature seed, arm offset, size seed
|
|
247
|
+
void discStar(float t, out vec3 p, out vec3 col, out float lum, out float soft, out vec3 vel) {
|
|
248
|
+
float pop = aPop.x;
|
|
249
|
+
float R0 = aOrb.x;
|
|
250
|
+
float vc = R0 / sqrt(R0 * R0 + ${(RC * RC).toFixed(4)});
|
|
251
|
+
float om = vc / max(R0, 1e-3);
|
|
252
|
+
float bulge = step(2.5, pop);
|
|
253
|
+
om *= mix(1.0, 0.35, bulge);
|
|
254
|
+
float th = aOrb.y + om * t;
|
|
255
|
+
float psi = uArmCount * (th - ${PATTERN_SPEED.toFixed(3)} * t) + log(max(R0, 0.02)) / ${PITCH.toFixed(3)};
|
|
256
|
+
float R = R0 * (1.0 + (1.0 - bulge) * ${WAVE_EPS.toFixed(3)} * cos(psi));
|
|
257
|
+
if (bulge > 0.5) {
|
|
258
|
+
float c = cos(om * t), s = sin(om * t);
|
|
259
|
+
vec3 q = vec3(R0 * cos(aOrb.y), aOrb.z, R0 * sin(aOrb.y));
|
|
260
|
+
p = vec3(c * q.x - s * q.z, q.y, s * q.x + c * q.z);
|
|
261
|
+
} else {
|
|
262
|
+
p = vec3(R * cos(th), aOrb.z, R * sin(th));
|
|
263
|
+
}
|
|
264
|
+
vel = (1.0 - bulge) * vc * vec3(-sin(th), 0.0, cos(th));
|
|
265
|
+
float since = fract((psi - aPop.z) / 6.2831853);
|
|
266
|
+
lum = 0.25 + 1.6 * pow(aOrb.w, 8.0);
|
|
267
|
+
soft = 0.0;
|
|
268
|
+
if (pop < 0.5) {
|
|
269
|
+
col = blackbody(mix(3400.0, 6200.0, aPop.y));
|
|
270
|
+
} else if (pop < 1.5) {
|
|
271
|
+
col = blackbody(mix(12000.0, 26000.0, aPop.y));
|
|
272
|
+
lum = (0.5 + 2.5 * pow(aOrb.w, 6.0)) * 2.2 * exp(-3.5 * since);
|
|
273
|
+
} else if (pop < 2.5) {
|
|
274
|
+
lum = 1.3 * exp(-14.0 * since);
|
|
275
|
+
col = uNebulaColor;
|
|
276
|
+
soft = 1.0;
|
|
277
|
+
} else {
|
|
278
|
+
col = blackbody(mix(3200.0, 5200.0, aPop.y));
|
|
279
|
+
lum *= 1.2;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
const discVertex = /* glsl */ `
|
|
284
|
+
uniform float uSize, uViewH, uIntensity, uOpacity, uDopplerScale, uStarSize, uSimT, uDiscStars;
|
|
285
|
+
uniform vec3 uColor, uRedshiftColor;
|
|
286
|
+
varying vec3 vCol;
|
|
287
|
+
varying float vA;
|
|
288
|
+
varying float vSoft;
|
|
289
|
+
${BLACKBODY_GLSL}
|
|
290
|
+
${POINTER_GLSL}
|
|
291
|
+
${DISC_GLSL}
|
|
292
|
+
void main() {
|
|
293
|
+
vec3 p, col, vel; float lum, soft;
|
|
294
|
+
discStar(uSimT, p, col, lum, soft, vel);
|
|
295
|
+
vec4 wp = modelMatrix * vec4(p * uSize, 1.0);
|
|
296
|
+
vec3 mv0 = (viewMatrix * wp).xyz;
|
|
297
|
+
float vr = dot(mat3(modelMatrix) * vel, normalize(cameraPosition - wp.xyz));
|
|
298
|
+
float sgn = clamp(vr * uDopplerScale * 0.25 * (1.0 + 3.0 * loupeWeight(mv0)), -1.0, 1.0);
|
|
299
|
+
col *= sgn > 0.0 ? mix(vec3(1.0), uColor * 1.3, sgn * 0.6) : mix(vec3(1.0), uRedshiftColor * 1.3, -sgn * 0.6);
|
|
300
|
+
float hz = horizonFactor(p);
|
|
301
|
+
float mag;
|
|
302
|
+
vec3 mv = pointerView(mv0, mag);
|
|
303
|
+
float crowd = clamp(70000.0 / max(uDiscStars, 1.0), 0.4, 3.0);
|
|
304
|
+
vSoft = soft;
|
|
305
|
+
vCol = col * lum * uIntensity * mag * hz * hz;
|
|
306
|
+
vA = uOpacity * mix(0.55, 0.22, soft) * crowd;
|
|
307
|
+
float px = mix(0.0075 * (0.7 + 0.8 * aPop.w), 0.05, soft);
|
|
308
|
+
gl_PointSize = clamp(uStarSize * uSize * uViewH * px * sqrt(mag) / -mv.z, 1.0, 64.0);
|
|
309
|
+
gl_Position = projectionMatrix * vec4(mv, 1.0);
|
|
310
|
+
}
|
|
311
|
+
`;
|
|
312
|
+
// Analytic disc trails: the same orbit evaluated at earlier times (template x = sample fraction).
|
|
313
|
+
function discTrailVertex(mode) {
|
|
314
|
+
const profile = {
|
|
315
|
+
streak: 'float tau = f * uTrailLength * uVelScale * 1.2; float fade = pow(1.0 - f, 1.2) * 0.8;',
|
|
316
|
+
comet: 'float tau = f * uTrailLength * uVelScale * 3.0; float fade = pow(1.0 - f, 1.8) * 0.5;',
|
|
317
|
+
echo: 'float tau = f * uTrailLength * uVelScale * 2.5; float fade = pow(1.0 - f, 2.0) * 0.8;',
|
|
318
|
+
}[mode];
|
|
319
|
+
return /* glsl */ `
|
|
320
|
+
uniform float uSize, uViewH, uIntensity, uOpacity, uStarSize, uSimT, uDiscStars;
|
|
321
|
+
uniform float uTrailLength, uTrailOpacity, uVelScale;
|
|
322
|
+
uniform vec3 uTrailColor;
|
|
323
|
+
varying vec3 vCol;
|
|
324
|
+
varying float vA;
|
|
325
|
+
varying float vSoft;
|
|
326
|
+
${BLACKBODY_GLSL}
|
|
327
|
+
${POINTER_GLSL}
|
|
328
|
+
${DISC_GLSL}
|
|
329
|
+
void main() {
|
|
330
|
+
float f = position.x;
|
|
331
|
+
${profile}
|
|
332
|
+
vec3 p, col, vel; float lum, soft;
|
|
333
|
+
discStar(uSimT - tau, p, col, lum, soft, vel);
|
|
334
|
+
float mag;
|
|
335
|
+
vec3 mv = pointerView((modelViewMatrix * vec4(p * uSize, 1.0)).xyz, mag);
|
|
336
|
+
vSoft = ${mode === 'echo' ? '0.6' : '0.0'};
|
|
337
|
+
vCol = col * mix(vec3(1.0), uTrailColor * 1.3, smoothstep(0.0, 0.8, f)) * lum * uIntensity * mag * horizonFactor(p);
|
|
338
|
+
vA = uOpacity * uTrailOpacity * fade * (1.0 - soft) * step(0.001, f);
|
|
339
|
+
gl_PointSize = clamp(uStarSize * uSize * uViewH * 0.006 * ${mode === 'echo' ? '(1.0 + 1.5 * f)' : '(1.0 - 0.5 * f)'} / -mv.z, 1.0, 32.0);
|
|
340
|
+
gl_Position = projectionMatrix * vec4(mv, 1.0);
|
|
341
|
+
}
|
|
342
|
+
`;
|
|
343
|
+
}
|
|
344
|
+
const fragment = /* glsl */ `
|
|
345
|
+
varying vec3 vCol;
|
|
346
|
+
varying float vA;
|
|
347
|
+
varying float vSoft;
|
|
348
|
+
${SPRITE_GLSL}
|
|
349
|
+
void main() {
|
|
350
|
+
vec2 d = gl_PointCoord * 2.0 - 1.0;
|
|
351
|
+
float r2 = dot(d, d);
|
|
352
|
+
float soft = exp(-r2 * 2.5) * (1.0 - r2);
|
|
353
|
+
float a = mix(spriteAlpha(gl_PointCoord), soft, vSoft) * vA;
|
|
354
|
+
if (a < 0.003 || r2 > 1.0) discard;
|
|
355
|
+
gl_FragColor = vec4(vCol, a);
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
// The cursor made visible: faint Einstein ring (subhalo), Doppler-beamed photon ring (black hole),
|
|
359
|
+
// reticle (spectroscope). Sized from the same angular radii the optics use.
|
|
360
|
+
const glyphVertex = /* glsl */ `
|
|
361
|
+
uniform float uSize, uViewH, uTime;
|
|
362
|
+
varying float vMode;
|
|
363
|
+
${POINTER_GLSL}
|
|
364
|
+
void main() {
|
|
365
|
+
vec4 mv = modelViewMatrix * vec4(uPointer * uSize, 1.0);
|
|
366
|
+
float ang = uPointerMode > 1.5 ? 0.16 * uPointerRadius
|
|
367
|
+
: (uPointerMode > 0.5 ? 0.06 : 0.085) * uPointerRadius * sqrt(max(uPointerStrength, 0.0)) * 0.55;
|
|
368
|
+
float px = ang * projectionMatrix[1][1] * uViewH * 0.5; // angular radius -> pixels
|
|
369
|
+
gl_PointSize = clamp(px * 2.0 * 1.35, 0.0, 900.0) * step(0.02, pointerOn());
|
|
370
|
+
vMode = uPointerMode;
|
|
371
|
+
gl_Position = projectionMatrix * mv;
|
|
372
|
+
}
|
|
373
|
+
`;
|
|
374
|
+
const glyphFragment = /* glsl */ `
|
|
375
|
+
uniform float uTime, uOpacity;
|
|
376
|
+
uniform vec3 uPointerColor;
|
|
377
|
+
uniform float uPointerActive, uPointerStrength;
|
|
378
|
+
varying float vMode;
|
|
379
|
+
void main() {
|
|
380
|
+
vec2 d = (gl_PointCoord * 2.0 - 1.0) * vec2(1.0, -1.0) * 1.35;
|
|
381
|
+
float r = length(d);
|
|
382
|
+
float a = 0.0;
|
|
383
|
+
vec3 col = uPointerColor;
|
|
384
|
+
if (vMode < 0.5) {
|
|
385
|
+
a = 0.12 * exp(-pow((r - 1.0) / 0.05, 2.0)); // faint Einstein ring
|
|
386
|
+
} else if (vMode < 1.5) {
|
|
387
|
+
// photon ring at ~1.0 plus a dim inner glow; the approaching side is Doppler-beamed
|
|
388
|
+
float ang = atan(d.y, d.x) + uTime * 1.2;
|
|
389
|
+
float beam = 0.55 + 0.45 * cos(ang);
|
|
390
|
+
a = (exp(-pow((r - 0.62) / 0.035, 2.0)) * (0.35 + 0.9 * beam * beam) + 0.12 * exp(-pow((r - 0.62) / 0.18, 2.0)));
|
|
391
|
+
col = mix(uPointerColor, vec3(1.0, 0.95, 0.9), 0.3 * beam);
|
|
392
|
+
} else {
|
|
393
|
+
float ring = exp(-pow((r - 1.0) / 0.012, 2.0));
|
|
394
|
+
float ticks = step(0.985, cos(atan(d.y, d.x) * 12.0)) * smoothstep(0.93, 0.97, r) * (1.0 - smoothstep(1.0, 1.04, r));
|
|
395
|
+
float cross = (1.0 - smoothstep(0.004, 0.012, abs(d.x))) * step(0.08, abs(d.y)) * step(abs(d.y), 0.18)
|
|
396
|
+
+ (1.0 - smoothstep(0.004, 0.012, abs(d.y))) * step(0.08, abs(d.x)) * step(abs(d.x), 0.18);
|
|
397
|
+
a = 0.45 * ring + 0.5 * ticks + 0.35 * cross;
|
|
398
|
+
}
|
|
399
|
+
a *= uPointerActive * uOpacity * min(uPointerStrength, 1.0);
|
|
400
|
+
if (a < 0.003 || r > 1.3) discard;
|
|
401
|
+
gl_FragColor = vec4(col, a);
|
|
402
|
+
}
|
|
403
|
+
`;
|
|
404
|
+
/** Halo acceleration (flattened cored log potential, v0 = 1). */
|
|
405
|
+
function hostAccel(x, y, z, out) {
|
|
406
|
+
const r2 = x * x + z * z + (y * y) / (Q * Q) + RC * RC;
|
|
407
|
+
out[0] = -x / r2;
|
|
408
|
+
out[1] = -y / (Q * Q * r2);
|
|
409
|
+
out[2] = -z / r2;
|
|
410
|
+
}
|
|
411
|
+
function build(u, c, size) {
|
|
412
|
+
const n = Math.max(1, Math.round(c.particleCount));
|
|
413
|
+
const ns = Math.min(3, Math.max(1, Math.round(c.satelliteCount)));
|
|
414
|
+
const rng = mulberry32(0x7d15a1);
|
|
415
|
+
// ---- satellites ----
|
|
416
|
+
const sat = new Float64Array(ns * 6);
|
|
417
|
+
const satMass = new Float64Array(ns);
|
|
418
|
+
for (let k = 0; k < ns; k++) {
|
|
419
|
+
const [ra, inc, node, eOff, mf] = ORBITS[k];
|
|
420
|
+
const e = Math.min(Math.max(c.orbitEccentricity + eOff, 0.02), 0.92);
|
|
421
|
+
const vc = ra / Math.sqrt(ra * ra + RC * RC);
|
|
422
|
+
const vt = vc * (1 - 0.66 * e);
|
|
423
|
+
const ci = Math.cos(inc), si = Math.sin(inc), cn = Math.cos(node), sn = Math.sin(node);
|
|
424
|
+
const vy = vt * si, vz = -vt * ci;
|
|
425
|
+
sat[k * 6] = cn * ra;
|
|
426
|
+
sat[k * 6 + 1] = 0;
|
|
427
|
+
sat[k * 6 + 2] = -sn * ra;
|
|
428
|
+
sat[k * 6 + 3] = sn * vz;
|
|
429
|
+
sat[k * 6 + 4] = vy;
|
|
430
|
+
sat[k * 6 + 5] = cn * vz;
|
|
431
|
+
satMass[k] = mf;
|
|
432
|
+
}
|
|
433
|
+
// ---- stream stars: Plummer spheres around their dwarf ----
|
|
434
|
+
const pos = new Float32Array(n * 3);
|
|
435
|
+
const vel = new Float32Array(n * 3);
|
|
436
|
+
const seed = new Float32Array(n * 2);
|
|
437
|
+
const owner = new Uint8Array(n);
|
|
438
|
+
for (let i = 0; i < n; i++) {
|
|
439
|
+
const k = i % ns;
|
|
440
|
+
owner[i] = k;
|
|
441
|
+
const m = u.uSatelliteMass.value * satMass[k];
|
|
442
|
+
const sigma = Math.sqrt(m / (6 * PLUMMER_B)) * 0.85;
|
|
443
|
+
let r;
|
|
444
|
+
do {
|
|
445
|
+
r = PLUMMER_B / Math.sqrt(Math.pow(Math.max(rng(), 1e-6), -2 / 3) - 1);
|
|
446
|
+
} while (r > 4 * PLUMMER_B);
|
|
447
|
+
const ct = 2 * rng() - 1, ph = 6.283185307 * rng(), st = Math.sqrt(1 - ct * ct);
|
|
448
|
+
pos[i * 3] = sat[k * 6] + r * st * Math.cos(ph);
|
|
449
|
+
pos[i * 3 + 1] = sat[k * 6 + 1] + r * ct;
|
|
450
|
+
pos[i * 3 + 2] = sat[k * 6 + 2] + r * st * Math.sin(ph);
|
|
451
|
+
vel[i * 3] = sat[k * 6 + 3] + sigma * gauss(rng);
|
|
452
|
+
vel[i * 3 + 1] = sat[k * 6 + 4] + sigma * gauss(rng);
|
|
453
|
+
vel[i * 3 + 2] = sat[k * 6 + 5] + sigma * gauss(rng);
|
|
454
|
+
seed[i * 2] = rng();
|
|
455
|
+
seed[i * 2 + 1] = rng();
|
|
456
|
+
}
|
|
457
|
+
const a = new Float64Array(3);
|
|
458
|
+
const sPrev = new Float64Array(ns * 3);
|
|
459
|
+
// Frame dragging as an exact rotation of v about the spin axis by angle 2 |Omega| dt, with
|
|
460
|
+
// Omega = J / (r^2 + eps^2)^(3/2) (gravitomagnetic field of a spinning mass, softened).
|
|
461
|
+
const rot = (j, ax, ay, az, ang, arr) => {
|
|
462
|
+
const vx = arr[j], vy = arr[j + 1], vz = arr[j + 2];
|
|
463
|
+
const cs = Math.cos(ang), sn = Math.sin(ang), dot = ax * vx + ay * vy + az * vz;
|
|
464
|
+
arr[j] = vx * cs + (ay * vz - az * vy) * sn + ax * dot * (1 - cs);
|
|
465
|
+
arr[j + 1] = vy * cs + (az * vx - ax * vz) * sn + ay * dot * (1 - cs);
|
|
466
|
+
arr[j + 2] = vz * cs + (ax * vy - ay * vx) * sn + az * dot * (1 - cs);
|
|
467
|
+
};
|
|
468
|
+
const sv = new Float64Array(3);
|
|
469
|
+
// P: pointer (sim units), mp: mass, ep2: softening^2, J: spin, (sx,sy,sz): spin axis
|
|
470
|
+
function step(h, mp, ep2, P, J, spin) {
|
|
471
|
+
const hh = h * 0.5;
|
|
472
|
+
const baseM = u.uSatelliteMass.value;
|
|
473
|
+
const usePtr = mp > 1e-6;
|
|
474
|
+
const useSpin = J > 1e-7;
|
|
475
|
+
const px = P.x, py = P.y, pz = P.z;
|
|
476
|
+
for (let k = 0; k < ns; k++) {
|
|
477
|
+
const o = k * 6;
|
|
478
|
+
sPrev[k * 3] = sat[o];
|
|
479
|
+
sPrev[k * 3 + 1] = sat[o + 1];
|
|
480
|
+
sPrev[k * 3 + 2] = sat[o + 2];
|
|
481
|
+
for (let half = 0; half < 2; half++) {
|
|
482
|
+
hostAccel(sat[o], sat[o + 1], sat[o + 2], a);
|
|
483
|
+
if (usePtr) {
|
|
484
|
+
const dx = sat[o] - px, dy = sat[o + 1] - py, dz = sat[o + 2] - pz;
|
|
485
|
+
const d2 = dx * dx + dy * dy + dz * dz + ep2, f = mp / (d2 * Math.sqrt(d2));
|
|
486
|
+
a[0] -= dx * f;
|
|
487
|
+
a[1] -= dy * f;
|
|
488
|
+
a[2] -= dz * f;
|
|
489
|
+
}
|
|
490
|
+
sat[o + 3] += a[0] * hh;
|
|
491
|
+
sat[o + 4] += a[1] * hh;
|
|
492
|
+
sat[o + 5] += a[2] * hh;
|
|
493
|
+
if (half === 0) {
|
|
494
|
+
sat[o] += sat[o + 3] * h;
|
|
495
|
+
sat[o + 1] += sat[o + 4] * h;
|
|
496
|
+
sat[o + 2] += sat[o + 5] * h;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const b2 = PLUMMER_B * PLUMMER_B;
|
|
501
|
+
for (let i = 0, j = 0; i < n; i++, j += 3) {
|
|
502
|
+
const k = owner[i];
|
|
503
|
+
const m = baseM * satMass[k];
|
|
504
|
+
let x = pos[j], y = pos[j + 1], z = pos[j + 2];
|
|
505
|
+
for (let half = 0; half < 2; half++) {
|
|
506
|
+
const sx = half === 0 ? sPrev[k * 3] : sat[k * 6];
|
|
507
|
+
const sy = half === 0 ? sPrev[k * 3 + 1] : sat[k * 6 + 1];
|
|
508
|
+
const sz = half === 0 ? sPrev[k * 3 + 2] : sat[k * 6 + 2];
|
|
509
|
+
const r2 = x * x + z * z + (y * y) / (Q * Q) + RC * RC;
|
|
510
|
+
let ax = -x / r2, ay = -y / (Q * Q * r2), az = -z / r2;
|
|
511
|
+
let dx = x - sx, dy = y - sy, dz = z - sz;
|
|
512
|
+
let d2 = dx * dx + dy * dy + dz * dz + b2, f = m / (d2 * Math.sqrt(d2));
|
|
513
|
+
ax -= dx * f;
|
|
514
|
+
ay -= dy * f;
|
|
515
|
+
az -= dz * f;
|
|
516
|
+
if (usePtr) {
|
|
517
|
+
dx = x - px;
|
|
518
|
+
dy = y - py;
|
|
519
|
+
dz = z - pz;
|
|
520
|
+
d2 = dx * dx + dy * dy + dz * dz + ep2;
|
|
521
|
+
f = mp / (d2 * Math.sqrt(d2));
|
|
522
|
+
ax -= dx * f;
|
|
523
|
+
ay -= dy * f;
|
|
524
|
+
az -= dz * f;
|
|
525
|
+
if (useSpin) {
|
|
526
|
+
const ang = Math.min((2 * J * hh) / (d2 * Math.sqrt(d2)), 0.5);
|
|
527
|
+
if (ang > 2e-5)
|
|
528
|
+
rot(j, spin[0], spin[1], spin[2], ang, vel); // negligible far away
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
vel[j] += ax * hh;
|
|
532
|
+
vel[j + 1] += ay * hh;
|
|
533
|
+
vel[j + 2] += az * hh;
|
|
534
|
+
if (half === 0) {
|
|
535
|
+
x += vel[j] * h;
|
|
536
|
+
y += vel[j + 1] * h;
|
|
537
|
+
z += vel[j + 2] * h;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
pos[j] = x;
|
|
541
|
+
pos[j + 1] = y;
|
|
542
|
+
pos[j + 2] = z;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const P0 = new THREE.Vector3();
|
|
546
|
+
const warmT = Math.max(0, c.streamAge) * 6.2832;
|
|
547
|
+
const warmSteps = Math.ceil(warmT / H_WARM);
|
|
548
|
+
for (let s = 0; s < warmSteps; s++)
|
|
549
|
+
step(warmT / warmSteps, 0, 1, P0, 0, sv);
|
|
550
|
+
let simT = warmT;
|
|
551
|
+
u.uSimT.value = simT;
|
|
552
|
+
// ---- stream points ----
|
|
553
|
+
const geo = new THREE.BufferGeometry();
|
|
554
|
+
const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
|
|
555
|
+
const velAttr = new THREE.BufferAttribute(vel, 3).setUsage(THREE.DynamicDrawUsage);
|
|
556
|
+
geo.setAttribute('position', posAttr);
|
|
557
|
+
geo.setAttribute('aVel', velAttr);
|
|
558
|
+
geo.setAttribute('aSeed', new THREE.BufferAttribute(seed, 2));
|
|
559
|
+
const mat = new THREE.ShaderMaterial({
|
|
560
|
+
uniforms: u, vertexShader: streamVertex, fragmentShader: fragment,
|
|
561
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
562
|
+
});
|
|
563
|
+
const streams = new THREE.Points(geo, mat);
|
|
564
|
+
streams.frustumCulled = false;
|
|
565
|
+
attachViewH(streams, u);
|
|
566
|
+
// ---- host galaxy (analytic) ----
|
|
567
|
+
const nd = Math.max(0, Math.round(c.discStars));
|
|
568
|
+
const drng = mulberry32(0x9a1a);
|
|
569
|
+
const orb = new Float32Array(Math.max(nd, 1) * 4);
|
|
570
|
+
const popA = new Float32Array(Math.max(nd, 1) * 4);
|
|
571
|
+
for (let i = 0; i < nd; i++) {
|
|
572
|
+
const t = drng();
|
|
573
|
+
let pop, R, z;
|
|
574
|
+
if (t < 0.16) {
|
|
575
|
+
pop = 3; // Hernquist bulge, a = 0.06, sampled by its cumulative mass
|
|
576
|
+
const m = drng() * 0.96;
|
|
577
|
+
let r = (0.06 * Math.sqrt(m)) / (1 - Math.sqrt(m));
|
|
578
|
+
r = Math.min(r, 0.35);
|
|
579
|
+
z = (drng() * 2 - 1) * r * 0.75;
|
|
580
|
+
R = Math.sqrt(Math.max(r * r - z * z, 0));
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
pop = t < 0.6 ? 0 : t < 0.97 ? 1 : 2;
|
|
584
|
+
do {
|
|
585
|
+
R = -0.18 * Math.log(Math.max(drng() * drng(), 1e-9));
|
|
586
|
+
} while (R > 0.8);
|
|
587
|
+
R = Math.max(R, 0.03);
|
|
588
|
+
const hz = pop === 0 ? 0.022 : 0.008;
|
|
589
|
+
z = hz * Math.log(Math.max(drng(), 1e-6)) * (drng() < 0.5 ? -1 : 1) * 0.5;
|
|
590
|
+
}
|
|
591
|
+
orb[i * 4] = R;
|
|
592
|
+
orb[i * 4 + 1] = drng() * 6.2831853;
|
|
593
|
+
orb[i * 4 + 2] = z;
|
|
594
|
+
orb[i * 4 + 3] = drng();
|
|
595
|
+
popA[i * 4] = pop;
|
|
596
|
+
popA[i * 4 + 1] = drng();
|
|
597
|
+
popA[i * 4 + 2] = (drng() - 0.5) * 0.8;
|
|
598
|
+
popA[i * 4 + 3] = drng();
|
|
599
|
+
}
|
|
600
|
+
const dgeo = new THREE.BufferGeometry();
|
|
601
|
+
dgeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(Math.max(nd, 1) * 3), 3));
|
|
602
|
+
dgeo.setAttribute('aOrb', new THREE.BufferAttribute(orb, 4));
|
|
603
|
+
dgeo.setAttribute('aPop', new THREE.BufferAttribute(popA, 4));
|
|
604
|
+
dgeo.setDrawRange(0, nd);
|
|
605
|
+
const dmat = new THREE.ShaderMaterial({
|
|
606
|
+
uniforms: u, vertexShader: discVertex, fragmentShader: fragment,
|
|
607
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
608
|
+
});
|
|
609
|
+
const disc = new THREE.Points(dgeo, dmat);
|
|
610
|
+
disc.frustumCulled = false;
|
|
611
|
+
// ---- cursor glyph ----
|
|
612
|
+
const ggeo = new THREE.BufferGeometry();
|
|
613
|
+
ggeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(3), 3));
|
|
614
|
+
const gmat = new THREE.ShaderMaterial({
|
|
615
|
+
uniforms: u, vertexShader: glyphVertex, fragmentShader: glyphFragment,
|
|
616
|
+
transparent: true, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending,
|
|
617
|
+
});
|
|
618
|
+
const glyph = new THREE.Points(ggeo, gmat);
|
|
619
|
+
glyph.frustumCulled = false;
|
|
620
|
+
glyph.renderOrder = 10;
|
|
621
|
+
const object = new THREE.Group();
|
|
622
|
+
object.add(disc, streams, glyph);
|
|
623
|
+
// ---- trails (rebuilt only when trailMode / trailShare change) ----
|
|
624
|
+
const trailGLSL = /* glsl */ `
|
|
625
|
+
${POINTER_GLSL}
|
|
626
|
+
vec3 trailShade(vec3 p, float f) { float hz = horizonFactor(p); return vec3(1.0, 0.95, 0.9) * 0.9 * hz * hz; }
|
|
627
|
+
vec3 trailView(vec3 mv, out float mag) { return pointerView(mv, mag); }
|
|
628
|
+
`;
|
|
629
|
+
let trail = null, discTrail = null, discTrailMat = null, discTrailGeo = null;
|
|
630
|
+
let trailKey = '';
|
|
631
|
+
const materials = [mat, dmat, gmat];
|
|
632
|
+
function buildTrails(mode, share) {
|
|
633
|
+
const key = mode + ':' + share;
|
|
634
|
+
if (key === trailKey)
|
|
635
|
+
return;
|
|
636
|
+
trailKey = key;
|
|
637
|
+
if (trail) {
|
|
638
|
+
object.remove(trail.object);
|
|
639
|
+
trail.dispose();
|
|
640
|
+
trail = null;
|
|
641
|
+
}
|
|
642
|
+
if (discTrail) {
|
|
643
|
+
object.remove(discTrail);
|
|
644
|
+
discTrailGeo.dispose();
|
|
645
|
+
discTrailMat.dispose();
|
|
646
|
+
discTrail = null;
|
|
647
|
+
}
|
|
648
|
+
materials.length = 3;
|
|
649
|
+
if (mode === 'none')
|
|
650
|
+
return;
|
|
651
|
+
trail = createCpuTrail(mode, { u, n, pos, vel, glsl: trailGLSL, sizePx: 0.0065, spacing: 0.0035 });
|
|
652
|
+
if (!trail)
|
|
653
|
+
return;
|
|
654
|
+
object.add(trail.object);
|
|
655
|
+
materials.push(...trail.materials);
|
|
656
|
+
if (nd > 0) {
|
|
657
|
+
const S = mode === 'echo' ? 6 : mode === 'comet' ? 24 : 16;
|
|
658
|
+
discTrailGeo = new THREE.InstancedBufferGeometry();
|
|
659
|
+
const tpl = new Float32Array(S * 3);
|
|
660
|
+
for (let i = 0; i < S; i++)
|
|
661
|
+
tpl[i * 3] = i / (S - 1);
|
|
662
|
+
discTrailGeo.setAttribute('position', new THREE.BufferAttribute(tpl, 3));
|
|
663
|
+
discTrailGeo.setAttribute('aOrb', new THREE.InstancedBufferAttribute(orb, 4));
|
|
664
|
+
discTrailGeo.setAttribute('aPop', new THREE.InstancedBufferAttribute(popA, 4));
|
|
665
|
+
discTrailGeo.instanceCount = Math.max(1, Math.round(nd * Math.min(share, 1) * DISC_TRAIL_SHARE));
|
|
666
|
+
discTrailMat = new THREE.ShaderMaterial({
|
|
667
|
+
uniforms: u, vertexShader: discTrailVertex(mode), fragmentShader: fragment,
|
|
668
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
669
|
+
});
|
|
670
|
+
discTrail = new THREE.Points(discTrailGeo, discTrailMat);
|
|
671
|
+
discTrail.frustumCulled = false;
|
|
672
|
+
object.add(discTrail);
|
|
673
|
+
materials.push(discTrailMat);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
buildTrails(c.trailMode, c.trailShare);
|
|
677
|
+
const P = new THREE.Vector3();
|
|
678
|
+
const spinAxis = new THREE.Vector3();
|
|
679
|
+
const invQ = new THREE.Quaternion();
|
|
680
|
+
return {
|
|
681
|
+
object,
|
|
682
|
+
materials,
|
|
683
|
+
apply(cfg) { if (cfg)
|
|
684
|
+
buildTrails(cfg.trailMode, cfg.trailShare); },
|
|
685
|
+
update(dt, camera, pointer) {
|
|
686
|
+
const rdt = Math.min(Math.max(dt, 0), 1 / 20);
|
|
687
|
+
const d = rdt * u.uSpeed.value;
|
|
688
|
+
u.uTime.value += d;
|
|
689
|
+
u.uVelScale.value = TIME_SCALE * u.uSpeed.value;
|
|
690
|
+
if (pointer && camera)
|
|
691
|
+
stepPointerField(u, pointer, camera, object, u.uSize.value, rdt);
|
|
692
|
+
const T = d * TIME_SCALE;
|
|
693
|
+
if (T > 0) {
|
|
694
|
+
const mode = u.uPointerMode.value;
|
|
695
|
+
const act = u.uPointerActive.value * u.uPointerStrength.value;
|
|
696
|
+
const mp = mode === 0 ? PERTURBER_MASS * act : mode === 1 ? HOLE_MASS * act : 0;
|
|
697
|
+
const J = mode === 1 ? HOLE_SPIN * act : 0;
|
|
698
|
+
const ep = (mode === 1 ? 0.035 : 0.05) * u.uPointerRadius.value;
|
|
699
|
+
if (J > 0 && camera) {
|
|
700
|
+
// spin axis = view direction, expressed in the object's frame (swirl in the screen plane)
|
|
701
|
+
camera.getWorldDirection(spinAxis);
|
|
702
|
+
object.getWorldQuaternion(invQ).invert();
|
|
703
|
+
spinAxis.applyQuaternion(invQ).normalize();
|
|
704
|
+
sv[0] = spinAxis.x;
|
|
705
|
+
sv[1] = spinAxis.y;
|
|
706
|
+
sv[2] = spinAxis.z;
|
|
707
|
+
}
|
|
708
|
+
P.copy(u.uPointer.value);
|
|
709
|
+
const steps = Math.min(Math.ceil(T / H_MAX), 6);
|
|
710
|
+
for (let s = 0; s < steps; s++)
|
|
711
|
+
step(T / steps, mp, ep * ep, P, J, sv);
|
|
712
|
+
simT += T;
|
|
713
|
+
u.uSimT.value = simT;
|
|
714
|
+
}
|
|
715
|
+
posAttr.needsUpdate = true;
|
|
716
|
+
velAttr.needsUpdate = true;
|
|
717
|
+
if (trail)
|
|
718
|
+
trail.update(d);
|
|
719
|
+
},
|
|
720
|
+
dispose() {
|
|
721
|
+
geo.dispose();
|
|
722
|
+
mat.dispose();
|
|
723
|
+
dgeo.dispose();
|
|
724
|
+
dmat.dispose();
|
|
725
|
+
ggeo.dispose();
|
|
726
|
+
gmat.dispose();
|
|
727
|
+
if (trail)
|
|
728
|
+
trail.dispose();
|
|
729
|
+
if (discTrail) {
|
|
730
|
+
discTrailGeo.dispose();
|
|
731
|
+
discTrailMat.dispose();
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
_state: { pos, vel, sat },
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
|
|
738
|
+
const DEFAULTS = {
|
|
739
|
+
intensity: 1,
|
|
740
|
+
speed: 1,
|
|
741
|
+
opacity: 0.9,
|
|
742
|
+
size: 1,
|
|
743
|
+
color: '#8cbcff',
|
|
744
|
+
particleCount: 18000,
|
|
745
|
+
satelliteCount: 2,
|
|
746
|
+
satelliteMass: 0.0005,
|
|
747
|
+
orbitEccentricity: 0.5,
|
|
748
|
+
streamAge: 2.2,
|
|
749
|
+
dopplerScale: 2,
|
|
750
|
+
redshiftColor: '#ffa070',
|
|
751
|
+
discStars: 70000,
|
|
752
|
+
armCount: 2,
|
|
753
|
+
nebulaColor: '#ff5c8a',
|
|
754
|
+
starSize: 1,
|
|
755
|
+
pointerMode: 'subhalo',
|
|
756
|
+
pointerStrength: 1,
|
|
757
|
+
pointerRadius: 1,
|
|
758
|
+
pointerColor: '#ffd29a',
|
|
759
|
+
trailMode: 'comet',
|
|
760
|
+
trailLength: 1.6,
|
|
761
|
+
trailOpacity: 0.55,
|
|
762
|
+
trailColor: '#7fa8ff',
|
|
763
|
+
trailShare: 0.25,
|
|
764
|
+
};
|
|
765
|
+
const TidalStreamParticles = ({ size, config, animations, onObjectReady, onObjectDispose }) => {
|
|
766
|
+
const rootRef = useRef(null);
|
|
767
|
+
const animCache = useRef({});
|
|
768
|
+
useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
|
|
769
|
+
const s = size > 0 ? size : 1;
|
|
770
|
+
const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
|
|
771
|
+
// uniforms outlive the objects, so keyframe animations survive a rebuild
|
|
772
|
+
const uniforms = useMemo(() => createUniforms(), []);
|
|
773
|
+
// rebuilt only when a structural knob changes (trails rebuild inside apply())
|
|
774
|
+
const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.satelliteCount, cfg.orbitEccentricity, cfg.streamAge, cfg.discStars]);
|
|
775
|
+
useEffect(() => () => instance.dispose(), [instance]);
|
|
776
|
+
useLayoutEffect(() => {
|
|
777
|
+
applyConfig(uniforms, cfg, s);
|
|
778
|
+
instance.apply(cfg);
|
|
779
|
+
}, [instance, uniforms, cfg, s]);
|
|
780
|
+
const pointer = usePointerField();
|
|
781
|
+
useFrame((state, delta) => {
|
|
782
|
+
instance.update(Math.min(delta, 0.1), state.camera, pointer);
|
|
783
|
+
if (animations?.length)
|
|
784
|
+
applyGenerativeAnimations(instance.materials, animations, animCache.current, Date.now(), false);
|
|
785
|
+
});
|
|
786
|
+
return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
|
|
787
|
+
};
|
|
788
|
+
export default TidalStreamParticles;
|