这份文档是什么

下面就是「文档组」的模型实际拿到的全部内容。它由 Claude Opus 5 (Ultracode·xhigh) 在完成自己那版实现之后, 从自身的实现过程反推整理而成——包括数据表、算法公式、渲染管线设计, 以及第 7 节那 15 条实际踩过的坑。

正因为如此,文档组本质上是在做一道已知答案的命题作文。 这也是为什么本站强调:它能说明「详细文档能极大提升产出质量」, 但不能用来给模型能力排序。

目录
0. 目标1. 硬性约束2. 天文数据(照抄,不要自己编)3. 核心算法4. 渲染架构5. 程序化天体表面6. 交互与界面7. 必须避开的坑8. 验收标准9. 建议的实施顺序附:一个容易被忽略的整体建议

任务说明:太阳系实时运动模型(单 HTML 文件)

本文件是一份实现规格书。数据表和公式请照抄,不要自己回忆或推测数值。 渲染代码、着色器、UI 代码由你自己编写。 第 7 节「必须避开的坑」请在动手前完整读一遍——里面每一条都是实际会遇到的问题。

0. 目标

做一个太阳系的实时运动模型,单个 HTML 文件,双击用浏览器打开即可运行。

两条核心要求:

  1. 尽可能真实——天体位置必须由真实轨道根数实时计算得出,不能是预设动画或近似圆周运动。尺寸和距离的比例可以压缩(否则同屏看不见),但角位置、轨道相位、周期、偏心率、倾角必须是真的。
  2. 视觉效果拉满——这不是一个示意图,是一个应该让人愿意盯着看的东西。

1. 硬性约束

项要求
文件数1 个 .html,CSS 和 JS 全部内联
外部依赖零。不允许 <script src>、CDN、import、外部图片/字体/纹理
离线断网必须能完整运行
图形 APIWebGL2(原生,不使用 Three.js 等库)
目标帧率1080p 下 60 fps
界面语言中文

关于「不使用 Three.js」:这意味着矩阵库、球体网格生成、相机控制、帧缓冲管理都要自己写。如果允许用库,那 600 KB 的库内联进来会让文件体积失控,且泛光管线、环系互投影这类东西反正要写自定义着色器。

纹理来源:不允许外部图片,所以所有天体表面必须用程序化噪声在片元着色器里现场生成。这是本任务的主要难点之一。


2. 天文数据(照抄,不要自己编)

2.1 行星轨道根数(NASA/JPL《主要行星近似位置》)

六个根数 + 每儒略世纪的变化率。标称有效区间 1800–2050 年。

单位:a 用 AU,其余用度。

天体aeILϖ (近日点黄经)Ω (升交点黄经)
水星0.387099270.205635937.00497902252.2503235077.4577962848.33076593
金星0.723335660.006776723.39467605181.97909950131.6024671876.67984255
地球1.000002610.01671123−0.00001531100.46457166102.937681930.0
火星1.523710340.093394101.84969142−4.55343205−23.9436295949.55953891
木星5.202887000.048386241.3043969534.3964405114.72847983100.47390909
土星9.536675940.053861792.4859918749.9542442392.59887831113.66242448
天王星19.189164640.047257440.77263783313.23810451170.9542763074.01692503
海王星30.069922760.008590481.77004347−55.1200296944.96476227131.78422574
冥王星39.482116750.2488273017.14001206238.92903833224.06891629110.30393684

变化率(每儒略世纪,即每 36525 天):

天体da/dtde/dtdI/dtdL/dtdϖ/dtdΩ/dt
水星0.000000370.00001906−0.00594749149472.674111750.16047689−0.12534081
金星0.00000390−0.00004107−0.0007889058517.815387290.00268329−0.27769418
地球0.00000562−0.00004392−0.0129466835999.372449810.323273640.0
火星0.000018470.00007882−0.0081313119140.302684990.44441088−0.29257343
木星−0.00011607−0.00013253−0.001837143034.746127750.212526680.20469106
土星−0.00125060−0.000509910.001936091222.49362201−0.41897216−0.28867794
天王星−0.00196176−0.00004397−0.00242939428.482027850.408052810.04240589
海王星0.000262910.000051050.00035372218.45945325−0.32241464−0.00508664
冥王星−0.000315960.000051700.00004818145.20780515−0.04062942−0.01183482
地球那一行严格说是「地月质心」。差别在视觉上不可见,直接当地球用。

2.2 自转周期与转轴倾角

rotH = 自转周期(小时),负值表示逆行自转。

tilt = 自转轴与黄道北极的夹角(度)。poleLon = 自转轴在黄道面上的投影所指的黄经(度)。

注意:教科书上写的「转轴倾角」是相对自身轨道面的,这里给的是相对黄道面的,两者差几度。渲染要用下表的值(因为世界坐标系是黄道系),信息面板展示可以用教科书值。 这些是由 IAU 极点赤经赤纬换算过来的,直接抄。
天体tilt(对黄道)poleLonrotH教科书倾角(对自身轨道)
水星7.0318.41407.60.03°
金星177.4211.2−5832.5177.4°
地球23.4490.023.934523.44°
火星26.76352.924.622925.19°
木星2.25247.89.9253.13°
土星28.0679.510.65626.73°
天王星97.7777.7−17.2497.77°
海王星28.02319.316.1128.32°
冥王星112.8137.4−153.29119.6°

自转轴在黄道直角坐标下的方向向量:

axis_ecl = ( sin(tilt)·cos(poleLon),  sin(tilt)·sin(poleLon),  cos(tilt) )

自转轴必须在惯性系中固定(不随公转改变方向)——这样土星环的张角才会随土星公转周期(29.5 年)自然地开合,天王星才会真的「躺着滚」。

2.3 行星物理参数(用于信息面板与显示半径)

天体半径 (km)质量 (kg)公转周期卫星数表面/云顶温度
太阳696,3401.989×10³⁰——5,772 K
水星2,439.73.30×10²³87.97 天0−173 ~ 427 ℃
金星6,051.84.87×10²⁴224.70 天0464 ℃
地球6,371.05.972×10²⁴365.256 天1−89 ~ 57 ℃
火星3,389.56.42×10²³686.98 天2−143 ~ 35 ℃
木星69,9111.898×10²⁷11.86 年95−108 ℃
土星58,2325.683×10²⁶29.46 年274−139 ℃
天王星25,3628.68×10²⁵84.01 年28−195 ℃
海王星24,6221.024×10²⁶164.8 年16−201 ℃
冥王星1,188.31.303×10²²247.9 年5−233 ~ −223 ℃

信息面板还建议展示:轨道偏心率、轨道倾角、转轴倾角、大气成分、以及一段 2–3 句的科普文字。

2.4 卫星

轨道用真实周期的匀速圆周运动即可(不必解开普勒方程),但周期必须真实——这样木卫一/二/三的 1:2:4 轨道共振会自然浮现。

卫星母行星半径 (km)轨道半径 (km)周期 (天)轨道倾角参考面
月球地球1,737.4384,40027.321665.145°黄道面
木卫一 Io木星1,821.6421,7001.7691380.05°母行星赤道面
木卫二 Europa木星1,560.8671,0343.5511810.47°母行星赤道面
木卫三 Ganymede木星2,634.11,070,4127.1545530.20°母行星赤道面
木卫四 Callisto木星2,410.31,882,70916.68900.19°母行星赤道面
土卫六 Titan土星2,574.71,221,87015.9450.35°母行星赤道面
海卫一 Triton海王星1,353.4354,759−5.876854156.9°母行星赤道面
冥卫一 Charon冥王星60619,5916.38720.08°母行星赤道面

2.5 哈雷彗星 1P/Halley

参数值
a17.834 AU
e0.96714
I162.26°(逆行)
ω(近日点幅角)111.33°
Ω(升交点黄经)58.42°
过近日点时刻 TpJD 2446470.96(1986-02-09)
周期75.32 年 = 27512.6 天

平近点角直接由过近日点时刻算:M = 2π·(JD − Tp) / P_days。

2.6 小行星带 / 柯伊伯带(程序化生成)

用顶点着色器为每个小天体解开普勒方程(每个粒子存 a, e, I, Ω, ω, M₀, n, size 八个浮点数),CPU 只在初始化时生成一次。数量建议:主带 9000,希尔达族 900,特洛伊群 2000,柯伊伯带 6000。

主带:a ∈ [2.06, 3.28] AU,用拒绝采样挖出柯克伍德空隙(这是真实存在的共振空隙,做出来非常加分):

空隙位置 (AU)宽度 σ挖除强度
2.502(3:1)0.0220.93
2.825(5:2)0.0160.85
2.958(7:3)0.0120.70
3.279(2:1)0.0200.90
2.0650.0100.60

保留概率 = Π(1 − 强度·exp(−((a−位置)/σ)²)),再乘一个以 2.72 AU 为中心、σ=0.55 的整体密度包络。 偏心率 e ~ 半正态,均值约 0.14,上限 0.34;倾角 I ~ 半正态,均值约 8°。

希尔达族:a ≈ 3.90 ± 0.09 AU,e ∈ [0.10, 0.30]。

木星特洛伊群:这是最出彩的一个。a ≈ 5.202,但平近点角必须锁在木星前后 60°:

M₀ = L_木星(J2000) ± 60° + 高斯扰动(σ≈26°)     // L_木星(J2000) = 34.396°
n  = 木星的平均运动(不要用自己的 a 去算 n,否则几十年后就散开了)

这样 L4/L5 两团会永远跟着木星跑。

柯伊伯带:22% 冥族(a ≈ 39.45 ± 0.55,与海王星 3:2 共振),64% 经典带(a ∈ [42.5, 47.7],e < 0.11,I 均值 7°),14% 离散盘(a ∈ [48, 80],e ∈ [0.15, 0.5],I 均值 22°)。

渲染:gl.POINTS,点大小随距离衰减并 clamp 到 [0.75, 4.5] 像素,加法混合,亮度乘一个随日心距衰减的因子(远的更暗)。

2.7 环系

用径向密度函数 ringDensity(r) 描述,r 以母行星半径为单位。这是把光学厚度直接建模,而不是贴图。

土星(1.235 – 2.34 母行星半径):

结构范围光学厚度 τ
C 环1.235 – 1.530.11
B 环1.525 – 1.951.20
卡西尼缝1.95 – 2.03把上面结果削掉 87%
A 环2.025 – 2.270.58
恩克缝2.214(σ≈0.0045)削掉 92%
基勒缝2.265(σ≈0.0018)削掉 80%
F 环2.324(σ≈0.0055)+0.30

最后整体乘一个高频噪声 0.70 + 0.60·fbm(r·230) 制造细密的环纹。

天王星:一组极窄的暗环,用窄高斯叠加。环带几何体建议做到 1.60 – 2.00,实际的环位于其中:ε 环在 1.951(τ≈0.55,σ≈0.0055),另外 6 条在 1.909 / 1.863 / 1.823 / 1.786 / 1.751 / 1.690,τ 从 0.20 递减到 0.07,σ 都在 0.0015–0.0025。颜色接近炭黑(真实反照率仅 0.03)。 (真实天王星有 13 条已知环,取其中主要的 7 条即可。)

木星:极稀薄的尘埃环,1.40 – 1.81,τ ≈ 0.016。必须做得非常淡,原因见第 7.4 节。


3. 核心算法

3.1 时间

JD = unix_ms / 86400000 + 2440587.5
T  = (JD − 2451545.0) / 36525          // 儒略世纪数,J2000 起算

3.2 开普勒方程

给定平近点角 M 和偏心率 e,求偏近点角 E,解 E − e·sin(E) = M:

先把 M 归一化到 [−π, π]
初值:E = M + e·sin(M)·(1 + e·cos(M))
牛顿迭代 8~10 次:E −= (E − e·sin(E) − M) / (1 − e·cos(E))
收敛判据 |ΔE| < 1e-13

对 e < 0.97 的轨道(包括哈雷彗星)这个初值 3~5 次就收敛了。

3.3 轨道根数 → 黄道直角坐标

1. 按 T 更新六个根数:a = a₀ + (da/dt)·T,其余同理
2. 近日点幅角  ω = ϖ − Ω
   平近点角    M = L − ϖ         (归一化到 [−π, π])
3. 解开普勒方程得 E
4. 轨道平面内坐标:
   x' = a·(cos E − e)
   y' = a·√(1 − e²)·sin E
5. 旋转到黄道坐标:
   x = (cosω·cosΩ − sinω·sinΩ·cosI)·x' + (−sinω·cosΩ − cosω·sinΩ·cosI)·y'
   y = (cosω·sinΩ + sinω·cosΩ·cosI)·x' + (−sinω·sinΩ + cosω·cosΩ·cosI)·y'
   z = (sinω·sinI)·x'              + (cosω·sinI)·y'

3.4 坐标系映射

黄道坐标是 z 轴朝黄北极的右手系;WebGL 习惯 y 轴朝上。映射:

world = ( x_ecl,  z_ecl,  −y_ecl )

(这个映射保持右手性。)

银河背景定向:银北极在黄道坐标下约为 (−0.8678, 0.4972, 0.0003)(已是 world 坐标)。用它定向银河带,星空和黄道面的相对姿态就是真的。

3.5 显示比例压缩

海王星在 30 AU、水星在 0.39 AU,线性比例下内行星是一个点。用径向幂律压缩:

r_display = SCALE · r_AU ^ COMPRESS        // 建议 SCALE = 34,COMPRESS = 0.62
方向不变,只压缩径向距离

关键性质:幂律只改径向距离,角位置完全不变,所以行星的黄经、相对位置关系、合冲关系仍然精确。这一点要在 UI 里说明。

天体显示半径同样用幂律放大:

R_display = 0.72 · (radius_km / 6371) ^ 0.42        // 太阳单独给 4.6

必须提供一个「真实距离比例」开关,把 COMPRESS 在 0.62 ↔ 1.0 之间做动画过渡(约 0.4 秒)。切到 1.0 时内行星塌成一团、小行星带变成一条密集环带——这个对比非常有说服力,是整个作品的一个亮点。注意过渡期间轨道曲线要逐帧重建。

3.6 自转

模型矩阵 = 平移(世界位置) × 以自转轴为 Y 的正交基 × 绕 Y 旋转(自转相位) × 缩放(显示半径)。

构造正交基:以 axis 为 Y 列,任取一个不平行的参考向量叉乘得到 X 列,再叉乘得到 Z 列。

自转相位必须限速(详见 7.11):

每帧 Δphase = clamp(2π·Δt_days / 自转周期_天,  −0.22,  +0.22)

3.7 卫星位置

θ = 2π·(JD − J2000)/周期 + 初始相位
局部坐标 = (cos θ · d, 0, sin θ · d),再绕 X 轴倾斜 inc
然后:
  参考面 = 黄道 → 直接加到母行星世界坐标上
  参考面 = 赤道 → 先用母行星的自转轴正交基变换,再加上去

显示距离(有坑,见 7.6):

a₀ = 该行星最内层卫星的真实轨道半径
基准 = max( 2.35,  环外缘 + 最内层卫星显示半径/母行星显示半径 + 0.55 )   // 单位:母行星半径
d = 母行星显示半径 × 基准 × (a_moon / a₀) ^ 0.55

4. 渲染架构

4.1 管线总览

[MSAA 帧缓冲 RGBA16F + DEPTH24]
   1. 背景星云         深度测试关、深度写关
   2. 恒星点           深度测试关、加法混合
   3. 太阳球体         深度测试开、深度写开、背面剔除
   4. 行星/卫星球体    同上
   5. 轨道缎带         深度测试开、深度写关、加法混合
   6. 小行星带         同上
   7. 大气壳层         同上
   8. 环系             深度测试开、深度写关、正常 alpha 混合、不剔除
   9. 彗尾 + 彗发      加法混合
  10. 日冕             加法混合
        ↓ blitFramebuffer 解析 MSAA
[场景纹理 RGBA16F]
        ↓ 亮度阈值提取 + 6 级降采样(13-tap)
        ↓ 6 级升采样(3×3 tent,用加法混合累加,见 7.3)
[泛光纹理]
        ↓
[合成:色散 → 加泛光 → 镜头眩光 → 曝光 → ACES 色调映射 → gamma → 暗角 → 颗粒]
[默认帧缓冲]

需要 EXT_color_buffer_float 扩展做 HDR。拿不到就退回 RGBA8 并降低泛光阈值。

MSAA 用 renderbufferStorageMultisample + blitFramebuffer,4×。如果 checkFramebufferStatus 不完整要能优雅退回非 MSAA 路径。

4.2 各 Pass 要点

背景:全屏三角形,由逆 VP 矩阵反算视线方向。用 3D fbm 噪声画银河带(exp(−(视线·银北极)²·24) 作为带型),叠一层暗尘埃。深空底色必须极暗(线性值约 0.002),否则整个画面发灰(见 7.7)。

恒星:约 11000 个点。方向用拒绝采样向银道面聚集;亮度用 pow(random, 3.6)(大量暗星、少量亮星);颜色从一个 7 档色温调色板按真实光谱型比例采样(M 型偏橙 → B 型偏蓝)。放在 相机位置 + 方向×4000 处,深度测试关。加轻微闪烁。

太阳:见 5.1。

日冕:面向相机的billboard 四边形,加法混合,深度测试开(这样行星经过时会正确遮挡)。画两层:内层 3.1 倍太阳半径、外层 9 倍。用极角上的 fbm 制造放射状冕流。

轨道缎带:不要用 gl.LINES(线宽在多数实现里锁死 1px 且不能抗锯齿)。用 triangle strip 在屏幕空间展宽:每个采样点发两个顶点(side = ±1),顶点着色器里把切线投影到屏幕空间、取法向、按像素宽度偏移。片元着色器按 1−|side| 做柔边。

透明度沿轨道做梯度:在天体当前位置最亮,沿运动反方向拖尾衰减(alpha = 基底 + 峰值·pow(max(0, 1−d·k), 3.2),d 是环绕参数上的落后量)。这个「彗尾式」轨道比均匀亮度的椭圆好看得多。

彗尾:两条。离子尾笔直指向反日方向、偏蓝;尘埃尾更宽、沿速度反方向弯曲滞后、偏暖黄。长度和亮度按 (3/r_AU)² 缩放,r > 6.5 AU 就不画。做成绕尾轴朝相机的 billboard 缎带,CPU 每帧重建(几十个顶点,开销可忽略)。

4.3 后期处理

4.4 深度精度

场景尺度跨度极大(行星半径 0.5,海王星轨道 274,真实比例下 1020)。用动态近远平面:

near = max(0.012, 相机到目标距离 × 0.0035)
far  = max(相机到目标距离 × 14, 4200)

5. 程序化天体表面

所有表面在物体空间用归一化位置 p = normalize(物体空间坐标) 采样噪声——这样纹理会随模型矩阵一起自转,不会「浮」在表面上。

需要的噪声工具:hash13 / hash33、3D 值噪声、fbm(每个倍频之间乘一个旋转矩阵避免轴向对齐)、Worley(F1)、Worley 双距离(F1, F2)。

凹凸映射:不要为了求梯度重复计算高度函数三次。用屏幕空间导数一次搞定:

vec3 dpx = dFdx(worldPos), dpy = dFdy(worldPos);
float dhx = dFdx(height), dhy = dFdy(height);
vec3 r1 = cross(dpy, N), r2 = cross(N, dpx);
float det = dot(dpx, r1);
vec3 grad = (r1*dhx + r2*dhy) / det;
N = normalize(N − 强度 * grad);

5.1 太阳

5.2 岩质天体(水星、月球、木卫四、冥卫一)

撞击坑函数(这是关键,普通的 Worley 阈值只会得到一堆等大的黑点):

在 Worley 循环里同时记录最近特征点的距离 d1 和该胞的随机数 id
每个胞独立的坑半径:rad = 0.13 + 0.22·frac(id·7.31)
t = d1 / rad
碗状凹陷:bowl = (t < 1) ? −(1 − t²) : 0
环形坑缘:rim  = exp(−((t−1)/0.26)²)
高度 = (bowl·0.90 + rim·0.50) · 深度 · 存在标志
反照率调制 = (bowl·0.55 + rim·0.32) · 存在标志       // 坑底暗、坑缘亮
存在标志 = step(id, 0.74) · smoothstep(0.60, 0.40, d1)    // 后一项见 7.5

叠加 4 个倍频(频率约 5 / 12 / 28 / 62,深度约 1.0 / 0.52 / 0.26 / 0.13)得到真实的坑径分布。

月球额外加:月海(低频 fbm 阈值出的暗色玄武岩平原,海内部坑更少更平)、以及亮色辐射纹。

5.3 地球

5.4 金星

只有厚云,看不见地表。中高频 fbm 做涡旋状条纹,随纬度做扭曲,色调乳黄到米白,反照率很高(0.75)。

5.5 火星

5.6 气态巨行星(木星、土星)

5.7 冰巨行星(天王星、海王星)

大体无特征的青/蓝色,只有很淡的带纹和几道明亮的甲烷云条纹。海王星额外加一个暗斑。 注意:明亮云条纹不要放进高度场(见 7.10)。

5.8 其他

5.9 大气

不要用「稍大一点的球 + 菲涅尔边缘光」——那样会得到一个边界很硬的玻璃罩。

正确做法是按视线撞击参数积分光学厚度:

vec3 rd = normalize(片元世界坐标 − 相机);
vec3 oc = 行星中心 − 相机;
float tca = dot(oc, rd);
float b = sqrt(dot(oc,oc) − tca*tca) / 行星半径;      // 归一化撞击参数
float dens = exp(−max(b − 1.0, 0.0) / 标高);          // 星球边缘最厚,向外指数衰减
dens *= 1.0 − smoothstep(1.0, 0.960, b) * 0.86;       // 盘面内视线只穿过半层大气
// 昼夜判定用「视线离球心最近点」的法向,而不是片元法向
vec3 an = normalize(相机 + rd*tca − 行星中心);

再乘上:受光因子、前向(米氏)散射项 pow(dot(rd, L)*0.5+0.5, 4)。 壳层几何体半径取 行星半径 × (1 + 标高×7.5),让指数项在几何体边缘前衰减到 0。 标高取行星半径的 0.02–0.05(真实值约 0.0013,肉眼不可见,必须夸张)。

强度务必调低——盘面内的雾应该只有地表亮度的 5%~12%,边缘才是一道明亮细弧。调过头会把整个昼面糊成白色。

5.10 环系(重点,最出彩的部分)

密度剖面见 2.7。渲染时:

μ_view = max(|dot(环法向, 视线)|, 0.10)          // 下限 0.10 很重要,见 7.4
μ_sun  = max(|dot(环法向, 光线)|, 0.10)
τ_view = d · 1.45 / μ_view
τ_sun  = d · 1.45 / μ_sun
alpha  = 1 − exp(−τ_view)

正面受光(观察者与太阳在环平面同侧):
    亮度 = (1 − exp(−τ_sun)) · 0.85
背光(异侧):
    亮度 = exp(−τ_sun·0.42) · 1.15 · (1 − exp(−τ_sun·0.75))
    // 效果:薄的 C 环、卡西尼缝前向散射而发亮;厚的 B 环挡光而变暗——这是真实现象

两个互相投影(做出来效果极好):

  1. 行星在环上的投影:从环上的点向太阳发射线,与母行星球体做解析求交。设 oc = 环点 − 行星中心,bb = dot(oc, L),若 bb < 0 则算最近距离 dmin = sqrt(dot(oc,oc) − bb²),用 smoothstep(R·0.97, R·1.09, dmin) 得到带半影软边的阴影系数。
  2. 环在行星上的投影:在行星片元着色器里,把太阳方向变换到物体空间,从当前片元沿该方向求与环平面(物体空间 y = 0)的交点,若交点半径落在环范围内,就按 exp(−ringDensity(r)·1.45 / |sunDir.y|) 衰减光照。

6. 交互与界面

相机:轨道式(yaw / pitch / distance)。鼠标拖拽旋转,滚轮缩放,触摸双指缩放。 点击天体聚焦:用屏幕空间投影做拾取(把每个天体投影到屏幕,找距离点击位置最近且在阈值内的),不需要 GPU 拾取。聚焦切换时相机目标点和距离都要平滑过渡(约 1 秒),不要瞬移。

界面元素:

位置内容
左侧天体列表(恒星 / 行星 / 卫星缩进 / 矮行星),点击聚焦
右侧选中天体的信息卡:实时数据(日心距离、黄经黄纬、与地球距离、光行时、近远日点、当前轨道速度)+ 静态参数表 + 科普文字
底部中央播放/暂停、日期时间(UTC)、速度滑块、「此刻」按钮
右下显示开关:轨道 / 名称 / 卫星 / 小行星带 / 彗星 / 真实距离比例 / 辉光
天体旁HTML 覆盖层标签(不要在 canvas 里画字),透明度随天体视半径衰减

实时轨道速度用活力公式:v = 29.7847 · sqrt(2/r − 1/a) km/s(r、a 单位 AU)。

时间速度:对数映射,从实时(1 秒/秒)到约 10 年/秒,支持负值(倒放)。建议:

速度_天每秒 = sign(v) · 1.157407e-5 · 10^(|v|/100 · 8.55)      // v ∈ [−100, 100]

显示时按量级自动切换单位(秒/分/小时/天/年 每秒)。

诚实性要求:当日期超出 1800–2050 年时,在界面上标注「已超出根数有效区间(外推)」。同样,要在某处说明距离比例被压缩了、但角位置是精确的。

快捷键:空格暂停、左右方向键调速、H 隐藏界面。

视觉风格:深色玻璃拟态面板(backdrop-filter: blur)、发丝级边框、小字号大字距的标签、数字用等宽字体且 font-variant-numeric: tabular-nums(否则数字跳动时界面会抖)。


7. 必须避开的坑

这一节是本文档最重要的部分。 下面每一条都是实际写这个东西时踩到的,有几个会导致「看起来完全不工作」但报错为零。

7.1 gl.clear(DEPTH_BUFFER_BIT) 受 depthMask 约束 ★★★

如果调用 clear 时 depthMask 是 false,深度缓冲根本不会被清除。

典型症状:第一帧正常,之后深度缓冲永远停留在第一帧的值。远景看不出来(因为深度值恰好自洽),一旦相机拉近或场景尺度变化,残留的深度值就会把所有几何体剔除掉——太阳变成一个黑盘、行星消失,但星空和背景还在(因为它们关了深度测试)。而且 gl.getError() 返回 0,控制台干干净净。

绝大多数管线会在某个 pass(比如透明物体或后期)里把 depthMask 关掉,然后忘了恢复,下一帧开头的 clear 就废了。

对策:每帧 clear 之前显式 gl.depthMask(true)。

7.2 屏幕空间线宽的单位 ★★

用 triangle strip 展宽线条时,如果把切线投影到屏幕空间得到的法向再除以视口半尺寸,那么这个向量在 NDC 里的长度正好是 1 个像素。此时偏移量应该直接乘「半线宽(像素)」。

如果误以为还要再除一次视口尺寸,线宽会变成约 0.001 像素——轨道线完全不可见,而且没有任何报错。

7.3 泛光升采样的纹理反馈环 ★★

升采样时如果同时把目标 mip 作为纹理采样(为了「读出旧值再相加」),就构成了同一张纹理既读又写的反馈环。行为未定义,某些驱动上悄悄产生垃圾数据,且不一定报 GL 错误。

对策:改用加法混合(blendFunc(ONE, ONE))把模糊结果累加到目标上,着色器只采样源 mip。

7.4 掠射角下薄环变成黑线 ★★

光学厚度公式 τ = d / μ 在 μ→0 时爆炸。当视线接近环平面时,即使是极稀薄的木星尘埃环(d ≈ 0.016)也会算出 alpha ≈ 1,于是在行星脸上画出一道纯黑的横线,看起来像渲染 bug。

对策:(1) 把 μ 的下限设到 0.10 而不是 0.045;(2) 木星环的密度要设得足够低(τ ≈ 0.016)。土星环在掠射时确实应该是一条实线,那是对的。

7.5 Voronoi 特征在胞界被硬切断 ★

如果撞击坑的影响范围(尤其是溅射毯)延伸到超过 Voronoi 胞的边界,在边界处「获胜胞」会突变,导致坑的参数(半径、是否存在)跳变,表面上出现一段段的直线接缝,看起来像划痕。

对策:让坑的所有贡献在接近胞界前就衰减到 0,比如整体乘 smoothstep(0.60, 0.40, d1);同时限制坑半径上限。

7.6 卫星轨道与环系相撞 ★

卫星显示距离如果用一个固定的基准(比如 2.35 倍行星半径),会和土星环的外缘(2.34)几乎重合——土卫六是土星唯一的卫星,比例系数为 1,于是它的球体有一半埋在环里。

对策:基准距离必须为 环外缘 + 最内层卫星自身显示半径 + 余量,见 3.7。

7.7 深空底色 ★★

背景的环境光底噪极易设过高。线性值 0.01 经过 ACES + gamma 之后是约 13% 的灰——整个画面会蒙上一层灰雾,星星和行星全都失去对比。

对策:深空底色的线性值应在 0.002 量级。银河带的亮度也要相应压低。做完之后画面应该是接近纯黑的,星星才会「亮」起来。

7.8 HDR 预算与过曝 ★★★

这是最消耗调试时间的一类问题。ACES 色调映射会把线性值 > 1.5 的部分快速压向白色,同时把饱和度也压掉。所以:

7.9 Worley 的 F1 给不出细胞结构 ★★

F1(到最近特征点的距离)在特征点处最小,所以阈值化 F1 得到的是一堆圆点,不是细胞。太阳的米粒组织、任何「胞壁网络」都需要 F2 − F1(次近距离减最近距离),它在胞的边界处趋近 0。

实现时在同一个循环里维护两个最小值即可,开销几乎不变。

7.10 阶跃特征不能进高度场 ★

如果把一个 smoothstep 出来的锐利特征(云条纹、线纹)以较大权重放进高度场,屏幕空间导数的凹凸映射会把它的边缘放大成很深的暗色刻痕,看起来像划伤。

对策:这类特征进高度场的权重要小(0.1 量级),或者只影响反照率不影响高度。

7.11 高倍速下的自转频闪 ★

时间速度到 1 年/秒时,地球每帧要转约 6 圈,视觉上变成随机抖动的噪点。

对策:自转相位的每帧增量做 clamp(±0.22 弧度左右)。信息面板照常显示真实自转周期。注意 clamp 的是增量不是速度,这样低速时完全精确。

7.12 UV 球的三角形绕序 ★

自己生成球体网格时,绕序搞反会导致背面剔除剔掉正面。麻烦的是:对自发光的球体(太阳),正面和背面看起来几乎一样,你不会立刻发现;等到加了光照的行星才暴露出来。

验证方法:取赤道上一点手算叉积,确认几何法向朝外。

7.13 GLSL 的 smoothstep(a, b, x) 在 a > b 时未定义 ★

想做「反向渐变」时很自然会写 smoothstep(0.7, 0.3, x)。规范规定 edge0 >= edge1 时结果未定义,虽然多数实现能work,但不保证跨平台一致。

对策:自己写一个 sstep(a,b,x) { float t = clamp((x-a)/(b-a), 0.0, 1.0); return t*t*(3.0-2.0*t); },全程用它。

7.14 fbm 的均值是 0.5,不是 0 ★

多倍频 fbm(振幅 0.5, 0.25, 0.125...)的取值范围约 [0,1],均值 0.5。如果把几个 fbm 相加再送进色带,均值会漂到 0.6~0.8,色带的高端被吃满,整个天体变成单一颜色。太阳最容易出这个问题(会变成一个纯白球)。

对策:把送入色带前的值显式重新居中和缩放,并且用纸笔估算一遍典型值落在色带的哪一段。

7.15 readPixels 读默认帧缓冲不可靠 ★

preserveDrawingBuffer: false 时,在 rAF 回调之外读默认帧缓冲会得到全黑。调试时要读自己的 FBO(它们是持久的),或者在同一帧的绘制之后立刻读。


8. 验收标准

8.1 功能

8.2 天文正确性(这些数字可以直接自检)

把日期设到 2026-08-07,检查地球:

项期望值
日心距离≈ 1.014 AU(刚过 7 月初的远日点)
日心黄经≈ 314.6°(等于当天太阳视黄经 135° + 180°)
轨道速度≈ 29.3 km/s

其他检查点:

检查项期望
土星轨道速度≈ 9.7 km/s(平均 9.68)
海王星日心距(2026)≈ 29.9 AU
2026 年的土星环接近侧向、且是背光的暗环(2025 年刚过环平面穿越,太阳相对环平面高度角约 −7.4°)
2033 年的土星环大幅张开、正面受光、行星阴影清晰投在环上
木卫一/二/三周期比接近 1 : 2 : 4,肉眼可见共振节奏
木星特洛伊群长时间快进后仍稳定停留在木星前后 60°
天王星自转轴几乎躺在轨道面内,环系近乎正对观察者
海卫一公转方向与海王星自转方向相反
哈雷彗星2061 年 7 月底过近日点,此时彗尾最长且背向太阳

如果地球黄经算出来差了 180°,多半是 M = L − ϖ 写成了 L + ϖ,或者坐标映射的手性反了。

8.3 视觉


9. 建议的实施顺序

不要试图一次写完,按这个顺序分阶段跑通,每一步都能看到东西:

  1. 骨架:WebGL2 上下文 + 矩阵库 + UV 球 + 轨道相机 + 一个纯色球。(先解决 7.12 绕序)
  2. 轨道力学:开普勒求解 + 九大行星位置 + 时间控制。用无光照的纯色球验证——先对着 8.2 的数字把天文对上,再谈画面。
  3. 深度与帧缓冲:HDR FBO + MSAA + 动态近远平面。(7.1 会在这一步埋雷)
  4. 基础光照:点光源 + 柔和晨昏线,此时应该已经能看出「行星」了。
  5. 后期:泛光 + ACES + 眩光。(7.3、7.7、7.8 集中在这一步)
  6. 程序化表面:先写噪声库,然后一个类型一个类型地做,每做完一个就近距离截图检查。
  7. 轨道线 + 小行星带 + 星空。(7.2 在这一步)
  8. 环系与大气:包括两个互相投影。
  9. 卫星与彗星。(7.6 在这一步)
  10. 界面与信息面板。
  11. 调优:曝光平衡、颜色饱和度、各种强度系数。这一步会比预期花更多时间,建议用 readPixels 量化而不是靠眼睛。

附:一个容易被忽略的整体建议

这个项目的难点不在于任何单个技术点,而在于曝光和色彩的整体平衡。太阳、行星昼面、行星夜面、环系、星空、大气、泛光——这七类东西的亮度跨越好几个数量级,任何一个给多了都会毁掉其余的。

建议的做法是:先把所有东西用中性灰渲染出来、确认几何和运动全对,再一个一个地上色和调亮度,每加一个就用 readPixels 确认线性值落在预期区间。不要一边写着色器一边凭感觉调系数——那样会陷入无休止的来回。