这份文档是什么
下面就是「文档组」的模型实际拿到的全部内容。它由 Claude Opus 5 (Ultracode·xhigh) 在完成自己那版实现之后, 从自身的实现过程反推整理而成——包括数据表、算法公式、渲染管线设计, 以及第 7 节那 15 条实际踩过的坑。
正因为如此,文档组本质上是在做一道已知答案的命题作文。 这也是为什么本站强调:它能说明「详细文档能极大提升产出质量」, 但不能用来给模型能力排序。
本文件是一份实现规格书。数据表和公式请照抄,不要自己回忆或推测数值。 渲染代码、着色器、UI 代码由你自己编写。 第 7 节「必须避开的坑」请在动手前完整读一遍——里面每一条都是实际会遇到的问题。
做一个太阳系的实时运动模型,单个 HTML 文件,双击用浏览器打开即可运行。
两条核心要求:
| 项 | 要求 |
|---|---|
| 文件数 | 1 个 .html,CSS 和 JS 全部内联 |
| 外部依赖 | 零。不允许 <script src>、CDN、import、外部图片/字体/纹理 |
| 离线 | 断网必须能完整运行 |
| 图形 API | WebGL2(原生,不使用 Three.js 等库) |
| 目标帧率 | 1080p 下 60 fps |
| 界面语言 | 中文 |
关于「不使用 Three.js」:这意味着矩阵库、球体网格生成、相机控制、帧缓冲管理都要自己写。如果允许用库,那 600 KB 的库内联进来会让文件体积失控,且泛光管线、环系互投影这类东西反正要写自定义着色器。
纹理来源:不允许外部图片,所以所有天体表面必须用程序化噪声在片元着色器里现场生成。这是本任务的主要难点之一。
六个根数 + 每儒略世纪的变化率。标称有效区间 1800–2050 年。
单位:a 用 AU,其余用度。
| 天体 | a | e | I | L | ϖ (近日点黄经) | Ω (升交点黄经) |
|---|---|---|---|---|---|---|
| 水星 | 0.38709927 | 0.20563593 | 7.00497902 | 252.25032350 | 77.45779628 | 48.33076593 |
| 金星 | 0.72333566 | 0.00677672 | 3.39467605 | 181.97909950 | 131.60246718 | 76.67984255 |
| 地球 | 1.00000261 | 0.01671123 | −0.00001531 | 100.46457166 | 102.93768193 | 0.0 |
| 火星 | 1.52371034 | 0.09339410 | 1.84969142 | −4.55343205 | −23.94362959 | 49.55953891 |
| 木星 | 5.20288700 | 0.04838624 | 1.30439695 | 34.39644051 | 14.72847983 | 100.47390909 |
| 土星 | 9.53667594 | 0.05386179 | 2.48599187 | 49.95424423 | 92.59887831 | 113.66242448 |
| 天王星 | 19.18916464 | 0.04725744 | 0.77263783 | 313.23810451 | 170.95427630 | 74.01692503 |
| 海王星 | 30.06992276 | 0.00859048 | 1.77004347 | −55.12002969 | 44.96476227 | 131.78422574 |
| 冥王星 | 39.48211675 | 0.24882730 | 17.14001206 | 238.92903833 | 224.06891629 | 110.30393684 |
变化率(每儒略世纪,即每 36525 天):
| 天体 | da/dt | de/dt | dI/dt | dL/dt | dϖ/dt | dΩ/dt |
|---|---|---|---|---|---|---|
| 水星 | 0.00000037 | 0.00001906 | −0.00594749 | 149472.67411175 | 0.16047689 | −0.12534081 |
| 金星 | 0.00000390 | −0.00004107 | −0.00078890 | 58517.81538729 | 0.00268329 | −0.27769418 |
| 地球 | 0.00000562 | −0.00004392 | −0.01294668 | 35999.37244981 | 0.32327364 | 0.0 |
| 火星 | 0.00001847 | 0.00007882 | −0.00813131 | 19140.30268499 | 0.44441088 | −0.29257343 |
| 木星 | −0.00011607 | −0.00013253 | −0.00183714 | 3034.74612775 | 0.21252668 | 0.20469106 |
| 土星 | −0.00125060 | −0.00050991 | 0.00193609 | 1222.49362201 | −0.41897216 | −0.28867794 |
| 天王星 | −0.00196176 | −0.00004397 | −0.00242939 | 428.48202785 | 0.40805281 | 0.04240589 |
| 海王星 | 0.00026291 | 0.00005105 | 0.00035372 | 218.45945325 | −0.32241464 | −0.00508664 |
| 冥王星 | −0.00031596 | 0.00005170 | 0.00004818 | 145.20780515 | −0.04062942 | −0.01183482 |
地球那一行严格说是「地月质心」。差别在视觉上不可见,直接当地球用。
rotH = 自转周期(小时),负值表示逆行自转。
tilt = 自转轴与黄道北极的夹角(度)。poleLon = 自转轴在黄道面上的投影所指的黄经(度)。
注意:教科书上写的「转轴倾角」是相对自身轨道面的,这里给的是相对黄道面的,两者差几度。渲染要用下表的值(因为世界坐标系是黄道系),信息面板展示可以用教科书值。 这些是由 IAU 极点赤经赤纬换算过来的,直接抄。
| 天体 | tilt(对黄道) | poleLon | rotH | 教科书倾角(对自身轨道) |
|---|---|---|---|---|
| 水星 | 7.0 | 318.4 | 1407.6 | 0.03° |
| 金星 | 177.4 | 211.2 | −5832.5 | 177.4° |
| 地球 | 23.44 | 90.0 | 23.9345 | 23.44° |
| 火星 | 26.76 | 352.9 | 24.6229 | 25.19° |
| 木星 | 2.25 | 247.8 | 9.925 | 3.13° |
| 土星 | 28.06 | 79.5 | 10.656 | 26.73° |
| 天王星 | 97.77 | 77.7 | −17.24 | 97.77° |
| 海王星 | 28.02 | 319.3 | 16.11 | 28.32° |
| 冥王星 | 112.8 | 137.4 | −153.29 | 119.6° |
自转轴在黄道直角坐标下的方向向量:
axis_ecl = ( sin(tilt)·cos(poleLon), sin(tilt)·sin(poleLon), cos(tilt) )
自转轴必须在惯性系中固定(不随公转改变方向)——这样土星环的张角才会随土星公转周期(29.5 年)自然地开合,天王星才会真的「躺着滚」。
| 天体 | 半径 (km) | 质量 (kg) | 公转周期 | 卫星数 | 表面/云顶温度 |
|---|---|---|---|---|---|
| 太阳 | 696,340 | 1.989×10³⁰ | — | — | 5,772 K |
| 水星 | 2,439.7 | 3.30×10²³ | 87.97 天 | 0 | −173 ~ 427 ℃ |
| 金星 | 6,051.8 | 4.87×10²⁴ | 224.70 天 | 0 | 464 ℃ |
| 地球 | 6,371.0 | 5.972×10²⁴ | 365.256 天 | 1 | −89 ~ 57 ℃ |
| 火星 | 3,389.5 | 6.42×10²³ | 686.98 天 | 2 | −143 ~ 35 ℃ |
| 木星 | 69,911 | 1.898×10²⁷ | 11.86 年 | 95 | −108 ℃ |
| 土星 | 58,232 | 5.683×10²⁶ | 29.46 年 | 274 | −139 ℃ |
| 天王星 | 25,362 | 8.68×10²⁵ | 84.01 年 | 28 | −195 ℃ |
| 海王星 | 24,622 | 1.024×10²⁶ | 164.8 年 | 16 | −201 ℃ |
| 冥王星 | 1,188.3 | 1.303×10²² | 247.9 年 | 5 | −233 ~ −223 ℃ |
信息面板还建议展示:轨道偏心率、轨道倾角、转轴倾角、大气成分、以及一段 2–3 句的科普文字。
轨道用真实周期的匀速圆周运动即可(不必解开普勒方程),但周期必须真实——这样木卫一/二/三的 1:2:4 轨道共振会自然浮现。
| 卫星 | 母行星 | 半径 (km) | 轨道半径 (km) | 周期 (天) | 轨道倾角 | 参考面 |
|---|---|---|---|---|---|---|
| 月球 | 地球 | 1,737.4 | 384,400 | 27.32166 | 5.145° | 黄道面 |
| 木卫一 Io | 木星 | 1,821.6 | 421,700 | 1.769138 | 0.05° | 母行星赤道面 |
| 木卫二 Europa | 木星 | 1,560.8 | 671,034 | 3.551181 | 0.47° | 母行星赤道面 |
| 木卫三 Ganymede | 木星 | 2,634.1 | 1,070,412 | 7.154553 | 0.20° | 母行星赤道面 |
| 木卫四 Callisto | 木星 | 2,410.3 | 1,882,709 | 16.6890 | 0.19° | 母行星赤道面 |
| 土卫六 Titan | 土星 | 2,574.7 | 1,221,870 | 15.945 | 0.35° | 母行星赤道面 |
| 海卫一 Triton | 海王星 | 1,353.4 | 354,759 | −5.876854 | 156.9° | 母行星赤道面 |
| 冥卫一 Charon | 冥王星 | 606 | 19,591 | 6.3872 | 0.08° | 母行星赤道面 |
| 参数 | 值 |
|---|---|
| a | 17.834 AU |
| e | 0.96714 |
| I | 162.26°(逆行) |
| ω(近日点幅角) | 111.33° |
| Ω(升交点黄经) | 58.42° |
| 过近日点时刻 Tp | JD 2446470.96(1986-02-09) |
| 周期 | 75.32 年 = 27512.6 天 |
平近点角直接由过近日点时刻算:M = 2π·(JD − Tp) / P_days。
用顶点着色器为每个小天体解开普勒方程(每个粒子存 a, e, I, Ω, ω, M₀, n, size 八个浮点数),CPU 只在初始化时生成一次。数量建议:主带 9000,希尔达族 900,特洛伊群 2000,柯伊伯带 6000。
主带:a ∈ [2.06, 3.28] AU,用拒绝采样挖出柯克伍德空隙(这是真实存在的共振空隙,做出来非常加分):
| 空隙位置 (AU) | 宽度 σ | 挖除强度 |
|---|---|---|
| 2.502(3:1) | 0.022 | 0.93 |
| 2.825(5:2) | 0.016 | 0.85 |
| 2.958(7:3) | 0.012 | 0.70 |
| 3.279(2:1) | 0.020 | 0.90 |
| 2.065 | 0.010 | 0.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] 像素,加法混合,亮度乘一个随日心距衰减的因子(远的更暗)。
用径向密度函数 ringDensity(r) 描述,r 以母行星半径为单位。这是把光学厚度直接建模,而不是贴图。
土星(1.235 – 2.34 母行星半径):
| 结构 | 范围 | 光学厚度 τ |
|---|---|---|
| C 环 | 1.235 – 1.53 | 0.11 |
| B 环 | 1.525 – 1.95 | 1.20 |
| 卡西尼缝 | 1.95 – 2.03 | 把上面结果削掉 87% |
| A 环 | 2.025 – 2.27 | 0.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 节。
JD = unix_ms / 86400000 + 2440587.5
T = (JD − 2451545.0) / 36525 // 儒略世纪数,J2000 起算
给定平近点角 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 次就收敛了。
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'
黄道坐标是 z 轴朝黄北极的右手系;WebGL 习惯 y 轴朝上。映射:
world = ( x_ecl, z_ecl, −y_ecl )
(这个映射保持右手性。)
银河背景定向:银北极在黄道坐标下约为 (−0.8678, 0.4972, 0.0003)(已是 world 坐标)。用它定向银河带,星空和黄道面的相对姿态就是真的。
海王星在 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 时内行星塌成一团、小行星带变成一条密集环带——这个对比非常有说服力,是整个作品的一个亮点。注意过渡期间轨道曲线要逐帧重建。
模型矩阵 = 平移(世界位置) × 以自转轴为 Y 的正交基 × 绕 Y 旋转(自转相位) × 缩放(显示半径)。
构造正交基:以 axis 为 Y 列,任取一个不平行的参考向量叉乘得到 X 列,再叉乘得到 Z 列。
自转相位必须限速(详见 7.11):
每帧 Δphase = clamp(2π·Δt_days / 自转周期_天, −0.22, +0.22)
θ = 2π·(JD − J2000)/周期 + 初始相位
局部坐标 = (cos θ · d, 0, sin θ · d),再绕 X 轴倾斜 inc
然后:
参考面 = 黄道 → 直接加到母行星世界坐标上
参考面 = 赤道 → 先用母行星的自转轴正交基变换,再加上去
显示距离(有坑,见 7.6):
a₀ = 该行星最内层卫星的真实轨道半径
基准 = max( 2.35, 环外缘 + 最内层卫星显示半径/母行星显示半径 + 0.55 ) // 单位:母行星半径
d = 母行星显示半径 × 基准 × (a_moon / a₀) ^ 0.55
[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 路径。
背景:全屏三角形,由逆 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 每帧重建(几十个顶点,开销可忽略)。
场景尺度跨度极大(行星半径 0.5,海王星轨道 274,真实比例下 1020)。用动态近远平面:
near = max(0.012, 相机到目标距离 × 0.0035)
far = max(相机到目标距离 × 14, 4200)
所有表面在物体空间用归一化位置 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);
limb = 0.24 + 0.76·pow(max(dot(N,V),0), 0.38)。要同时作用于亮度和色温——边缘应该更暗且更红,而不只是更暗。撞击坑函数(这是关键,普通的 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 阈值出的暗色玄武岩平原,海内部坑更少更平)、以及亮色辐射纹。
dot(N,L) < 0 的区域,用高频噪声的高次幂做点状暖黄色自发光。只有厚云,看不见地表。中高频 fbm 做涡旋状条纹,随纬度做扭曲,色调乳黄到米白,反照率很高(0.75)。
hexEdge = 0.115 / cos(mod(经度, 60°) − 30°),在边界处画一圈暗纹。一个很讨喜的细节。大体无特征的青/蓝色,只有很淡的带纹和几道明亮的甲烷云条纹。海王星额外加一个暗斑。 注意:明亮云条纹不要放进高度场(见 7.10)。
abs(fbm − 0.5) 的低值区画细线,三个频率叠加)。abs(fbm−0.5) 的低值区)。不要用「稍大一点的球 + 菲涅尔边缘光」——那样会得到一个边界很硬的玻璃罩。
正确做法是按视线撞击参数积分光学厚度:
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%,边缘才是一道明亮细弧。调过头会把整个昼面糊成白色。
密度剖面见 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 环挡光而变暗——这是真实现象
两个互相投影(做出来效果极好):
oc = 环点 − 行星中心,bb = dot(oc, L),若 bb < 0 则算最近距离 dmin = sqrt(dot(oc,oc) − bb²),用 smoothstep(R·0.97, R·1.09, dmin) 得到带半影软边的阴影系数。exp(−ringDensity(r)·1.45 / |sunDir.y|) 衰减光照。相机:轨道式(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(否则数字跳动时界面会抖)。
这一节是本文档最重要的部分。 下面每一条都是实际写这个东西时踩到的,有几个会导致「看起来完全不工作」但报错为零。
gl.clear(DEPTH_BUFFER_BIT) 受 depthMask 约束 ★★★如果调用 clear 时 depthMask 是 false,深度缓冲根本不会被清除。
典型症状:第一帧正常,之后深度缓冲永远停留在第一帧的值。远景看不出来(因为深度值恰好自洽),一旦相机拉近或场景尺度变化,残留的深度值就会把所有几何体剔除掉——太阳变成一个黑盘、行星消失,但星空和背景还在(因为它们关了深度测试)。而且 gl.getError() 返回 0,控制台干干净净。
绝大多数管线会在某个 pass(比如透明物体或后期)里把 depthMask 关掉,然后忘了恢复,下一帧开头的 clear 就废了。
对策:每帧 clear 之前显式 gl.depthMask(true)。
用 triangle strip 展宽线条时,如果把切线投影到屏幕空间得到的法向再除以视口半尺寸,那么这个向量在 NDC 里的长度正好是 1 个像素。此时偏移量应该直接乘「半线宽(像素)」。
如果误以为还要再除一次视口尺寸,线宽会变成约 0.001 像素——轨道线完全不可见,而且没有任何报错。
升采样时如果同时把目标 mip 作为纹理采样(为了「读出旧值再相加」),就构成了同一张纹理既读又写的反馈环。行为未定义,某些驱动上悄悄产生垃圾数据,且不一定报 GL 错误。
对策:改用加法混合(blendFunc(ONE, ONE))把模糊结果累加到目标上,着色器只采样源 mip。
光学厚度公式 τ = d / μ 在 μ→0 时爆炸。当视线接近环平面时,即使是极稀薄的木星尘埃环(d ≈ 0.016)也会算出 alpha ≈ 1,于是在行星脸上画出一道纯黑的横线,看起来像渲染 bug。
对策:(1) 把 μ 的下限设到 0.10 而不是 0.045;(2) 木星环的密度要设得足够低(τ ≈ 0.016)。土星环在掠射时确实应该是一条实线,那是对的。
如果撞击坑的影响范围(尤其是溅射毯)延伸到超过 Voronoi 胞的边界,在边界处「获胜胞」会突变,导致坑的参数(半径、是否存在)跳变,表面上出现一段段的直线接缝,看起来像划痕。
对策:让坑的所有贡献在接近胞界前就衰减到 0,比如整体乘 smoothstep(0.60, 0.40, d1);同时限制坑半径上限。
卫星显示距离如果用一个固定的基准(比如 2.35 倍行星半径),会和土星环的外缘(2.34)几乎重合——土卫六是土星唯一的卫星,比例系数为 1,于是它的球体有一半埋在环里。
对策:基准距离必须为 环外缘 + 最内层卫星自身显示半径 + 余量,见 3.7。
背景的环境光底噪极易设过高。线性值 0.01 经过 ACES + gamma 之后是约 13% 的灰——整个画面会蒙上一层灰雾,星星和行星全都失去对比。
对策:深空底色的线性值应在 0.002 量级。银河带的亮度也要相应压低。做完之后画面应该是接近纯黑的,星星才会「亮」起来。
这是最消耗调试时间的一类问题。ACES 色调映射会把线性值 > 1.5 的部分快速压向白色,同时把饱和度也压掉。所以:
强度 = clamp(1.5 · (1/r_AU)^0.40, 0.26, 2.1)——注意这是压缩过的衰减(真实的 1/r² 会让海王星完全看不见)。gl.readPixels 从 HDR 场景缓冲里读像素值,看线性值是多少,而不是靠肉眼判断截图。目标:行星盘面典型线性值在 0.3–0.7,只有太阳、云顶、冰盖、环系应该超过 1.0。F1(到最近特征点的距离)在特征点处最小,所以阈值化 F1 得到的是一堆圆点,不是细胞。太阳的米粒组织、任何「胞壁网络」都需要 F2 − F1(次近距离减最近距离),它在胞的边界处趋近 0。
实现时在同一个循环里维护两个最小值即可,开销几乎不变。
如果把一个 smoothstep 出来的锐利特征(云条纹、线纹)以较大权重放进高度场,屏幕空间导数的凹凸映射会把它的边缘放大成很深的暗色刻痕,看起来像划伤。
对策:这类特征进高度场的权重要小(0.1 量级),或者只影响反照率不影响高度。
时间速度到 1 年/秒时,地球每帧要转约 6 圈,视觉上变成随机抖动的噪点。
对策:自转相位的每帧增量做 clamp(±0.22 弧度左右)。信息面板照常显示真实自转周期。注意 clamp 的是增量不是速度,这样低速时完全精确。
自己生成球体网格时,绕序搞反会导致背面剔除剔掉正面。麻烦的是:对自发光的球体(太阳),正面和背面看起来几乎一样,你不会立刻发现;等到加了光照的行星才暴露出来。
验证方法:取赤道上一点手算叉积,确认几何法向朝外。
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); },全程用它。
多倍频 fbm(振幅 0.5, 0.25, 0.125...)的取值范围约 [0,1],均值 0.5。如果把几个 fbm 相加再送进色带,均值会漂到 0.6~0.8,色带的高端被吃满,整个天体变成单一颜色。太阳最容易出这个问题(会变成一个纯白球)。
对策:把送入色带前的值显式重新居中和缩放,并且用纸笔估算一遍典型值落在色带的哪一段。
readPixels 读默认帧缓冲不可靠 ★preserveDrawingBuffer: false 时,在 rAF 回调之外读默认帧缓冲会得到全黑。调试时要读自己的 FBO(它们是持久的),或者在同一帧的绘制之后立刻读。
gl.getError() 全程为 0把日期设到 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 + ϖ,或者坐标映射的手性反了。
不要试图一次写完,按这个顺序分阶段跑通,每一步都能看到东西:
readPixels 量化而不是靠眼睛。这个项目的难点不在于任何单个技术点,而在于曝光和色彩的整体平衡。太阳、行星昼面、行星夜面、环系、星空、大气、泛光——这七类东西的亮度跨越好几个数量级,任何一个给多了都会毁掉其余的。
建议的做法是:先把所有东西用中性灰渲染出来、确认几何和运动全对,再一个一个地上色和调亮度,每加一个就用 readPixels 确认线性值落在预期区间。不要一边写着色器一边凭感觉调系数——那样会陷入无休止的来回。