简体中文 · English
在线演示 → https://player4086.github.io/gargantua/ · 源码 → https://github.com/player4086/gargantua
一个全屏实时黑洞光线追踪器。屏幕上每一个像素,都是片元着色器在该像素的视线方向上 反向积分史瓦西时空零测地线得到的:光线在弯曲时空里走了多远、穿过吸积盘几次、 以多大频移逃逸到无穷远,全部现算。
场景里没有任何几何体——没有球、没有圆环、没有天空盒,只有一块铺满屏幕的三角形。
| 常见做法 | 本项目 |
|---|---|
| 黑色球体 + 平面圆环摆出黑洞 | 无任何几何体,画面由全屏三角形上的单个片元着色器生成 |
| 预渲染视频 / 截图 / 序列帧 | 逐帧实时积分,成本随视角与画质档变化 |
| 环境贴图假装星空 | 立方体面格点哈希程序化星场 + 银河带,且在逃逸渐近方向采样 |
| 贴一张"亮边"假装光子环 | 光子环是临界曲线附近光线绕转多圈的自然结果 |
| 手动调两侧亮度假装多普勒 | 由 g = √(1−3M/r)/(1−Ω·b_φ) 与 g⁴ 精确计算 |
物理量一览:r_s = 1 · 事件视界 r = 1 · 光子球 r = 1.5 · ISCO r = 3 · 临界碰撞参数 b_crit = 3√3/2 ≈ 2.598
cd gargantua
# A. 内置零依赖静态服务器(推荐)
node tools/serve.mjs --port 8123 # → http://127.0.0.1:8123/
npm start # 等价写法
# B. 任何静态服务器都行(不需要 Node)
python -m http.server 8123
npx serve -l 8123必须通过 HTTP 打开:file:// 下 ES Module 与 import map 会被浏览器安全策略拦截。
需要支持 WebGL2 与 import map 的浏览器(Chrome / Edge / Firefox / Safari 16.4+)。
npm test # 快速档:静态审计 + 单元测试 + GLSL + API + 物理/视觉验收
npm run test:full # 全量档:外加 21 项参数响应、0–9 调试视图、4 个预设
npm run test:unit # 41 项单元测试(在 Node 中直接跑真实模块)
npm run test:glsl # 用真实 GLSL ES 1.0 语法分析器解析全部着色器
npm run test:api # 跨模块 API:THREE 符号、方法解析、uniform 写通、DOM/CSS 契约
npm run test:physics # 物理与图像结构验收(CPU 参考渲染器)npm run ref # CPU 参考渲染器输出 PNG(默认 docs/ref-preset0.png)
npm run docs:images # 渲染 4 预设 + 湍流时序 + 调试视图到 docs/
npm run view # 终端 ASCII 亮度图 + 径向/横向剖面
npm run check:served # 通过运行中的服务器逐一校验 27 个资源
npm run shot # 无头浏览器截图(需本机可运行 Chromium,见 §7)
npm run audio # 重新合成氛围音乐 WAV
npm run vendor # 重新下载并 vendor 指定版本的 Three.js| 输入 | 作用 |
|---|---|
| 拖拽 / 单指滑动 | 环绕旋转(OrbitControls,带阻尼) |
| 滚轮 / 双指捏合 | 拉近拉远(2.6 – 260 r_s) |
| 右键拖拽 / 双指拖动 | 平移观察目标 |
⇧1 – ⇧4 |
四个电影视角预设(1.6 s 缓动过渡) |
Tab |
循环切换视角预设 |
C |
电影镜头循环开/关(缓慢公转 + 倾角呼吸 + 距离呼吸) |
G |
循环切换画质档 |
0 – 9 |
调试视图(见 §3.3) |
P |
参数面板开/关 |
H |
隐藏 / 显示整个 HUD |
空格 |
暂停 / 继续吸积盘时间演化 |
F |
全屏 |
S |
保存 PNG 截图 |
M |
氛围音乐开/关 |
R / ⇧R |
相机复位 / 全部参数复位 |
+ / - |
曝光增减 |
Esc |
关闭参数面板 |
参数面板中拖动滑块调节,双击滑块恢复该项默认值。
| # | 键 | 名称 | 范围 | 默认 | 说明 |
|---|---|---|---|---|---|
| 1 | diskInner |
盘内半径 | 1.7 – 8 r_s | 3.0 | 内缘(ISCO = 3 r_s) |
| 2 | diskOuter |
盘外半径 | 5 – 30 r_s | 12.0 | 外缘,带软边衰减 |
| 3 | diskThick |
盘厚度 | 0.02 – 1.2 | 0.22 | 半厚,随半径张开(flaring) |
| 4 | diskTemp |
峰值温度 | 2000 – 30000 K | 9400 | 驱动黑体色温 |
| 5 | diskBright |
发射亮度 | 0 – 8 | 1.50 | 辐射强度倍率 |
| 6 | diskOpacity |
光学厚度 | 0 – 8 | 2.60 | τ,控制遮挡与半透明 |
| 7 | turbAmp |
湍流强度 | 0 – 1 | 0.66 | 噪声对密度的调制深度 |
| 8 | turbScale |
湍流尺度 | 0.6 – 8 | 4.40 | 噪声空间频率 |
| 9 | diskSpin |
旋转速度 | 0 – 4 × | 1.00 | 开普勒角速度时间缩放 |
| 10 | diskTilt |
盘面倾角 | −80 – 80° | 0 | 盘法线相对世界 +Y 的倾斜 |
| 11 | lensing |
引力透镜强度 | 0 – 2 × | 1.00 | 3M u² 项倍率,1 = 物理 |
| 12 | doppler |
多普勒增亮 | 0 – 1.5 × | 1.00 | g_doppler 指数,1 = 物理 |
| 13 | redshift |
引力红移 | 0 – 1.5 × | 1.00 | g_grav 指数,1 = 物理 |
| 14 | steps |
积分步数 | 80 – 700 | 320 | 每像素测地线积分预算 |
| 15 | starDensity |
星场密度 | 0.3 – 3 × | 1.00 | 三层星场共用倍率 |
| 16 | starBright |
星场亮度 | 0 – 3 × | 1.00 | |
| 17 | galaxyBright |
银河亮度 | 0 – 3 × | 1.00 | |
| 18 | exposure |
曝光 | 0.05 – 4 | 1.00 | 线性 HDR 曝光 |
| 19 | bloomStrength |
泛光强度 | 0 – 3 | 0.92 | UnrealBloomPass |
| 20 | bloomRadius |
泛光半径 | 0 – 2 | 0.62 | |
| 21 | bloomThreshold |
泛光阈值 | 0 – 3 | 0.85 |
另有 8 个不计入 21 项的开关:泛光 / 胶片颗粒 / 暗角 / 色散 / 渐进累积 AA / 盘面动画 / 氛围音乐 / 统计面板。暗角 0.62、颗粒 0.045、色散 0.55 是调校过的固定强度 (即"轻微色散"),随开关启停。
参数表、预设、调试视图、画质档全部集中在
js/params.js——HUD、持久化与 URL 接口都由它派生, 改一处即可全局生效。
| 键 | 名称 | 距离 | 倾角 | FOV | 特点 |
|---|---|---|---|---|---|
⇧1 |
边缘掠影 EDGE-ON GRAND | 26 r_s | 86.5° | 34° | 近边缘视角,次级透镜像与光子环最清晰 |
⇧2 |
极轴俯瞰 POLAR CASCADE | 30 r_s | 22° | 38° | 近极轴,完整环形盘面与螺旋湍流 |
⇧3 |
光子环近观 PHOTON RING | 15 r_s | 84.5° | 42° | 贴近临界曲线,强透镜、多重像、大阴影 |
⇧4 |
远景全貌 WIDE VISTA | 46 r_s | 64° | 28° | 远景构图,银河背景与盘面全景 |
倾角定义:0° = 沿盘法线俯视(正面),90° = 与盘面共面(边缘)。
| 键 | 视图 | 内容 |
|---|---|---|
0 |
最终合成 | ACES + Bloom + 暗角 + 颗粒 + 色散 |
1 |
原始 HDR | 线性 HDR 缓冲,未经色调映射(查看真实动态范围) |
2 |
透镜方向场 | 逃逸方向编码为 RGB,直接显示引力透镜如何扭曲天球 |
3 |
积分步数热图 | 每像素实际积分步数 / 预算;红 = 落入视界 |
4 |
吸积盘单独 | 关闭星空,只看盘面辐射与遮挡 |
5 |
多普勒因子 g | 蓝 = 蓝移(接近侧),红 = 红移(远离侧) |
6 |
引力红移因子 | √(1 − r_s/r) 分布 |
7 |
捕获掩码 | 红 = 落入视界,蓝 = 逃逸到无穷远 |
8 |
星空与银河 | 关闭吸积盘,检查程序化天球 |
9 |
HDR 余量 | 蓝 < 1,绿 1…4,红 > 4(过曝区分析) |
| 档 | 像素比上限 | 积分步数 | 步长 κ | 渐进累积 |
|---|---|---|---|---|
| Standard 标准 | 1.0 | 190 | 0.030 | 关 |
| High 高 | 1.5 | 330 | 0.022 | 关 |
| Cinematic 电影级 | 2.0 | 620 | 0.016 | 开(静止时最多 12 帧) |
- 启动时探测移动端 UA / 粗指针 / 软件渲染 / 核心数 ≤ 4 → 默认 Standard,否则 High。
devicePixelRatio按档封顶,超大视口再做面积自适应缩放,保证片元着色器维持交互帧率。- 连续 4 次采样低于 24 FPS 时自动降档并提示(可手动
G切回)。 - 标签页隐藏时暂停渲染循环,回到前台自动恢复并丢弃累积的时间差。
- 触摸设备
touch-action: none;窄屏下 HUD 折叠为底部控制条。
任何参数都能写进 URL,用于复现一个确定性画面(?shot=1 会关闭电影镜头、开启累积 AA、冻结音频):
http://127.0.0.1:8123/?shot=1&preset=0&t=2&quality=cinematic&accum=24
http://127.0.0.1:8123/?debug=3&cam=18,75,210,28
http://127.0.0.1:8123/?p=diskTemp:14000,diskBright:3.2,doppler:1.4
http://127.0.0.1:8123/?reset=1&hud=0
| 参数 | 说明 |
|---|---|
shot=1 |
确定性截图模式:关闭电影镜头与音频,开启累积 AA,渲染完成后置 window.__GARGANTUA_READY__ = true |
preset=0..3 |
视角预设 |
cam=r,inc,az,fov |
直接指定相机球坐标 |
debug=0..9 |
调试视图 |
quality=standard|high|cinematic |
画质档 |
t=<秒> |
冻结盘面时间(可复现同一湍流形态) |
p=key:value,key:value |
覆盖任意参数(非法值自动钳制) |
accum=1..64 |
累积帧数 |
hud=0 / cine=0 |
隐藏 HUD / 关闭电影镜头 |
reset=1 |
忽略并清除 localStorage 中的持久化状态 |
w= / h= |
供自动化脚本记录目标分辨率(配合 CDP 视口覆盖使用) |
GARGANTUA.setParam('diskTemp', 12000); // 任意 21 项参数
GARGANTUA.setQuality('cinematic');
GARGANTUA.setPreset(0);
GARGANTUA.setDebug(3);
GARGANTUA.setCamera({ distance: 20, inclination: 80, azimuth: 45, fov: 36 });
GARGANTUA.setTime(2.0); // 冻结湍流时刻
await GARGANTUA.renderFrames(24); // 累积 24 帧(渐进 AA)
const stats = GARGANTUA.analyze(); // 画布像素统计
const png = GARGANTUA.capture(); // data:image/png;base64,…
GARGANTUA.screenshot(); // 触发下载
GARGANTUA.stats(); // 帧率 / 分辨率 / 步数 / 渲染信息
GARGANTUA.build; // 当前模块版本戳analyze() 返回 mean / max / p95 / p99 / darkFrac / brightFrac / center / ring / histogram,
可直接用于自动化视觉验收——本项目的验收脚本正是这么做的。
node tools/shot.mjs --url "http://127.0.0.1:8123/?shot=1&preset=0&t=2" \
--out docs/shot-preset0.png --w 1600 --h 900 --accum 24脚本用零依赖 CDP 驱动本机 Chromium:等待 __GARGANTUA_READY__、收集控制台错误与未捕获异常、
调用 analyze() 输出像素统计并保存 PNG;任何控制台错误都会让退出码非 0。
完整推导与数值稳定性清单见 docs/PHYSICS.md。要点:
测地线积分. 由相机位置与视线确定轨道平面,以 u = 1/r 为变量积分
d²u/dφ² = −u + 3M u²(四阶 Runge–Kutta)。步长 h = clamp(κ/u, h_min, h_max),
并额外限制单步 |Δu| ≤ 0.45u——没有这一条,远场光线会一步跨到 u < 0
而被误判成落入视界。
终止条件. u ≥ 1/r_s → 落入事件视界,返回纯黑;到达逃逸半径且向外 → 采样天球。
光子环与多重像. 临界曲线附近的光线会在光子球附近绕转多圈,每次穿过盘面都被记一次, 于是次级、三级透镜像自动出现,极窄的亮环也自然成形。
吸积盘. 盘面是过原点的倾斜平面。每一步检测 dot(pos, n_disk) 是否变号,
插值出交点后做发射–吸收合成(前向透射率),因此近侧盘面会遮挡远侧与背景星空。
频移. 对开普勒圆轨道发射体,精确给出
g = ν_obs / ν_emit = √(1 − 3M/r) / (1 − Ω·b_φ), Ω = √(M/r³), b_φ = L_z/E(沿光线守恒)
观测温度 T_obs = g·T_emit,玻尔兹曼亮度按 g⁴ 缩放——这正是边缘视角下接近侧比远离侧亮数倍的原因。
其余. 盘面温度用 Shakura–Sunyaev 剖面 T ∝ r^(−3/4)(1−√(r_in/r))^(1/4);
颜色用 Kim 等人 CIE 色度拟合 + XYZ→线性 sRGB;天球用立方体面格点哈希星场(三层)
加银河带,并在逃逸渐近方向采样,因此引力透镜对星空的扭曲是免费且物理正确的;
盘面湍流在共转方位角 ψ' = ψ − Ω(r)·t 上采样 3D 噪声,较差自转自动把结构剪切成螺旋细丝。
gargantua/
├── index.html 全屏 canvas + import map + 三层遮罩
├── README.md / README.en.md 中文 / English 说明文档
├── CHANGELOG.md 版本变更
├── LICENSE MIT
├── css/style.css HUD / 面板 / 遮罩 / 响应式
├── js/
│ ├── main.js 入口:启动、window.GARGANTUA、首帧看门狗
│ ├── app.js 渲染循环、状态机、上下文恢复、截图/分析 API
│ ├── params.js 21 项参数 + 4 预设 + 10 调试视图 + 3 画质档(唯一真源)
│ ├── build.js 版本戳(同时是模块缓存失效查询串的真源)
│ ├── store.js localStorage 持久化(版本化、防御式读取)
│ ├── camera.js OrbitControls + 球坐标 + 电影镜头 + 缓动过渡
│ ├── postfx.js RenderPass → 累积 AA → UnrealBloom → 最终合成
│ ├── hud.js 由参数表生成的 HUD 与面板
│ ├── audio.js 氛围音乐(WAV + WebAudio 处理,失败时程序化兜底)
│ ├── quality.js 设备探测与像素比/步数策略
│ └── shaders/
│ ├── common.glsl.js 哈希 / 噪声 / fbm / 黑体 / ACES / sRGB
│ ├── geodesic.glsl.js ★ 测地线积分 + 吸积盘 + 星空 + 银河
│ └── post.glsl.js 色散 / 曝光 / ACES / 暗角 / 颗粒 / 累积 / HDR 余量
├── vendor/three/ 本地 Three.js r160.1(构建产物 + 10 个 addon)
├── assets/
│ ├── audio/gargantua-ambient.wav 48 s 无缝循环无人机音(程序化合成)
│ └── img/favicon.svg
└── tools/ 开发/测试工具(不参与运行时)
├── serve.mjs 零依赖静态服务器
├── verify.mjs 一键全量验证流水线(--report 输出报告)
├── unit.mjs 41 项单元测试(在 Node 中跑真实模块)
├── glsl-check.mjs 真实 GLSL 解析器 + 作用域 + uniform 契约审计
├── api-check.mjs THREE 符号 / 方法解析 / uniform 写通 / DOM-CSS 契约
├── refcheck.mjs GLSL ↔ CPU 参考实现数值字面量一致性
├── accept.mjs 物理与图像结构验收(含弱场偏折、临界碰撞参数)
├── refrender.mjs ★ 片元着色器的 CPU 逐行对照实现(可输出 PNG)
├── refview.mjs 终端 ASCII 亮度图 + 剖面分析
├── raydebug.mjs 单像素逐步光线追踪日志
├── shot.mjs 零依赖 CDP 无头浏览器截图/验收
├── check-served.mjs 通过运行中的服务器校验全部资源
├── make-docs-images.mjs 文档图像批量渲染
├── make-audio.mjs 氛围音乐 WAV 合成器
├── push-via-api.mjs git 端点不可用时的 REST API 推送兜底
└── fetch-vendor.mjs Three.js vendor 下载器
docs/
├── PHYSICS.md 完整物理推导与数值稳定性清单
├── TEST-REPORT.md 验证流水线的完整输出(--report 自动生成)
├── ref-preset0…3-*.png 4 个视角的 CPU 参考渲染
├── ref-timelapse-t*.png 盘面湍流时序(t = 0 / 2.5 / 5 / 7.5 s)
└── ref-debug0…8.png 0–8 号调试视图
node tools/verify.mjs 一条命令跑完全部检查,当前 111/111 通过,
完整输出见 docs/TEST-REPORT.md。验证分为四层:
① 静态审计. 资源清单、无外部依赖(运行时零网络请求)、import map 正确性、 全部 JS 以 ESM 解析、相对 import 可解析、缓存版本图一致、vendored three.js 完整性、 WAV 头、CSS 括号与响应式断点。
② 跨模块 API 审计. 每个 THREE.* 符号都存在于 vendored 构建;每个
this.<协作对象>.<方法>() 都能解析到已声明的方法;每个被 JS 切换的 CSS 类都有样式规则;
每个 getElementById 都能在 HTML 中找到。
这条检查在开发中真实抓出过两个会导致黑屏的缺陷:uResolution 从未写入、
renderer.setSize() 从未调用(画布会停在默认 300×150 后备缓冲)。
③ GLSL 审计. 用真实 GLSL ES 1.0 语法分析器解析运行时真正交给 WebGL 的字符串
(不是重建的副本),检查语法、作用域、uniform 契约(名称/类型/是否真的被赋值),
并断言「着色器调用的每个辅助函数都存在于装配后的源码」与「不存在未定义函数调用」。
后者能精确复现后处理通道漏拼 GLSL_COMMON 时驱动报出的
linearToSRGB / acesFilmic / hash13 / luma 四个符号。
④ 物理与图像结构验收. tools/refrender.mjs 是片元着色器的 CPU 逐行对照实现,
tools/refcheck.mjs 逐函数比对两者的数值常量(14/14 一致),保证它不会悄悄偏离;
在此基础上直接积分测地线方程验证物理量,并对渲染结果做结构断言:
physics · b < b_crit 被捕获 captured=true, minR=1.000
physics · b > b_crit 逃逸 φ=8.053 rad
physics · 临界曲线多次绕转 φ=11.26 rad (1.79 圈)
physics · 弱场偏折角 ≈ 4M/b err=2.35%
physics · ISCO 轨道速度 v=0.500000 c
image · 事件视界阴影深黑 0.0238 / 0.0039
image · 多普勒增亮左右不对称 ratio=2.05
image · 多次吸积盘穿越(垂直切面) 5 条亮带
image · 引力透镜次级像(阴影上下均有盘光) top=0.0449 bottom=0.0987
image · 星空背景极暗 0.0129
关于 GPU 侧验证. 本项目的开发沙箱无法运行 Chromium(多进程 IPC 被环境策略拦截), 因此 GPU 侧由上述四层证据链替代;浏览器侧脚本(
tools/shot.mjs、tools/check-served.mjs) 随源码交付,在本机执行即可得到真实截图与 HTTP 层校验。
本项目没有构建步骤,也就没有内容哈希文件名。为了不让「新 index.html + 旧模块」
这种混合状态出现,所有模块的相对 import 都带 ?v= 版本号,版本号只在
js/build.js 声明一次,tools/verify.mjs 会断言它们全部一致:
import { PostFX } from './postfx.js?v=1.0.1'; // 缓存键包含查询串刷新发布版本时:改 js/build.js 的 VERSION,把 js/ 与 index.html 里的
?v= 字面量一起替换(一次全局替换),再跑一次 verify 检查是否漏改。
部署到静态托管后如果仍然看到旧版本:
- 最可靠:打开带查询串的地址,例如
https://<user>.github.io/gargantua/?v=1.0.1—— 文档 URL 变化会绕过 HTML 缓存,模块 URL 自带版本号会绕过模块缓存。 - 或者
Ctrl+Shift+R强制刷新 / 使用无痕窗口。 - 控制台首行会打印当前模块版本,可直接与部署版本比对;着色器编译失败时错误浮层也会打印。
打开后弹出「无法启动渲染器 · 着色器编译/链接失败」,报 no matching overloaded function?
几乎总是浏览器缓存了旧模块,而不是代码问题。判断方法:three.js 会在片元着色器前
拼接约 55 行的 prefix,所以 驱动行号 − 55 = 源码行号。用带版本的地址打开即可绕过
(见 §8),或 Ctrl+Shift+R / 无痕窗口。
提示「无法创建 WebGL 上下文 / WebGL 初始化失败」?
浏览器未启用硬件加速,或设备不支持 WebGL2。Chrome/Edge 可检查
设置 → 系统 → 使用硬件加速,或在地址栏打开 chrome://gpu 查看 WebGL2 状态。
提示「渲染超时:GPU 未能在 30 秒内完成首帧」?
多见于软件渲染(SwiftShader)或驱动异常。页面有 30 秒看门狗以避免无声黑屏;
可先降到 Standard 档(G)再试。
用 file:// 直接双击打开没反应?
ES Module + import map 在 file:// 下会被安全策略拦截,必须通过 HTTP 打开(见 §1)。
听不到氛围音乐?
浏览器禁止无交互自动播放。按 M 或点击 HUD 的「氛围音乐」按钮即可(这也构成用户手势);
音频资源不可用时会自动退化为程序化合成。
帧率低 / 画面很糊?
脚本会在帧率持续偏低时自动降档并提示;也可手动 G 循环三档。移动端默认从 Standard 起步。
怎么拿到干净的高分辨率截图?
按 S 直接下载;或用确定性模式 ?shot=1&preset=0&t=2&quality=cinematic&accum=24
(冻结湍流 + 累积 AA);脚本化见 §4.3。
- 仅实现史瓦西(无自旋)度规;克尔黑洞的参考系拖拽不在范围内。
- 吸积盘采用薄板 + 光学厚度近似(单次穿越的发射–吸收解析合成),不是完整的辐射转移求解器。
- 湍流是程序化噪声而非磁流体力学模拟,仅用于视觉与演化感。
- 积分步数有预算上限,极端贴近临界曲线的光线在低步数下可能少绕半圈(调试视图
3可直接看到)。 - 渐进累积 AA 在相机/参数变化时自动重置,因此只有静止时才会收敛到干净边缘。
代码以 MIT 许可发布。vendor/three/ 为 Three.js r160.1 的原始构建产物,
遵循其 MIT 许可。版本变更见 CHANGELOG.md。