
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噪声生成旋转纹理
- 事件视界:实现光子球以内的完全吸收效果
- 星空背景:多层噪声叠加产生逼真的星空效果
💡 扩展方向
- 添加引力透镜对背景星空的扭曲效果
- 实现多层次吸积盘
- 添加黑洞两极粒子喷流效果
- 优化性能(使用光线步进优化算法)
本文档由 ThreeLab 编辑整理,基于开源实现整理,如需转载,请注明出处。







京公网安备 11010502038735号
💬 评论区
评论功能即将上线,敬请期待!