有人用 Three.js 把 14000 颗卫星的轨道在浏览器里全画出来了
SatDeck 是一个纯前端的卫星轨道可视化工具,用 Three.js 渲染 3D 地球和轨道路径,用 Leaflet 展示 2D 地面轨迹,数据来源是 CelesTrak 的公开 TLE 文件——里面有超过 14000 颗在轨卫星的轨道数据。选一颗卫星,3D 视图里轨迹线跟着画出来,2D 地图同步显示地面投影,还能输入自己的坐标计算该卫星当前的方位角和仰角。

Demo:satdeck.pages.dev GitHub:https://github.com/Boex9/SatDeck (opens in a new tab)
TLE 格式和解析
卫星轨道用的是 TLE(Two-Line Element) 标准格式,每颗卫星三行:
ISS (ZARYA)
1 25544U 98067A 25362.53357479 .00015441 00000+0 27889-3 0 9992
2 25544 51.6321 64.2558 0003250 314.4858 45.5864 15.49907133545304第一行是名字,第二行第三行编码了六个轨道根数:半长轴、离心率、轨道倾角、升交点赤经、近地点幅角、平近点角。NORAD ID 在第二行第 2-7 位。
解析就是按三行分组,提取各字段:
function parseTLE(rawData) {
const lines = rawData.split("\n").map(l => l.trim()).filter(l => l !== "");
const satellites = [];
for (let i = 0; i < lines.length; i += 3) {
const name = lines[i];
const line1 = lines[i + 1];
const line2 = lines[i + 2];
const noradId = line1.slice(2, 7).trim(); // NORAD ID 在第 2-7 位
if (!line1 || !line2) continue;
satellites.push({ name, noradId, line1, line2 });
}
return satellites;
}数据可以用本地 tledata.txt 文件,也可以直接拉 CelesTrak 的接口(https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=tle)。CelesTrak 有速率限制,生产环境建议把 TLE 文件托管在自己的后端,fetchtle() 只需改一个 URL。
SGP4:坐标计算的完整链路
有了 TLE,要知道卫星在某个时刻在哪里,需要用 SGP4 轨道传播模型(satellite.js 实现)。输出不是直接的经纬度,而是一个叫 ECI(地心惯性坐标系) 的三维坐标——以地球质心为原点、不随地球自转的参考系。
到最终渲染坐标,需要走完一条转换链路:
ECI 坐标 → GMST 修正地球自转 → 大地坐标(经纬度+高度)→ Three.js 场景坐标用代码表示:
const satrec = satellite.twoline2satrec(line1, line2); // TLE → 轨道根数对象
const pv = satellite.propagate(satrec, now); // SGP4 计算 ECI 坐标
// GMST(格林尼治平恒星时)修正地球自转角度
const gmst = satellite.gstime(now);
const geo = satellite.eciToGeodetic(pv.position, gmst);
const lat = satellite.degreesLat(geo.latitude);
const lon = satellite.degreesLong(geo.longitude);
const alt = geo.height; // km之所以需要 GMST,是因为 ECI 坐标系不随地球转动,要把它换算到地面坐标,必须知道地球此刻转了多少角度。
最后一步是把大地坐标转换为 Three.js 的场景坐标,用球坐标系:
function LatLonAltToXYZ(lat, lon, alt) {
const polarAngle = THREE.MathUtils.degToRad(90 - lat); // 极角 = 90° - 纬度
const azimuthalAngle = THREE.MathUtils.degToRad(-lon); // 方位角 = -经度
const radius = 1 + (alt / 6378); // 归一化,地球半径 = 1
return new THREE.Vector3(
radius * Math.sin(polarAngle) * Math.cos(azimuthalAngle),
radius * Math.cos(polarAngle),
radius * Math.sin(polarAngle) * Math.sin(azimuthalAngle)
);
}地球半径归一化为 1,高度相对地球半径(6378 km)缩放,低轨卫星大约在 1.1-1.2 左右。
轨道路径:每 30 秒采一个点
轨道路径就是在时间轴上批量采样,默认每 30 秒一个点,支持只看历史、只看预测、或前后都显示:
export async function calculateOrbitPath(
noradId,
durationMinutes = 200,
stepSeconds = 30,
mode = "both" // "forward" / "backward" / "both"
) {
for (let t = start; t <= end; t += stepSeconds) {
const futureTime = new Date(now.getTime() + t * 1000);
const pv = satellite.propagate(satrec, futureTime);
const gmst = satellite.gstime(futureTime);
const geo = satellite.eciToGeodetic(pv.position, gmst);
segment.push(LatLonAltToXYZ(
satellite.degreesLat(geo.latitude),
satellite.degreesLong(geo.longitude),
geo.height
));
}
}200 分钟 ÷ 30 秒 = 400 个采样点,连成 THREE.Line。轨迹每 2 秒重新计算一次,SGP4 每秒更新一次当前位置。
2D 地面轨迹还要处理一个边缘情况:日期线跨越——卫星从东经 179° 飞到西经 179°,经度跳变超过 180°,如果不做处理 Leaflet 会画出一条横跨地图的直线。解决方式是检测前后两点的经度差,超过 180° 就切断线段:
if (lastLon !== null && Math.abs(lon - lastLon) > 180) {
segments.push(segment); // 当前段结束
segment = []; // 开新段
}2D 地图:昼夜线和太阳位置
3D 视图配了一个 Leaflet 2D 地图,做地面轨迹补充显示。地图用深色底图(CartoDB dark),加了几个实用图层:
昼夜线:用 Leaflet Terminator 插件,每分钟更新一次,把地球当前被太阳照到的半球和夜晚半球的边界画出来。
太阳位置:根据当前时间计算太阳的地下点经纬度,用图标标在地图上。
用户位置:调用浏览器地理定位 API,把观测者位置标红点。如果用户拒绝授权,可以在输入框手动填经纬度。
观测参数:从你的位置看这颗卫星
如果输入了观测者坐标,卫星计算模块还会实时算出拓扑坐标——方位角、仰角、距离和径向速度:
// 观测者 ECEF 坐标
const obsEcf = satellite.geodeticToEcf(observer);
// 相对位置向量
const rho = {
x: positionEcf.x - obsEcf.x,
y: positionEcf.y - obsEcf.y,
z: positionEcf.z - obsEcf.z
};
// 方位角和仰角
const look = satellite.ecfToLookAngles(observer, positionEcf);
// look.azimuth(方位角)/ look.elevation(仰角)/ look.rangeSat(距离 km)仰角 > 0° 说明卫星在地平线以上,这时候理论上是可见的(不过还要考虑光照条件和大气散射)。同时还会显示径向速度(正值为远离,负值为靠近)——这个数据用来理解多普勒频移,HAM 无线电爱好者追踪卫星信号时会用到。
地球用了自定义 Shader:Cubemap 盒子投影
地球渲染用的不是标准材质,而是自定义 ShaderMaterial,原因是普通球面 UV 贴图在极点附近会严重拉伸。解决方案是把地球纹理转成 6 面 Cubemap,用盒子投影采样,极点完全不拉伸:
// 顶点着色器:earthRot 控制地球自转,vDir 传给片元着色器
uniform mat3 earthRot;
varying vec3 vDir;
void main() {
vec3 worldDir = (modelMatrix * vec4(position, 1.0)).xyz;
vDir = normalize(earthRot * worldDir);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// 片元着色器:Cubemap 采样 + sRGB 伽马校正
uniform samplerCube cubeMap;
varying vec3 vDir;
void main() {
vec3 col = textureCube(cubeMap, normalize(vDir)).rgb;
gl_FragColor = vec4(pow(col, vec3(1.0 / 2.2)), 1.0); // gamma 校正
}地球纹理推荐 NASA Blue Marble 系列,先用全景转 Cubemap 工具生成 6 张面图。地球自转通过每帧更新 earthRot 矩阵实现,与 GMST 时间同步,确保卫星位置和地球方位保持一致。
跑起来
git clone https://github.com/Boex9/SatDeck.git
cd SatDeck
python -m http.server
# 打开 http://localhost:8000不能直接双击 HTML 文件,file:// 协议会阻止 fetch() 加载本地文件。Backend/tledata.txt 里预置了 ISS 等卫星,换成从 CelesTrak 下载的完整文件就能看到 14000 颗的轨道。
写在最后
SatDeck 技术上几个值得记住的点:TLE → SGP4 → ECI → GMST 修正 → 大地坐标 → Three.js 坐标的完整链路;Cubemap 盒子投影解决极点拉伸;日期线跨越的分段处理。这套管线在任何需要精确地球位置可视化的场景都可以直接参考。