# Kerr 轨道行星系统(Phase 3.4) **日期:** 2026-07-16 **状态:** 设计稿,待实现 **前置:** Phase 2(Kerr 光线积分)、Phase 3.1(体积盘) ## 目标 让 5–8 颗行星沿 Kerr 时空的**圆轨道**绕黑洞旋转,轨道面因 Lense–Thirring 效应绕黑洞自旋轴进动。行星位置每帧由 CPU 用闭式解析公式计算,写入既有 storage buffer;渲染端 shader **不改动**。 这是 AGENTS.md 里档次 3 的方案:Kerr 圆轨 + 强场节点进动。路径 A(CPU 算位置 + 现有上传机制),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 频率([Okazaki 1987](https://articles.adsabs.harvard.edu/pdf/1985PASJ...37..807O);Kato/Fukue/Mineshige "Black-Hole Accretion Disks"): ``` Ω_θ² = Ω_φ² · (1 − 4a√M/r^1.5 + 3a²/r²) // a = 0.5χ, M = 0.5 Ω_θ = Ω_φ · sqrt(1 − 4a√M/r^1.5 + 3a²/r²) ``` **关键:交叉项是 `a√M/r^1.5`(半径 −1.5 次幂),不是 `a·Ω_φ/r`。** 后者会让 `Ω_θ` 偏大、进动偏小,并破坏"进动随 χ 单调增"的物理性质(实现时这个错误被 `nodal_precession_grows_with_spin` 测试当场抓住)。 **退化验证:** `χ = 0` 时 `a = 0`,括号内 = 1,故 `Ω_θ = Ω_φ`,`Ω_LT = 0`——精确退化为"轨道面固定"(Schwarzschild 球对称),满足 AGENTS.md 的核心不变量。 **弱场极限交叉验证:** 大 `r` 展开,`Ω_LT → 2aM/r³ = 0.5χ/r³`(M=0.5,a=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`。 ## 随机生成 5–8 颗,全随机散布。用确定性 PRNG(`ChaCha8Rng` + UI 种子),改种子时整个系统可复现地重生。 ```rust pub fn spawn_planet_system( mut commands: Commands, params: Res, seed: Res, ) { // 先 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