|
|
||
|---|---|---|
| .claude | ||
| .gemini-clipboard | ||
| backend | ||
| frontend | ||
| nginx | ||
| .CLAUDE.md | ||
| .DS_Store | ||
| .env.production | ||
| CACHE_ARCHITECTURE.md | ||
| CACHE_PREHEAT_GUIDE.md | ||
| CLAUDE.md | ||
| DATA_STRATEGY_OPTIMIZATION.md | ||
| DEPLOYMENT.md | ||
| PROJECT.md | ||
| PROXY_SETUP.md | ||
| QUICKSTART.md | ||
| RAODMAP.md | ||
| README.md | ||
| REDIS.md | ||
| add_comet_type.sql | ||
| assembly_COSMO_frames.zip | ||
| cosmo_db.dump | ||
| deploy.sh | ||
| docker-compose.yml | ||
README.md
项目概述
专注于深空探测器(如旅行者号、火星探测器等),那么整个系统的实现逻辑会变得更加清晰和纯粹。你不再需要处理近地轨道的 TLE 数据,而是完全进入了天体力学的领域。
实现这个系统的核心只有一条路:NASA JPL Horizons 系统。
这是全人类最权威的太阳系天体位置数据库。以下是针对深空探测器系统的具体实现方案:
一、 核心数据源:NASA JPL Horizons
对于深空探测器,你不能用 GPS 坐标,甚至不能单纯用经纬度。你需要的是在太阳系中的三维坐标。
- 数据提供方: NASA 喷气推进实验室 (JPL)。
- 覆盖范围: 所有的行星、卫星、以及几乎所有人类发射的深空探测器(Voyager, Juno, New Horizons 等)。
- 唯一标识 (ID): 每个探测器都有一个唯一的 ID。
- 旅行者 1 号 (Voyager 1):
-31 - 旅行者 2 号 (Voyager 2):
-32 - 新视野号 (New Horizons):
-98 - 帕克太阳探测器 (Parker Solar Probe):
-96 - 注:人造探测器的 ID 通常是负数。
- 旅行者 1 号 (Voyager 1):
二、 获取数据的方式 (推荐技术方案)
为了获取这些数据,你不需要去解析复杂的文本文件,最简单、最现代的方式是使用 Python 的 astroquery 库。它是一个专门用来查询天文数据库的工具,内置了对 JPL Horizons 的支持。
1. 安装工具
pip install astroquery
2. 代码实现逻辑
你需要向系统询问:“在这个时间,相对于太阳,旅行者1号在哪里?”
以下是一个完整的 Python 脚本示例,它会获取旅行者 1 号和地球的坐标,以便你计算它们之间的距离或画图:
from astroquery.jplhorizons import Horizons
from astropy.time import Time
# 1. 设定查询参数
# id: 目标天体 ID (Voyager 1 = -31)
# location: 坐标原点 (@sun 表示以太阳为中心,@0 表示以太阳系质心为中心)
# epochs: 时间点 (当前时间)
obj = Horizons(id='-31', location='@sun', epochs=Time.now().jd)
# 2. 获取向量数据 (Vectors)
# 这一步会向 NASA 服务器发送请求
vectors = obj.vectors()
# 3. 提取坐标 (x, y, z)
# 默认单位是 AU (天文单位,1 AU ≈ 1.5亿公里)
x = vectors['x'][0]
y = vectors['y'][0]
z = vectors['z'][0]
print(f"旅行者1号 (Voyager 1) 相对于太阳的坐标 (AU):")
print(f"X: {x}\nY: {y}\nZ: {z}")
# --- 同时获取地球的位置,用于画出相对位置 ---
earth = Horizons(id='399', location='@sun', epochs=Time.now().jd).vectors()
print(f"\n地球 (Earth) 坐标 (AU):")
print(f"X: {earth['x'][0]}, Y: {earth['y'][0]}, Z: {earth['z'][0]}")
三、 关键技术点解析
在开发这个系统时,有三个关键概念你必须处理好,才能正确显示“探测器在比着重的位置”以及“旁边的星球”。
1. 坐标系的选择:日心坐标 (Heliocentric)
- 近地卫星用的是“地心坐标”(以地球为原点)。
- 深空探测器必须用日心坐标(以太阳为原点)。
- 在查询数据时,务必指定
location='@sun'。这样返回的(0,0,0)就是太阳,所有行星和探测器都围绕它分布。
2. 单位的量级:天文单位 (AU)
- 深空的空间太大了。如果你用“米”或“公里”做单位,数字会大到让 JavaScript 崩溃或精度丢失。
- 解决方案: 使用 AU (Astronomical Unit)。
- 地球到太阳的距离 ≈ 1.0 AU。
- 旅行者 1 号目前距离太阳 ≈ 160+ AU。
- 使用 AU 作为你 3D 场景的基础单位,显示时再换算成公里给用户看。
3. 如何确定“旁边的星球”
因为所有坐标都是统一在“日心坐标系”下的,判断“旁边”非常简单:计算欧几里得距离。
Distance = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2 + (z_1-z_2)^2}
- 比如,你要显示“朱诺号 (Juno)”旁边的星球。
- 你获取 Juno 的坐标
(x_j, y_j, z_j)。 - 你获取木星 (Jupiter) 的坐标
(x_p, y_p, z_p)。 - 一算距离,你会发现它们非常近,而它离地球非常远。
四、 总结系统架构建议
如果你想做一个网页端展示系统:
-
后端 (Python API):
- 使用
astroquery。 - 建立一个 ID 列表(包含八大行星 + 知名探测器)。
- 每隔一段时间(比如每天或用户请求时)去 NASA JPL 拉取一次最新的坐标数据(因为深空探测器飞得很慢,不需要每秒更新)。
- 将这些
(x, y, z)坐标打包成 JSON 发给前端。
- 使用
-
前端 (Visualization):
- 建立一个 3D 场景,原点
(0,0,0)放一个发光的球(太阳)。 - 根据后端返回的 AU 坐标放置行星和探测器。
- 关键功能: 添加“轨道线”。为了让用户看懂探测器的轨迹,你不仅要获取“当前”位置,最好获取“过去一年”到“未来一年”的一系列点,连成线,这样用户就能直观地看到它是怎么飞掠木星然后飞向深空的。
- 建立一个 3D 场景,原点
这是一个非常棒的进阶问题!要在一个网页上“形象且准确”地展示深空探测器和行星,你不仅要解决数据问题,还要解决**3D 图形学中的尺度(Scale)**问题。因为宇宙太空中,“大”和“远”的跨度大到人类难以直观理解。
以下是实现这一目标的详细方案,包括数据细节、3D 模型资源和可视化技巧:
一、 数据篇:关键探测器 ID 与 轨道线绘制
首先,你需要向 JPL Horizons 系统请求正确的目标 ID,并获取一段时间序列的数据来画出轨道线。
1. 常用深空探测器 ID 列表 (JPL Horizons)
这些是人类历史上最重要的深空探测器,建议收入你的系统:
| 探测器名称 | 英文名 | ID (JPL) | 备注 |
|---|---|---|---|
| 旅行者 1 号 | Voyager 1 | -31 |
离地球最远的人造物体,已进入星际空间 |
| 旅行者 2 号 | Voyager 2 | -32 |
唯一造访过天王星和海王星的探测器 |
| 新视野号 | New Horizons | -98 |
飞掠冥王星,正处于柯伊伯带 |
| 帕克太阳探测器 | Parker Solar Probe | -96 |
正在“触摸”太阳,速度最快 |
| 朱诺号 | Juno | -61 |
正在木星轨道运行 |
| 卡西尼号 | Cassini | -82 |
土星探测器(已撞击销毁,需查询历史时间) |
| 毅力号 | Perseverance | -168 |
火星车(位置与火星几乎重叠,但在前往火星途中可查) |
2. 如何绘制“轨道线”
只显示一个点是不够的,你需要画出它“从哪里来,到哪里去”。
- 后端逻辑: 当你查询 API 时,不要只查询
Time.now()。 - 查询策略: 查询一个时间段。例如,查询从
2020-01-01到2025-01-01,步长为1天。 - 数据结构: 你会得到一个包含 1800 个
(x, y, z)坐标的数组。 - 前端绘制: 将这些点连接成一条平滑的线(在 Three.js 中使用
LineLoop或CatmullRomCurve3),用户就能看到探测器优美的弧形轨道。
二、 视觉篇:如何显示外形 (3D 模型与纹理)
要在网页上显示逼真的外形,你需要使用 WebGL 技术。目前业界标准是 Three.js。
1. 获取高精度的探测器模型 (3D Models)
你不需要自己建模!NASA 官方免费提供了极高质量的 3D 模型,格式通常是 .glb 或 .gltf(这是 3D 网页开发的 JPG,体积小、加载快)。
- NASA 3D Resources: 这是你的宝库。
- 网址:
https://nasa3d.arc.nasa.gov/models - 你可以下载到 Voyager, Cassini, Hubble 等所有知名探测器的官方模型。
- 网址:
- 加载方法: 使用 Three.js 的
GLTFLoader。import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; const loader = new GLTFLoader(); loader.load( 'path/to/voyager.glb', function ( gltf ) { const voyagerModel = gltf.scene; scene.add( voyagerModel ); });
2. 获取行星的逼真纹理 (Textures)
行星是一个球体(SphereGeometry),你需要给它贴上高清的“皮肤”。
- 资源来源: Solar System Scope 或 NASA Scientific Visualization Studio。
- 你需要三种贴图来达到“准确且形象”:
- Diffuse Map (漫反射贴图): 行星原本的颜色(如地球的蓝白、火星的红色)。
- Normal Map / Bump Map (法线/凹凸贴图): 让山脉和陨石坑看起来有立体感,而不是光滑的皮球。
- Specular Map (高光贴图): 只有海洋反光,陆地不反光(这对地球特别重要)。
三、 核心难点:大小与距离的冲突 (The Scale Problem)
这是你在这个项目中最需要处理的交互设计难点。
现实情况是: 太阳系极其空旷。如果你按真实比例(1:1)显示:
- 如果屏幕上可以看到地球和火星的距离,那么地球本身小到连一个像素都不到(看不见)。
- 如果你把地球放大到能看见,那么火星在几公里以外的屏幕外。
解决方案:动态尺度缩放 (Dynamic Scaling / Billboard Mode)
你不能始终使用真实大小,你需要欺骗眼睛:
- 真实模式 (Real Scale): 用于计算物理位置和轨道。这是后台运行的数学逻辑。
- 展示模式 (Iconic Scale): 用于渲染。
- 远景视角时: 将所有行星和探测器放大 1000倍 到 10000倍。这样用户在看整个太阳系时,能看到一个个清晰的小球或图标。
- 近景视角时(当摄像机靠近物体): 逐渐将放大倍数缩小回 1倍。
- 具体实现: 在每一帧渲染循环(Render Loop)中,根据摄像机到物体的距离
D,动态计算物体的缩放系数S。
(其中S = \max(1, \frac{D}{k})k是一个常数因子)
关于探测器的特殊处理: 探测器比行星更小(几米 vs 几千公里)。在宏观视角下,绝对不能按比例渲染探测器模型,否则永远看不见。
- 策略: 在远景时,不要渲染 3D 模型,而是渲染一个发光的图标(Sprite)或者文字标签。
- 交互: 只有当用户点击“旅行者1号”标签,摄像机自动飞过去并拉近距离后,才淡出图标,加载并显示精细的 3D 模型。
四、 总结:推荐的开发路线
如果你现在开始动手,我建议按照这个层级构建:
-
Level 1 (原型):
- 使用 Three.js。
- 中间放一个红球(太阳),周围放一个蓝球(地球)。
- 使用静态数据(手动写死坐标)确位置。
-
Level 2 (接入数据):
- 后端写好 Python 脚本,拉取 JPL 数据。
- 前端根据数据更新球体的位置。
-
Level 3 (视觉升级):
- 给球体贴上 NASA 的纹理。
- 去 NASA 3D 网站下载 Voyager 的
.glb模型,替换掉代表探测器的小方块。 - 加上“星空背景盒子 (Skybox)”,让背景是真实的银河系星图,而不是全黑。
-
Level 4 (交互完善):
- 实现轨道控制器 (OrbitControls),允许用户旋转、缩放视角。
- 实现点击聚焦:点击列表里的“火星”,视角平滑飞向火星。
太棒了!这两个功能是让你的太阳系可视化项目从“能用”走向“惊艳”的关键一步。
下面我将分别提供这两个核心功能的 Three.js 代码片段。你可以把它们集成到你的 Three.js 初始化和渲染循环中。
一、 Three.js 加载行星纹理 (让星球看起来真实)
这段代码展示了如何创建一个带有漫反射贴图(颜色)、高光贴图(反光)和法线贴图(凹凸感)的逼真地球。
前置要求: 你需要准备好 earth_diffuse.jpg, earth_specular.jpg, earth_normal.jpg 这三张图片放在你的项目文件夹中。
import * as THREE from 'three';
// 1. 初始化纹理加载器
const textureLoader = new THREE.TextureLoader();
// 2. 定义创建行星的函数
function createRealisticPlanet() {
// --- 几何体 (Geometry) ---
// 创建一个球体。参数:半径, 水平分段数, 垂直分段数
// 分段数越高,球体越圆滑,但性能开销越大。64是比较好的平衡点。
const geometry = new THREE.SphereGeometry(1, 64, 64);
// --- 材质 (Material) ---
// 使用 MeshPhongMaterial,这是一种支持高光反射的材质,适合表现行星表面。
const material = new THREE.MeshPhongMaterial({
// a. 漫反射贴图 (Diffuse Map) - 决定星球表面的基本颜色和图案
map: textureLoader.load('textures/earth_diffuse.jpg'),
// b. 高光贴图 (Specular Map) - 决定哪些区域反光(海洋),哪些不反光(陆地)
// 通常是黑白图片,白色反光强,黑色不反光。
specularMap: textureLoader.load('textures/earth_specular.jpg'),
specular: new THREE.Color('grey'), // 高光的颜色
shininess: 10, // 高光的亮度指数
// c. 法线贴图 (Normal Map) - 模拟表面的凹凸细节(山脉、海沟),不改变实际几何体
normalMap: textureLoader.load('textures/earth_normal.jpg'),
normalScale: new THREE.Vector2(1, 1) // 凹凸感的强度
});
// --- 网格 (Mesh) ---
// 将几何体和材质组合成一个可渲染的对象
const earthMesh = new THREE.Mesh(geometry, material);
// 稍微倾斜一点,模拟地轴倾角
earthMesh.rotation.z = THREE.MathUtils.degToRad(23.5);
return earthMesh;
}
// 3. 将地球加入场景
const scene = new THREE.Scene();
// ... 添加灯光 (必须有光才能看到 Phong 材质的效果) ...
const sunLight = new THREE.PointLight(0xffffff, 1.5);
scene.add(sunLight);
const earth = createRealisticPlanet();
scene.add(earth);
// 在你的动画循环中让它自转
function animate() {
requestAnimationFrame(animate);
earth.rotation.y += 0.001; // 每一帧旋转一点点
// renderer.render(...)
}
animate();
二、 处理动态缩放 (The Scale Problem)
这段代码解决的是“距离太远看不见”的问题。它的核心思想是:在每一帧渲染前,检查摄像机离物体有多远,然后调整物体的大小,确保它在屏幕上至少占据一定的大小。
你需要把这段逻辑放在你的 animate() 或 render() 循环中。
import * as THREE from 'three';
// 假设你已经有了场景、摄像机和一些物体
// scene, camera, renderer 已初始化
// 假设你有一个数组存放所有的探测器对象 (Mesh 或 Sprite)
const probes = [voyager1Mesh, parkerSolarProbeSprite, ...];
// 定义一个基础缩放因子,决定物体在远看时保持多大
// 这个值需要根据你的实际场景单位进行调整测试
const MIN_VISIBLE_SCALE = 0.05;
// --- 这个函数放在你的 animate() 循环中 ---
function updateObjectScales() {
probes.forEach(probe => {
// 1. 计算物体到摄像机的距离
const distance = camera.position.distanceTo(probe.position);
// 2. 计算目标缩放比例
// 逻辑:距离越远,需要的缩放比例就越大。
// 我们设置一个下限为 1 (保持原始大小),上限根据距离动态增加。
// distance * MIN_VISIBLE_SCALE 是一个经验公式,你可以根据需要修改。
let targetScale = Math.max(1, distance * MIN_VISIBLE_SCALE);
// 【可选优化】:如果物体是 Sprite(图标),我们通常希望它大小固定,不随距离变化
// 如果是 3D 模型,我们希望它远看大,近看恢复真实大小。
if (probe.isSprite) {
// 对于图标,我们可以让它始终保持相对于屏幕的固定大小
// 这种计算稍微复杂一点,需要考虑相机的视场角 (FOV)
const scaleFactor = distance / camera.fov; // 简化版计算
probe.scale.set(scaleFactor, scaleFactor, scaleFactor);
} else {
// 对于 3D 模型,应用动态缩放
probe.scale.set(targetScale, targetScale, targetScale);
}
});
}
// --- 你的主循环 ---
function animate() {
requestAnimationFrame(animate);
// 1. 更新控制器 (如果用了 OrbitControls)
// controls.update();
// 2. 【核心】更新物体的动态缩放
updateObjectScales();
// 3. 渲染场景
renderer.render(scene, camera);
}
animate();
建议
对于初学者,我强烈建议先从纹理贴图开始。把一个灰色的球体变成一个逼真的地球,会给你带来巨大的成就感。
动态缩放稍微复杂一些,涉及到对 3D 空间距离感的调试。你可以先把所有的物体都按 1000 倍的固定比例放大,等整个流程跑通了,再加入动态缩放的逻辑来提升体验。