168 lines
4.2 KiB
JavaScript
168 lines
4.2 KiB
JavaScript
import * as THREE from "three";
|
|
|
|
const EARTH_RADIUS_KM = 6378.137;
|
|
const SURFACE_SCALE = 1.003;
|
|
const SURFACE_OFFSET = 0.72;
|
|
const CLUSTER_DIAMETER_KM_APPROX = 4500;
|
|
const CLUSTER_RADIUS_KM_BASE = CLUSTER_DIAMETER_KM_APPROX / 2;
|
|
const SURFACE_AXIS = new THREE.Vector3(0, 0, 1);
|
|
|
|
function disposeMaterial(material) {
|
|
if (!material) return;
|
|
if (Array.isArray(material)) {
|
|
material.forEach(disposeMaterial);
|
|
return;
|
|
}
|
|
material.dispose();
|
|
}
|
|
|
|
function disposeObjectTree(object) {
|
|
if (!object) return;
|
|
object.traverse((child) => {
|
|
if (child.geometry) {
|
|
child.geometry.dispose();
|
|
}
|
|
if (child.material) {
|
|
disposeMaterial(child.material);
|
|
}
|
|
});
|
|
}
|
|
|
|
function createIridiumClusterMaterial() {
|
|
return new THREE.ShaderMaterial({
|
|
transparent: true,
|
|
side: THREE.DoubleSide,
|
|
depthTest: true,
|
|
depthWrite: false,
|
|
polygonOffset: true,
|
|
polygonOffsetFactor: -3,
|
|
polygonOffsetUnits: -3,
|
|
blending: THREE.AdditiveBlending,
|
|
uniforms: {
|
|
uColor: { value: new THREE.Color(0x5faeff) },
|
|
uOpacity: { value: 0.24 },
|
|
},
|
|
vertexShader: `
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform vec3 uColor;
|
|
uniform float uOpacity;
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vec2 p = vUv * 2.0 - 1.0;
|
|
float ellipseMetric = p.x * p.x * 0.82 + p.y * p.y * 1.06;
|
|
float alpha = exp(-ellipseMetric * 1.05) * (1.0 - smoothstep(0.86, 1.24, ellipseMetric));
|
|
alpha *= uOpacity;
|
|
if (alpha <= 0.001) discard;
|
|
gl_FragColor = vec4(uColor, alpha);
|
|
}
|
|
`,
|
|
});
|
|
}
|
|
|
|
function projectOffsetToSurface(
|
|
centerNormal,
|
|
alongTrack,
|
|
crossTrack,
|
|
alongKm,
|
|
crossKm,
|
|
earthRadiusWorld,
|
|
) {
|
|
const worldUnitsPerKm = earthRadiusWorld / EARTH_RADIUS_KM;
|
|
const surfaceRadius = earthRadiusWorld * SURFACE_SCALE + SURFACE_OFFSET;
|
|
return centerNormal
|
|
.clone()
|
|
.multiplyScalar(earthRadiusWorld)
|
|
.addScaledVector(alongTrack, alongKm * worldUnitsPerKm)
|
|
.addScaledVector(crossTrack, crossKm * worldUnitsPerKm)
|
|
.normalize()
|
|
.multiplyScalar(surfaceRadius);
|
|
}
|
|
|
|
function computeClusterRadiusKm(altitudeKm) {
|
|
const altitudeScale = THREE.MathUtils.clamp(
|
|
(Number(altitudeKm) || 780) / 780,
|
|
0.88,
|
|
1.18,
|
|
);
|
|
return CLUSTER_RADIUS_KM_BASE * altitudeScale;
|
|
}
|
|
|
|
export function createIridiumFootprintAdapter({
|
|
earthObj,
|
|
earthRadiusWorld,
|
|
renderOrder,
|
|
}) {
|
|
if (!earthObj) return null;
|
|
|
|
const group = new THREE.Group();
|
|
group.name = "iridium-footprint-overlay";
|
|
group.renderOrder = renderOrder;
|
|
group.userData = {
|
|
earthRadiusWorld,
|
|
clusterGlow: null,
|
|
};
|
|
|
|
const clusterGlow = new THREE.Mesh(
|
|
new THREE.CircleGeometry(1, 72),
|
|
createIridiumClusterMaterial(),
|
|
);
|
|
clusterGlow.name = "iridium-cluster-glow";
|
|
clusterGlow.renderOrder = renderOrder - 1;
|
|
group.add(clusterGlow);
|
|
group.userData.clusterGlow = clusterGlow;
|
|
|
|
earthObj.add(group);
|
|
return group;
|
|
}
|
|
|
|
export function updateIridiumFootprintAdapter(
|
|
group,
|
|
{ position, alongTrack, crossTrack, altitudeKm },
|
|
) {
|
|
if (!group || !position || !alongTrack || !crossTrack) return;
|
|
|
|
const earthRadiusWorld =
|
|
group.userData?.earthRadiusWorld || EARTH_RADIUS_KM;
|
|
const centerNormal = position.clone().normalize();
|
|
const clusterRadiusKm = computeClusterRadiusKm(altitudeKm);
|
|
const clusterGlow = group.userData?.clusterGlow || null;
|
|
const worldUnitsPerKm = earthRadiusWorld / EARTH_RADIUS_KM;
|
|
|
|
if (clusterGlow) {
|
|
const clusterCenter = projectOffsetToSurface(
|
|
centerNormal,
|
|
alongTrack,
|
|
crossTrack,
|
|
0,
|
|
0,
|
|
earthRadiusWorld,
|
|
);
|
|
const clusterNormal = clusterCenter.clone().normalize();
|
|
clusterGlow.position.copy(clusterCenter);
|
|
clusterGlow.quaternion.setFromUnitVectors(SURFACE_AXIS, clusterNormal);
|
|
clusterGlow.scale.set(
|
|
clusterRadiusKm * worldUnitsPerKm * 1.18,
|
|
clusterRadiusKm * worldUnitsPerKm * 0.96,
|
|
1,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function disposeIridiumFootprintAdapter(group, earthObj) {
|
|
if (!group) return;
|
|
if (earthObj) {
|
|
earthObj.remove(group);
|
|
} else if (group.parent) {
|
|
group.parent.remove(group);
|
|
}
|
|
disposeObjectTree(group);
|
|
}
|