Files
planet/frontend/public/earth/js/iridium-footprint-adapter.js
2026-04-24 15:41:42 +08:00

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);
}