docs(spec): Kerr orbiting planets design (Phase 3.4)

Spec for 5-8 planets on Kerr circular orbits with strong-field Lense-Thirring
nodal precession. Path A: CPU computes position each frame, uploads to the
existing storage buffer; shader unchanged.

Physics: closed-form Ω_φ (Bardeen 1972 eqn 2.16) and Ω_θ (Caltech Ph236
epicyclic vertical frequency), giving exact nodal precession Ω_φ - Ω_θ.
χ=0 degenerates exactly to Schwarzschild (Ω_LT = 0, Newtonian Ω_φ).

Explicitly rejects Mino-time analytic geodesics (Fujita-Hikida): circular
orbits collapse the three-frequency decomposition to Ω_φ alone, elliptic
functions have no WGSL/std backing, and it would break physics.rs's
single-testable-mirror design. Closed-form Kerr frequencies are the right
tool for circular orbits.

Orbit radius bound to k·kerr_isco(χ), so planets track the disk inner edge
as spin changes and span the strong-field region (k=2.5 → r ∈ [1.25, 7.5]).
Random initial attitudes (ChaCha8Rng + UI seed) so the three orbital planes
precess at different Ω_LT rates — the visible Kerr signature.

Decomposes Planet into OrbitParams (immutable elements) + Planet (derived
center), keeps SphereData GPU layout untouched. Adds time_scale UI knob
because Ω_LT at r=8 needs ~25 min per revolution otherwise.
This commit is contained in:
xfy 2026-07-16 16:46:10 +08:00
parent dd1facae39
commit 95968c0575

View File

@ -0,0 +1,404 @@
# Kerr 轨道行星系统Phase 3.4
**日期:** 2026-07-16
**状态:** 设计稿,待实现
**前置:** Phase 2Kerr 光线积分、Phase 3.1(体积盘)
## 目标
让 58 颗行星沿 Kerr 时空的**圆轨道**绕黑洞旋转,轨道面因 LenseThirring 效应绕黑洞自旋轴进动。行星位置每帧由 CPU 用闭式解析公式计算,写入既有 storage buffer渲染端 shader **不改动**
这是 AGENTS.md 里档次 3 的方案Kerr 圆轨 + 强场节点进动。路径 ACPU 算位置 + 现有上传机制shader 零改动。
## 非目标
- **不做完整类时测地线**偏心、倾轨、plunging。理由圆轨道下径向频率 `Ω_r = 0`Mino 时间的三频率分解退化为纯 `Ω_φ`;上 Mino 时间需要椭圆函数WGSL 与 std 都没有),破坏 `physics.rs` 单一可测试镜像的设计原则。这不是"测地线可视化"项目。
- **不改渲染端**`planet_hit``SphereData`、storage buffer 布局全部不动。进动和轨道运动只在 CPU 端,最终位置照常上传。
- **不做行星-行星引力相互作用**。测试粒子近似。
## 物理模型
全部闭式公式,无数值积分。自然单位 `Rs = 1`,故 `M = 0.5``a = χM = 0.5χ`
### 轨道角速度 `Ω_φ`
赤道 prograde 圆轨,[Bardeen 1972, eqn 2.16](https://physics.stackexchange.com/questions/502796/how-to-derive-the-angular-velocity-of-circular-orbits-in-kerr-geometry)
```
Ω_φ(r, χ) = 1 / (r^1.5 + a) // a = 0.5χ
```
`χ = 0``a = 0`,退化为牛顿开普勒 `Ω = 1/r^1.5`
### 节点进动率 `Ω_LT`
轨道面绕黑洞自旋轴Y 轴)的进动率。精确强场形式:
```
Ω_LT(r, χ) = Ω_φ(r, χ) Ω_θ(r, χ)
```
其中 `Ω_θ` 是 Kerr 赤道圆轨的垂直 epicyclic 频率,闭式表达([Caltech Ph236 lec27](http://www.tapir.caltech.edu/~chirata/ph236/2011-12/lec27.pdf)[Okazaki、Kato 等的 标准 epicyclic 频率结果](https://arxiv.org/pdf/1304.6936)
```
Ω_θ² = Ω_φ² · (1 4a·Ω_φ/r + 3a²/r²) // a = 0.5χ
Ω_θ = Ω_φ · sqrt(1 4a·Ω_φ/r + 3a²/r²)
```
**退化验证:** `χ = 0``a = 0`,括号内 = 1`Ω_θ = Ω_φ``Ω_LT = 0`——精确退化为"轨道面固定"Schwarzschild 球对称),满足 AGENTS.md 的核心不变量。
**弱场极限交叉验证:** 大 `r` 时展开 `Ω_φ ≈ r^1.5``Ω_θ ≈ Ω_φ(1 1.5·(2Ma/r³)/Ω_φ · ...)`,最终 `Ω_LT → 2Ma/r³ = χ/r³`M=0.5)。此弱场极限用作测试断言,不用于渲染。
**实现:** `kerr_nodal_precession(r, chi)` 封装上述两式,返回 `Ω_φ - Ω_θ`。注意括号内可能因数值精度略负(极端 r/a 组合),`sqrt` 前用 `.max(0.0)` 钳位。
### 轨道半径
动态绑定 Kerr ISCO`physics.rs:65` 已实现的 `kerr_isco`
```
r = k · kerr_isco(χ)
```
`k` 是 UI 可调的倍数(默认 2.5。ISCO 从 `χ=0` 的 3 缩到 `χ=1` 的 0.5,故默认 `k=2.5``r ∈ [1.25, 7.5]`**横跨强场区**——这是选强场 `Ω_LT` 而非弱场近似的物理理由。
### χ=0 退化验证表
| 量 | χ = 0 | 含义 |
|---|---|---|
| `Ω_φ` | `1/r^1.5` | 牛顿开普勒 |
| `Ω_LT` | `0` | 无进动,轨道面固定 |
| `r` | `k · 3` | Schwarzschild ISCO = 6M = 3 Rs |
## 组件与数据结构
### `OrbitParams`(轨道根数,启动时随机生成)
不可变,除非 UI 改种子重生整个系统。
```rust
#[derive(Component, Clone, Copy)]
pub struct OrbitParams {
pub radius_factor: f32, // k, 乘到 kerr_isco(χ) 上得实际半径
pub inclination: f32, // 轨道面法向与 Y 轴夹角 (rad)
pub longitude_of_node: f32, // 升交点经度 (rad), 决定初始进动相位
pub phase: f32, // 轨道内初始相位 (rad)
}
```
### `Planet`(渲染状态,每帧重算 center
字段不变,但 `center` 从"静态坐标"变成"每帧由 orbit_system 派生"。
```rust
#[derive(Component, Clone, Copy)]
pub struct Planet {
pub center: Vec3, // ← 每帧重算
pub radius: f32,
pub color: Vec3,
pub emissive: bool,
}
```
**为什么拆两个组件:** `OrbitParams` 是不可变根数,`Planet.center` 是派生量。分开后 `orbit_system` 只写 `Planet.center`、读 `OrbitParams`——职责清晰,且轨道力学逻辑可独立测试,不依赖渲染数据结构。符合 AGENTS.md 的 deep-module 原则。
### `SphereData`GPU 布局)— 不变
`render/material.rs:122``SphereData { center, radius, color, emissive, _pad0..2 }` **不改**。进动和轨道运动全在 CPU 算完,只把最终 `center` 写进 buffer。这是路径 A 的核心好处。
## 轨道几何
给定 `OrbitParams` 和当前时间 `t`,计算行星世界空间位置。
### 1. 轨道面法向与基向量
从倾角 `i` 和升交点 `Ω`(用 `longitude_of_node`)解析构造轨道面内两个正交单位基:
```rust
// 轨道面法向 (Y 轴为极轴的球坐标)
let n = Vec3::new(
i.sin() * Omega.cos(),
i.cos(),
i.sin() * Omega.sin(),
);
// 轨道面内基: u 沿升节点方向, v = n × u
let u = Vec3::new(-Omega.sin(), 0.0, Omega.cos());
let v = n.cross(u); // 已单位化 (u, n 均单位且正交)
```
### 2. 进动
整个 `(u, v)` 基绕 Y 轴旋转 `Ω_LT · t`。用旋转矩阵作用:
```rust
let prec_angle = Omega_LT * t;
let cos_p = prec_angle.cos();
let sin_p = prec_angle.sin();
// 绕 Y 轴: x' = x cos + z sin, z' = -x sin + z cos
let u_prec = Vec3::new(
u.x * cos_p + u.z * sin_p,
u.y,
-u.x * sin_p + u.z * cos_p,
);
let v_prec = Vec3::new(
v.x * cos_p + v.z * sin_p,
v.y,
-v.x * sin_p + v.z * cos_p,
);
```
### 3. 行星位置
```rust
let theta = phase + Omega_phi * t;
let r = radius_factor * kerr_isco(chi);
let center = r * (theta.cos() * u_prec + theta.sin() * v_prec);
```
**disk_tilt 的处理:** shader 的 `planet_hit``black_hole.wgsl:632`)会把世界空间球心用 `rot_x(center, -disk_tilt)` 转进盘局部空间。所以轨道平面定义在**世界空间**,倾斜交给 shader——轨道系统不读 `disk_tilt`
## 随机生成
58 颗,全随机散布。用确定性 PRNG`ChaCha8Rng` + UI 种子),改种子时整个系统可复现地重生。
```rust
pub fn spawn_planet_system(
mut commands: Commands,
params: Res<BlackHoleParams>,
seed: Res<PlanetSeed>,
) {
// 先 despawn 现有 (Planet, OrbitParams) — 由系统签名 query 完成
let mut rng = ChaCha8Rng::seed_from_u64(seed.0);
for _ in 0..params.planet_count_target {
let inclination = rng.gen_range(0.0..PI);
let longitude = rng.gen_range(0.0..TAU);
let phase = rng.gen_range(0.0..TAU);
let radius_factor = rng.gen_range(2.0..4.0);
commands.spawn((
OrbitParams { radius_factor, inclination, longitude_of_node: longitude, phase },
Planet {
center: Vec3::ZERO, // 首帧由 orbit_system 填
radius: rng.gen_range(0.8..1.6),
color: random_planet_color(&mut rng),
emissive: false,
},
));
}
}
```
`ChaCha8Rng`(不是 `thread_rng()`)的理由:
1. **可复现**——UI 改种子时整个系统重生,同样种子给同样布局,方便调试和截图对比。
2. **可测试**——CPU 测试能 seed 固定值断言生成的根数。
3. **跨平台一致**——web 与 desktop 同种子给同布局。
依赖:`rand`(通常已是 Bevy 间接依赖)+ `rand_chacha`。若 `rand` 未在 `Cargo.toml` 直接声明,需加上。
## 系统调度
### 新增系统
```rust
// Update: 读 OrbitParams + time + spin, 写 Planet.center
fn orbit_system(
time: Res<Time>,
params: Res<BlackHoleParams>,
mut query: Query<(&OrbitParams, &mut Planet)>,
)
```
逻辑:对每个 `(orbit, planet)`,用上面的轨道几何公式算 `center`,写入 `planet.center`
**调度顺序:** `orbit_system` 必须在 `upload_planets` 之前跑(否则上传的是上一帧位置)。用 Bevy 的 `.before(upload_planets)` 或同一个 `SystemSet` 排序。
### 改动的系统
- **`spawn_default_planet``planets.rs:66`)→ 删除**。由 `spawn_planet_system` 取代。
- **`upload_planets``planets.rs:30`)→ 不变**。它已经每帧全量重写 buffer天然支持动态位置。
- **`mirror_params``plugin.rs:579`)→ 加几行**,把新参数镜像进 uniform见下
### 插件注册(`plugin.rs`
```rust
.add_systems(Startup, spawn_planet_system) // 取代 spawn_default_planet
.add_systems(
Update,
orbit_system.before(crate::scene::planets::upload_planets),
)
```
## 参数镜像end-to-end4 处锁步)
按 AGENTS.md 的"Mirroring a new param"约定,每个新参数要改 4 处。这次新增的参数只影响 CPU 端的轨道计算,**不需要进 GPU uniform**——`Omega_φ``Omega_LT`、轨道几何全在 CPU 算shader 只拿最终 `center`
所以实际的锁步是 **3 处**(不是 4
1. **`BlackHoleParams``params.rs`** — 加字段:
```rust
pub planets_enabled: bool, // 行星系统开关
pub planet_count_target: u32, // 58
pub planet_radius_factor: f32, // k, 默认 2.5
pub planet_seed: u32, // 随机种子
pub planet_time_scale: f32, // 时间加速 (进动很慢, 需放大才可见)
```
2. **`mirror_params``plugin.rs`** — 把 `planet_count` 改为反映实际活动行星数(已有逻辑,保持),其余新字段不进 uniform。
3. **`ui.rs`** — 新增 "Planets" collapsing header见下
**没有第 4 处 shader 改动**,因为 `SphereData``BlackHoleUniforms` 都不改。
### `PlanetSeed` 资源
```rust
#[derive(Resource, Clone, Copy)]
pub struct PlanetSeed(pub u32);
```
种子变更需要触发行星系统重生。两种做法:
- **方案 A简单** UI 里把种子滑动条改成"改了就标记 dirty",下一帧 `spawn_planet_system` 检测到 dirty 就 despawn + respawn。需要一个 `PlanetSystemDirty` 资源。
- **方案 B事件** UI 发 `RespawnPlanets` 事件,专门系统消费。
推荐 **方案 A**——dirty flag 足够,事件系统对这个规模过度设计。
## UI`ui.rs`
新增 collapsing header放在 "Accretion Disk" 之后:
```rust
egui::CollapsingHeader::new("Planets")
.default_open(false)
.show(ui, |ui| {
ui.checkbox(&mut params.planets_enabled, "Enable");
ui.add(egui::Slider::new(&mut params.planet_count_target, 0..=8).text("Count"));
ui.add(egui::Slider::new(&mut params.planet_radius_factor, 1.5..=5.0).text("Radius factor k"));
ui.label(format!("ISCO: {:.3} → r = {:.3}",
crate::physics::kerr_isco(params.spin),
params.planet_radius_factor * crate::physics::kerr_isco(params.spin)));
ui.add(egui::Slider::new(&mut params.planet_seed, 0..=1000).text("Seed"));
ui.add(egui::Slider::new(&mut params.planet_time_scale, 1.0..=200.0).text("Time scale"));
});
```
**Time scale 的必要性:** `Ω_LT` 在 r=8 处约 `0.004 rad/s`,转一圈要 ~25 分钟。不放大根本看不见进动。`time_scale` 是让进动在合理时间内可见的必要旋钮,不是物理作弊——它等价于"模拟时间流速"。
## 测试策略
按 AGENTS.md测试面是 `physics.rs` 的 CPU 镜像。新增的轨道公式应该进 `physics.rs` 并加测试。
### 进 `physics.rs` 的函数
```rust
/// Kerr 赤道 prograde 圆轨角速度 (Rs=1, M=0.5). Bardeen 1972 eqn 2.16.
/// χ=0 退化为牛顿 1/r^1.5.
pub fn kerr_orbital_frequency(r: f32, chi: f32) -> f32 {
let a = 0.5 * chi;
1.0 / (r.powf(1.5) + a)
}
/// Kerr 圆轨节点进动率 (Lense-Thirring, 强场精确). χ=0 返回 0.
pub fn kerr_nodal_precession(r: f32, chi: f32) -> f32 {
let m = 0.5;
let a = chi * m;
let omega_phi = kerr_orbital_frequency(r, chi);
// 垂直 epicyclic 频率 (Caltech Ph236 lec27):
// Ω_θ² = Ω_φ² · (1 4a·Ω_φ/r + 3a²/r²)
let ratio = (1.0 - 4.0 * a * omega_phi / r + 3.0 * a * a / (r * r)).max(0.0);
let omega_theta = omega_phi * ratio.sqrt();
omega_phi - omega_theta
}
```
### 测试用例(`#[cfg(test)]` in `physics.rs`
```rust
#[test]
fn orbital_frequency_reduces_to_newton_at_zero_spin() {
// χ=0: Ω = 1/r^1.5
let r = 8.0;
let newton = 1.0 / r.powf(1.5);
assert!((kerr_orbital_frequency(r, 0.0) - newton).abs() < 1e-6);
}
#[test]
fn nodal_precession_vanishes_at_zero_spin() {
// χ=0: 球对称, 无进动
for r in [4.0, 6.0, 10.0, 20.0] {
assert!(kerr_nodal_precession(r, 0.0).abs() < 1e-6,
"χ=0 at r={} should have zero precession", r);
}
}
#[test]
fn nodal_precession_grows_with_spin() {
// 固定 r, 进动率随 χ 单调增
let r = 6.0;
let p_low = kerr_nodal_precession(r, 0.3);
let p_high = kerr_nodal_precession(r, 0.9);
assert!(p_high > p_low, "precession should grow with spin");
}
#[test]
fn nodal_preception_strong_field_exceeds_weak_field() {
// r<6 强场区: 精确 Ω_LT > 弱场近似 2Ma/r³
let r = 4.0;
let chi = 0.9;
let weak = chi / r.powi(3); // 2Ma/r³ = χ/r³ (M=0.5)
let strong = kerr_nodal_precession(r, chi);
assert!(strong > weak, "strong-field precession should exceed weak at r={}", r);
}
#[test]
fn orbital_radius_tracks_isco() {
// r = k · isco(χ), 随 χ 收缩
let k = 2.5;
let r0 = k * kerr_isco(0.0); // 7.5
let r1 = k * kerr_isco(1.0); // 1.25
assert!(r1 < r0);
}
```
### 轨道几何测试
`u, v, n` 的正交性、绕 Y 轴旋转保持手性、`χ=0` 时位置退化为赤道圆——这些可以用 `ChaCha8Rng` 固定种子生成 `OrbitParams`,断言几何不变量。
## 性能分析
### GPU 开销:零
`planet_hit``black_hole.wgsl:626`)不变。`planet_count` 从 1 涨到 ~8`planet_hit` 内循环(`:630`)从 1 轮涨到 8 轮——每步每像素多 7 次射线-球求交。对 300 steps × ~1.16M 像素desktop这是 ~24 亿次额外求交/帧,但每次求交是 ~10 次 FLOPGPU 上微秒级。
实测验证项:`planet_count=8` vs `planet_count=0` 的帧时间差应 < 1ms
### CPU 开销:可忽略
每帧每颗行星1 次 `kerr_isco` + 1 次 `kerr_orbital_frequency` + 1 次 `kerr_nodal_precession` + 1 次 `sin` + 1 次 `cos` + ~20 次乘加。8 颗 = ~200 次 FLOP/帧,亚微秒。
### 上传开销:与现状相同
`upload_planets` 每帧全量重写 32 颗 `SphereData`1.5KB)的逻辑不变——加轨道运动不增加它。
## 风险与缓解
1. **`Ω_θ` 闭式公式找错** → 用弱场极限 `2Ma/r³` 作交叉验证(测试用例已覆盖)。实现前先在 Python/KerrGeoPy 里算几个参考值对照。
2. **进动太慢看不见**`time_scale` 旋钮(默认 ~50×。这是必要的 UI 权宜,非物理错误。
3. **行星半径撞进吸积盘** → 默认 `k=2.5` 让 r ≥ ISCO×2.5,盘内边缘外。且随机倾角让多数行星不在盘面内。
4. **种子改动不触发重生**`PlanetSystemDirty` flag + `spawn_planet_system` 检测。
5. **`orbit_system``upload_planets` 竞态** → `.before(upload_planets)` 强制顺序。
## 实现顺序(供 writing-plans 参考)
1. `physics.rs`:加 `kerr_orbital_frequency` + `kerr_nodal_precession` + 测试。先确保物理公式正确。
2. `scene/planets.rs`:加 `OrbitParams` 组件 + 轨道几何函数(纯函数,可测)+ `orbit_system`
3. `scene/planets.rs`:加 `spawn_planet_system`(带 despawn 旧行星 + ChaCha8Rng+ 删除 `spawn_default_planet`
4. `params.rs`:加 5 个新字段 + Default。
5. `plugin.rs`:注册新系统 + `.before(upload_planets)` + `PlanetSeed` / `PlanetSystemDirty` 资源。
6. `ui.rs`Planets header。
7. 视觉调参:`cargo run --release`,调 `k` / `time_scale` / 颜色直到好看。
## 参考资料
- [Bardeen, Press, Teukolsky 1972 — Kerr 圆轨角速度 eqn 2.16](https://physics.stackexchange.com/questions/502796/how-to-derive-the-angular-velocity-of-circular-orbits-in-kerr-geometry)
- [Chakraborty 2014 — Kerr 强场 Lense-Thirring 进动](https://arxiv.org/pdf/1304.6936)
- [Fujita & Hikida 2009 — Mino 时间解析解](https://arxiv.org/abs/0906.1420)(本文档解释为何**不用**它)
- [Costa & Natário 2021 — frame-dragging 三种含义](https://www.mdpi.com/2218-1997/7/10/388)
- [KerrGeoPy 文档](https://kerrgeopy.readthedocs.io/)(圆轨特例用同一组 Ω_φ
- [Black Hole Perturbation Toolkit](http://bhptoolkit.org/toolkit.html)