Three.js 黑洞渲染实录:四轮迭代从硬边过渡到物理级光影

为什么要用 Three.js 做黑洞?

2024 年《星际穿越》十周年重映带火了一波黑洞可视化热潮。从科普教育到天文数据可视化,再到互动艺术装置,“在浏览器里渲染一个黑洞"的需求正在变得真实。

Three.js 作为 WebGL 生态最成熟的 3D 框架,天然适合做这件事——它提供了完整的渲染管线、后处理栈和自定义 shader 入口。但黑洞不是普通天体,它的视觉特征几乎全部来自广义相对论效应

  • 事件视界(Event Horizon):光线无法逃逸的边界
  • 引力透镜(Gravitational Lensing):背景星光被弯曲
  • 光子环(Photon Ring):在 1.5 倍史瓦西半径处绕行的光
  • 吸积盘(Accretion Disk):高速旋转的炽热物质
  • 多普勒频移:朝向观察者运动的物质偏蓝,远离的偏红

这些效果在传统 3D 渲染中没有现成方案。标准光照模型、PBR 材质、环境贴图全部失效。你需要的是一套完全自定义的 shader 管线

本文记录一个真实项目的四轮迭代过程,从最初的"黑色球体"到最终的物理级光影效果,拆解每一步的决策逻辑和参数调优细节。


第一轮迭代:硬边黑洞

最朴素的起点

第一轮的目标很简单:在场景中放一个"黑洞”。做法也极其直接——创建一个球体几何体,赋予纯黑材质,关闭光照响应。

1
2
3
4
// fragment shader - 第一版
void main() {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}

MeshBasicMaterialcolor: 0x000000 就能达到同样效果。一个纯黑色的球悬浮在星空中,从概念上讲没错——事件视界确实是一个"看不见"的球面。

问题暴露

但效果极其虚假。原因有三:

问题 表现 根本原因
边缘太硬 球体轮廓锐利如剪纸 没有大气散射过渡
无引力透镜 背景星星不被扭曲 缺少光线偏折计算
无深度感 黑洞像一个平面贴纸 缺少周围光学效应

在真实物理中,黑洞周围的光线会被极端引力弯曲。观察者看到的不是一个"黑色球",而是一个被扭曲的背景星空包裹的暗区。边缘处存在一个明亮的光子环,再往外是吸积盘发出的辐射光。

一个没有引力透镜效果的黑洞,就像一张没有阴影的纸片人——形似而神不似。

决策:必须上自定义 shader

MeshBasicMaterial 的渲染管线完全绕过了 vertex/fragment shader 的自定义空间(虽然技术上可以注入,但不如从头写清晰)。第一轮的核心收获是确认了方向:需要用 ShaderMaterial 接管整个渲染流程


第二轮迭代:加入光子环

光子环的物理背景

在广义相对论中,距离黑洞中心 1.5 倍史瓦西半径处存在一个"光子球"(Photon Sphere)。光子在这个距离上可以做不稳定的圆形轨道运动。对于远处观察者来说,这意味着在事件视界边缘会看到一个极其明亮的光环——光子环。

光子环不是物理实体,它是光线在极端引力场中的几何效应。要在 shader 中模拟它,核心思路是:

  1. 计算视线方向与黑洞中心的距离(impact parameter)
  2. 根据距离判断光线偏折角度
  3. 在光子环半径附近增强亮度

Shader 实现思路

在 fragment shader 中,对每个像素执行以下逻辑:

1
2
3
4
5
6
7
8
float dist = length(viewDir - blackHoleCenter);
float photonRadius = 1.5 * schwarzschildRadius;

// 光线偏折近似
float deflection = schwarzschildRadius / max(dist, 0.001);

// 光子环亮度
float ringGlow = exp(-pow((dist - photonRadius) * ringSharpness, 2.0));

这里的 ringSharpness 是控制光子环宽度的关键参数。值越大,环越窄越锐利;值越小,环越宽越发散。

参数调优过程

光子环的调参是第二轮最耗时的部分。以下是几个关键参数的迭代记录:

参数 初始值 最终值 调优依据
ringSharpness 5.0 18.0 太宽会像雾,太窄会闪烁
ringBrightness 2.0 4.5 需要在 bloom 后处理中保持可见
deflectionStrength 0.5 1.2 过大会导致整个画面扭曲
innerFade 0.0 0.3 事件视界边缘需要微弱的过渡

引力透镜的简化处理

完整的引力透镜需要求解测地线方程(geodesic equation),这在实时渲染中计算量过大。项目中采用的方案是解析近似

  • 对于距离黑洞较远的光线,使用弱场近似(偏折角 ∝ 1/r)
  • 对于近距离光线,使用经验公式插值
  • 背景星空用 cube map 采样,根据偏折后的方向重新查找

这个近似在视觉上已经足够 convincing。普通用户不会去验证测地线解的精度,他们关心的是"看起来像不像"。

第二轮效果

加入光子环后,黑洞从一个"黑色贴纸"变成了一个有光学特征的暗区。边缘的亮环给出了明确的"这是一个引力场"的视觉暗示。但整体仍然缺乏真实感——因为缺少了黑洞最具标志性的视觉元素:吸积盘


第三轮迭代:吸积盘

为什么选择粒子系统而非几何面片

吸积盘的经典渲染方式是用一个扁平的圆盘几何体,贴上噪声纹理,旋转动画。这个方案的问题在于:

  • 边缘太规整:几何面片的边界是数学上完美的圆或椭圆,而真实吸积盘的物质分布是湍流态的
  • 缺乏体积感:面片是二维的,无法表现吸积盘的厚度变化和湍流结构
  • 无法表现粒子轨迹:吸积盘中的物质沿螺旋线向内运动,面片无法自然表达这种动力学

粒子渲染优于曲线渲染,这是项目中确立的核心审美原则。粒子系统天然具有有机、模糊、非几何的质感,更接近真实天体物理中的等离子体视觉效果。

最终方案:用 GPU 粒子系统模拟吸积盘。每个粒子代表一小团等离子体,沿开普勒轨道运动,速度随半径变化。

粒子运动学

吸积盘中的物质运动遵循开普勒定律:内圈快、外圈慢。在 shader 中,每个粒子的角速度为:

1
2
float angularVelocity = sqrt(G * M / pow(radius, 3.0));
float angle = initialAngle + angularVelocity * time;

为了增加视觉丰富度,还叠加了:

  • 径向漂移:粒子缓慢向内螺旋(模拟吸积过程)
  • 垂直扰动:用 Perlin 噪声给粒子一个 z 方向的偏移,模拟盘的厚度
  • 湍流扰动:用 simplex noise 给速度加随机分量

多普勒频移的颜色处理

这是吸积盘渲染中最有趣的部分。由于吸积盘高速旋转,朝向观察者运动的一侧会蓝移(频率升高),远离的一侧会红移(频率降低)。

实现方式是在 fragment shader 中根据粒子的速度矢量与视线方向的点积来计算频移量:

1
2
3
float dopplerShift = dot(velocity, viewDirection) / speedOfLight;
vec3 baseColor = temperatureToColor(temperature);
vec3 shiftedColor = baseColor * (1.0 + dopplerShift * shiftStrength);

shiftStrength 控制频移的夸张程度。物理上真实的频移量很小,视觉上几乎看不出来。为了审美效果,通常会把这个值放大 5-10 倍。

噪声纹理的选择

吸积盘的纹理细节决定了它的"质感"。测试了几种方案:

噪声类型 效果 取舍
Perlin noise 平滑连续 太规则,缺乏湍流感
Simplex noise 各向同性 好一些,但仍偏平滑
Worley noise 细胞状结构 太离散
fBm(分形布朗运动) 多尺度湍流 最终选择

fBm 通过叠加多个频率和振幅递减的噪声层,产生了从大尺度旋涡到小尺度湍流的完整频谱。这正是吸积盘磁流体动力学湍流的视觉特征。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
float fbm(vec2 p) {
    float value = 0.0;
    float amplitude = 0.5;
    float frequency = 1.0;
    for (int i = 0; i < 6; i++) {
        value += amplitude * noise(p * frequency);
        frequency *= 2.0;
        amplitude *= 0.5;
    }
    return value;
}

第三轮效果

吸积盘加入后,整个场景的视觉信息量暴增。黑洞不再是一个孤立的暗球,而是一个动态的、有物质流入的天体系统。旋转的盘面、颜色渐变、粒子轨迹,一切都指向"这是一个活的天体"。

但画面仍然缺少最后一步——光影层次。粒子很亮,但亮度是"平"的,没有 bloom 的光晕,没有体积光的深度暗示。


第四轮迭代:物理级光影

Bloom(辉光后处理)

Bloom 是让高亮区域"溢出"到周围像素的后处理效果。对于黑洞场景,光子环和吸积盘的高温区域都需要强烈的 bloom 来模拟光的高动态范围。

Three.js 的 UnrealBloomPass 提供了三个核心参数:

参数 作用 项目取值
threshold 亮度阈值,超过此值才产生 bloom 0.6
strength bloom 强度 1.8
radius bloom 扩散半径 0.4

调参的关键平衡:threshold 太低会导致整个画面泛白,太高则只有最亮的像素有 bloom,效果不明显。strengthradius 需要配合——高强度小半径产生锐利的光晕,低强度大半径产生柔和的氛围光。

体积光(Volumetric Light)

体积光模拟光线在介质中散射的效果。对于黑洞场景,它的作用是:

  • 给吸积盘增加"厚度"和"密度"感
  • 在光子环周围产生光线散射的暗示
  • 增强整体的"在介质中观察"的氛围

实现上使用径向模糊(Radial Blur)的近似方案:从黑洞中心向外做射线采样,累积亮度。这比真正的体积渲染(raymarching)计算量小几个数量级,视觉效果却非常接近。

后处理管线

最终的后处理栈由以下 pass 组成:

  1. RenderPass:基础场景渲染
  2. UnrealBloomPass:辉光效果
  3. 自定义 VolumetricPass:体积光
  4. FXAA / SMAA:抗锯齿
  5. ToneMappingPass:HDR 到 LDR 的色调映射
  6. VignettePass:暗角,聚焦视觉中心

每个 pass 的顺序很重要。Bloom 必须在色调映射之前,因为它需要 HDR 数据才能正确工作。抗锯齿在 bloom 之后,否则 bloom 会模糊掉锯齿边缘。

色调映射的选择

测试了三种色调映射算法:

  • Reinhard:简单但高光压缩过度,光子环细节丢失
  • ACES Filmic:电影级色调曲线,高光保留好,暗部有层次
  • AgX:新一代算法,色彩保真度更高

最终选择 ACES Filmic,它在保持光子环高亮细节的同时,让暗部的星空背景也有足够的层次感。

最终效果对比

四轮迭代的效果对比:

迭代 视觉特征 核心问题
第一轮 纯黑球体 无光学效应,像贴纸
第二轮 黑球 + 光子环 有光学暗示,但缺乏物质
第三轮 光子环 + 吸积盘粒子 信息量足够,但光影平淡
第四轮 Bloom + 体积光 + 后处理 物理级光影,有机模糊质感

从"一个黑色球"到"一个发光的、有引力透镜效应的、有旋转吸积盘的天体系统",四轮迭代的核心驱动力是不断追问"缺少什么"。每一轮解决一个主要问题,同时暴露下一个问题。


关于审美偏好:为什么粒子渲染是更优解

在黑洞渲染项目中,有一个贯穿始终的审美决策:粒子渲染优于曲线渲染

这个决策的根源在于真实天体物理的视觉本质。吸积盘不是固体,不是面片,不是曲线——它是一团高速旋转的等离子体,由无数微观粒子组成。用粒子系统来渲染它,在概念上就是正确的。

有机模糊质感

粒子渲染天然具有一种"有机模糊"的质感:

  • 粒子之间没有硬边界,靠密度叠加形成连续感
  • 长曝光效果可以通过粒子拖尾(motion trail)自然实现
  • 每个粒子可以独立闪烁,产生湍流态的视觉节奏

相比之下,用 NURBS 曲线或参数曲面来渲染吸积盘,会得到数学上完美但视觉上"太干净"的结果。真实的天体不会这么干净。

纯光效 / Glow 无几何

项目后期进一步去掉了吸积盘的几何骨架,只保留纯光效。粒子不渲染为小球或点,而是渲染为带 bloom 的光点。最终画面上看不到任何"几何体",只有光的密度分布。

这种"无几何"的渲染风格与长曝光天文摄影的视觉语言高度一致——你看到的不是一颗颗星星,而是光在时间上的累积。

追求有机模糊质感,拒绝过度清晰的几何边界,是这个项目最核心的审美原则。它决定了从 shader 参数到后处理栈的每一个选择。


性能考量与取舍

实时渲染黑洞最大的性能瓶颈在两个地方:

  1. 引力透镜的背景采样:每个像素需要重新计算光线方向并采样 cube map,相当于全屏做一次方向重映射
  2. 粒子数量:吸积盘需要至少 50 万粒子才能形成足够密度的视觉效果

项目中的优化策略:

  • 引力透镜计算放在 fragment shader 中,利用 GPU 并行性
  • 粒子运动计算在 vertex shader 中完成,避免 CPU-GPU 数据传输
  • 后处理使用半分辨率渲染,bloom 和体积光在低分辨率下计算后上采样
  • 吸积盘粒子使用 THREE.Points + BufferGeometry,单次 draw call

在 GTX 1060 级别的显卡上,1080p 分辨率可以稳定 60fps。移动端(WebGL 2.0 支持的设备)可以跑到 30fps,需要降低粒子数量到 10 万并关闭体积光。


从项目中学到的三件事

第一,shader 调参是一门经验科学。 没有"正确答案",只有"看起来对"。光子环的宽度、多普勒频移的强度、bloom 的阈值,这些参数的最终值都是在视觉测试中反复微调得到的。物理公式给出的是起点,审美判断决定终点。

第二,迭代比规划更重要。 四轮迭代不是在项目开始前就规划好的。每一轮都是在前一轮的基础上"发现问题、解决问题"。如果你一开始就试图做"完美的黑洞渲染器",大概率会陷入过度工程化的泥潭。

第三,审美原则是技术决策的锚点。 “粒子渲染优于曲线渲染”、“有机模糊优于几何清晰”——这些审美原则不是装饰性的偏好,它们直接决定了技术方案的走向。当面临"用面片还是粒子"、“用曲线还是光点"的选择时,审美原则就是决策依据。

黑洞渲染是一个很好的案例:它的物理基础足够硬核(广义相对论),但它的视觉呈现又完全依赖审美判断。在这个项目中,技术和审美不是两条平行线,而是同一条线的两面。


总结

从四轮迭代的完整路径来看,Three.js 黑洞渲染的核心挑战不在于框架能力,而在于如何将广义相对论的光学效应转化为实时可计算的 shader 逻辑。第一轮确认了自定义 shader 的必要性,第二轮用解析近似解决了引力透镜的计算成本问题,第三轮通过粒子系统实现了吸积盘的有机质感,第四轮用完整的后处理管线赋予了画面物理级的光影层次。

每一步的参数调优都没有标准答案,最终值来自反复的视觉测试和审美判断。“粒子渲染优于曲线渲染”、“有机模糊优于几何清晰”——这些原则贯穿了整个项目的技术决策。

如果你对 WebGL 可视化、天文渲染或 shader 调优感兴趣,欢迎访问 文艺技术笔记 获取更多技术文章。

本文在写作过程中使用了 AI 工具作为辅助,内容基于真实项目经验整理。

广告
广告位预留中 (728x90)

📚 关注公众号,免费获取技术材料

扫码关注公众号,回复「资料」领取:

  • 📘 企业架构设计模板
  • 📗 数据治理实施指南
  • 📙 工业软件技术白皮书
公众号二维码

长按或扫描二维码