Three.js 实现超酷炫的黑洞引力透镜效果!附完整源码

特此感谢作者的贡献!!!

📸 效果预览

黑洞引力透镜效果

(黑洞引力透镜效果,星光线被强大的引力场弯曲,形成壮观的扭曲光环)

🚀 在线演示

👉 点击这里立即体验

🤔 前言

今天给大家带来一个非常震撼的黑洞引力透镜效果!这个效果模拟了爱因斯坦广义相对论中预言的光线弯曲现象,当光线经过大质量天体时会被引力场弯曲,形成独特的环状或弧形图案。

相信大家对 NASA 发布的黑洞照片都不陌生,那壮观的景象让人印象深刻。本篇文章将带你深入解析这个效果的实现原理。

💻 核心代码解析

第一步:基础场景搭建

首先,创建 Three.js 基础环境,包括场景、相机、渲染器和轨道控制器。

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const canvas = document.createElement('canvas');
document.body.appendChild(canvas);

const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    antialias: true,
    alpha: true,
    preserveDrawingBuffer: true
});

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(90, 1, 0.1, 1000);
camera.position.set(0, 3, 10);

const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.maxDistance = 25;
controls.enablePan = false;

第二步:创建黑洞着色器材质(核心)

该实现使用 RawShaderMaterial,核心是光线追踪算法模拟引力透镜效果。

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
const material = new THREE.RawShaderMaterial({
glslVersion: THREE.GLSL3,
uniforms: {
    hole: {
    value: {
        pos: new THREE.Vector3(0, 0, 0),
        rs: 0.05, // 史瓦西半径
    },
    },
    camera: {
    value: {
        pos: new THREE.Vector3(),
        world: new THREE.Matrix4(),
        proj_inv: new THREE.Matrix4(),
    },
    },
    max_steps: { value: 1000 },
    step_size: { value: 0.03 },
    time: { value: 0.0 },
    resolution: { value: new THREE.Vector2() },
    color: { value: new THREE.Color(0xe0eaff) },
},

vertexShader: `
    precision highp float;
    
    struct Hole {
    vec3 pos;
    float rs;
    };
    
    struct Camera {
    vec3 pos;
    mat4 world;
    mat4 proj_inv;
    };
    
    uniform Hole hole;
    uniform Camera camera;
    
    in vec3 position;
    in vec2 uv;
    
    smooth out vec3 V_pos;
    
    void main() {
    // 将UV坐标转换为世界坐标射线
    vec4 pos_vert = vec4((uv - 0.5) * 2.0, -1.0, 1.0);
    pos_vert = camera.proj_inv * pos_vert;
    pos_vert /= pos_vert.w;
    pos_vert.w = 1.0;
    pos_vert = camera.world * pos_vert;
    
    V_pos = pos_vert.xyz;
    gl_Position = vec4(position, 1.0);
    }
`,

fragmentShader: `
    precision highp float;
    
    #define pi 3.141592653589793
    
    struct Hole { vec3 pos; float rs; };
    struct Camera { vec3 pos; mat4 world; mat4 proj_inv; };
    struct Ray { vec3 pos; vec3 dir; float d; };
    
    uniform Hole hole;
    uniform Camera camera;
    uniform uint max_steps;
    uniform float step_size;
    uniform float time;
    uniform vec2 resolution;
    uniform vec3 color;
    
    smooth in vec3 V_pos;
    layout(location = 0) out vec4 c_out;
    
    // 引力加速度计算 - 模拟光线在黑洞引力场中的偏转
    vec3 a(in Ray ray) {
    vec3 dir = normalize(hole.pos - ray.pos);
    float strength = hole.rs / (ray.d * ray.d) * 0.4 + abs(dot(ray.dir, dir)) * 0.001;
    return dir * strength;
    }
    
    // Perlin噪声实现 - 用于吸积盘纹理
    float PerlinNoise(vec3 Position) {
    // ... 噪声计算逻辑
    }
    
    // 星空背景生成
    vec3 stars(vec3 p) {
    // ... 多层噪声叠加产生星星效果
    }
    
    void main() {
    Ray ray_now, ray_new;
    
    // 初始化光线
    ray_now.pos = V_pos;
    ray_now.dir = normalize(V_pos - camera.pos);
    ray_now.d = length(V_pos - hole.pos);
    
    c_out = vec4(0.0);
    
    // 光线追踪主循环
    for (uint i = 0u; i < max_steps; i++) {
        vec3 acc = a(ray_now);
        
        // 欧拉法更新光线
        ray_new.pos = ray_now.pos + ray_now.dir * step_size;
        ray_new.dir = normalize(ray_now.dir + acc);
        ray_new.d = length(ray_new.pos - hole.pos);
        
        // 检测吸积盘交点
        if (ray_now.pos.y * ray_new.pos.y < 0.0) {
        vec3 p = mix(ray_now.pos, ray_new.pos, -ray_now.pos.y / (ray_new.pos.y - ray_now.pos.y));
        float d = length(p - hole.pos);
        
        // 吸积盘范围:15-110倍史瓦西半径
        if (d < hole.rs * 110.0 && d > hole.rs * 15.0) {
            // 旋转纹理 + Perlin噪声
            float angle = mod(atan(p.z, p.x) + pi + time * 1.3, 2.0 * pi);
            vec2 uv = vec2(angle * d * 0.5, d * 13.0 + time * 0.5);
            float s = PerlinNoise(vec3(uv, 0.0)) * 0.5 + 0.5;
            // ...
        }
        }
        
        ray_now = ray_new;
        
        // 光子球检测(1.5倍史瓦西半径)
        if (ray_now.d < hole.rs * 1.5) {
        c_out.rgb *= c_out.a;
        c_out.a = 1.0;
        }
        
        if (c_out.a > 0.99) break;
    }
    
    // 添加星空背景
    if (c_out.a < 0.99) {
        blend(c_out, vec4(stars(ray_now.dir), 1.0));
    }
    }
`,
});

着色器核心原理

组件 实现方式
引力透镜 使用欧拉法步进模拟光线在引力场中的弯曲路径
吸积盘 Perlin噪声生成纹理,配合旋转动画
事件视界 光子球检测(1.5倍史瓦西半径),光线无法逃逸
星空背景 多层Perlin噪声叠加生成深度感

第三步:创建全屏平面

javascript
1
2
3
const geometry = new THREE.PlaneGeometry(2, 2);
const quad = new THREE.Mesh(geometry, material);
scene.add(quad);

第四步:动画循环与GUI控制

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const clock = new THREE.Clock();

const tick = (delta, elapsed) => {
controls.update(delta);
material.uniforms.time.value = elapsed;
material.uniforms.camera.value.pos.copy(camera.position);
material.uniforms.camera.value.world.copy(camera.matrixWorld);
};

const ani = () => {
const elapsed = clock.getElapsedTime();
const delta = clock.getDelta();
tick(delta, elapsed);
renderer.render(scene, camera);
requestAnimationFrame(ani);
};

ani();

// GUI控制面板
import GUI from 'lil-gui';
const gui = new GUI();
gui.add(data, 'rs', 0.02, 0.1, 0.001).name('史瓦西半径');
gui.add(data, 'max_steps', 500, 2000, 1).name('最大步进步数');
gui.add(data, 'step_size', 0.005, 0.05, 0.0001).name('步进步长');
gui.addColor(data, 'color').name('颜色');

🔍 关键技术点

技术点 说明 实现位置
RawShaderMaterial 自定义GLSL着色器,支持GLSL 3.0 材质定义
光线追踪 欧拉法步进模拟光线路径 fragmentShader
引力偏转 根据史瓦西半径计算引力加速度 a() 函数
Perlin噪声 生成吸积盘纹理细节 PerlinNoise() 函数
星空生成 多层噪声叠加产生深度感 stars() 函数
光子球检测 1.5倍史瓦西半径处光线无法逃逸 主循环判断

🔧 常见问题

问题1:性能较差

原因:光线追踪算法复杂度较高,每帧需要进行大量计算

优化方向

  • 降低 max_steps 值(默认1000,可降至500)
  • 增大 step_size 值(默认0.03,可增至0.05)
  • 使用更低的分辨率渲染

问题2:吸积盘不明显

原因:颜色或半径参数设置不当

解决

  • 调整 color 参数选择更明亮的颜色
  • 调整 rs(史瓦西半径)改变整体比例

📌 总结

本文介绍的黑洞引力透镜效果实现了:

  • 光线追踪算法:模拟光线在黑洞引力场中的弯曲
  • 吸积盘渲染:使用Perlin噪声生成旋转纹理
  • 事件视界:实现光子球以内的完全吸收效果
  • 星空背景:多层噪声叠加产生逼真的星空效果

💡 扩展方向

  1. 添加引力透镜对背景星空的扭曲效果
  2. 实现多层次吸积盘
  3. 添加黑洞两极粒子喷流效果
  4. 优化性能(使用光线步进优化算法)

本文档由 ThreeLab 编辑整理,基于开源实现整理,如需转载,请注明出处。