three.js 绝对是前端 3D 开发的王者,可以在前端实现各种各样的 3D 特效,只有你想不到没有它做不到

基于 three.js,我们可以实现很多漂亮的效果

下面我们来尝试着使用 three.js 实现一个简单的星系效果

准备

首先安装 three.js

1
npm install three

接着编写一个 js 文件并初始化一个基本的场景

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
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
document.body.appendChild(renderer.domElement);
camera.position.z = 20;
function animate() {
requestAnimationFrame(animate);
controls.update();

renderer.render(scene, camera);
}

animate();
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});

引入这个 js 文件到 html 页面中,我们就可以看到一个全屏的黑色场景,这样我们的环境就初始化成功了

实现

接着我们要思考如何创建一个星系效果

首先我们知道星系是由很多个小粒子组成,这些粒子围绕着中心旋转,形成一个圆形的结构,粒子会组成一个个星臂

我们先来定义参数,包括星星的数量,大小,颜色,分支数量,半径

1
2
3
4
5
6
7
8
9
10
let points = null;
const params = {
count: 10000,
size: 0.6,
color: "#2682C7",
branch: 6,
radius: 20,
rotateScale: 0.2,
endColor: "#CF2C72",
};

然后我们需要给星星一个贴图,要不然形成的粒子会是一个个的正方形结构

图片预览

粒子图片预览

图片的地址可以替换成你自己的地址

1
2
3
const particleTexture = new THREE.TextureLoader().load(
"https://img.hoshinagi.top/blog/post/front/three/particle/5.png"
);

接下来我们就可以开始编写生成星系的函数了,three.js 中的物体都是由几何形状和材质组成的,所以我们需要先创建几何形状,然后再创建材质,最后将物体添加到场景中

星星的几何形状是点状,所以我们只需要给一个点的位置就好了

那么点的位置是如何计算的呢?

首先我们得知道当前点在哪个分支,为了使各个分支平均,我们可以在遍历生成点的时候,使用当前遍历的索引模除分支的数量,就可以得到处于那个分支了

得到了点处于那个分支,接着再乘以分支对应的角度,就可以得到当前点相对于中心点的角度了

每个粒子根据其在总数中的索引 i 和分支数 params.branch 计算出它所在分支的角度。这里使用了模运算 % 来确保角度在 0 到 2π 之间循环。

1
const branchAngle = (i % params.branch) * ((Math.PI * 2) / params.branch);

计算粒子与中心的距离,这里使用了 Math.pow(Math.random(), 3) 来使得距离的分布更偏向于中心,即粒子更密集于星系的中心区域。

1
const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);

计算一个随机偏移,让粒子看起来更加自然,每个粒子在 X、Y 和 Z 方向上添加了随机偏移。这些偏移量是通过随机数生成的,并且使用了立方函数 Math.pow(…, 3) 来使得偏移量更偏向于零,即粒子更倾向于在其原始位置附近。

1
2
3
4
5
6
const randomX =
(Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
const randomY =
(Math.pow(Math.random() * 4 - 2, 3) * (params.radius - distance)) / 5;
const randomZ =
(Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;

确定粒子的最终位置

1
2
3
4
5
6
const current = i * 3;
positions[current] =
Math.cos(branchAngle + distance * params.rotateScale) * distance + randomX;
positions[current + 1] = randomY * (1 - distance / params.radius);
positions[current + 2] =
Math.sin(branchAngle + distance * params.rotateScale) * distance + randomZ;
  • X 坐标:使用余弦函数 Math.cos 计算,结合分支角度和距离,再加上 X 方向的随机偏移。
  • Y 坐标:直接使用 Y 方向的随机偏移,并乘以一个因子 (1 - distance / params.radius) 使得距离中心越远的粒子在 Y 方向上的偏移越小。
  • Z 坐标:使用正弦函数 Math.sin 计算,结合分支角度和距离,再加上 Z 方向的随机偏移。

总结上述代码,得到生成星系的函数如下

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
const genertateGalaxy = () => {
const geometry = new THREE.BufferGeometry();
//随机生成位置
const positions = new Float32Array(params.count * 3);
//顶点颜色
const colors = new Float32Array(params.count * 3);
// 中心的颜色
const centerColor = new THREE.Color(params.color);
// 星臂的颜色
const endColor = new THREE.Color(params.endColor);
//循环生成点
for (let i = 0; i < params.count; i++) {
//判断当前的点在那一条分支上
const branchAngle = (i % params.branch) * ((Math.PI * 2) / params.branch);
//当前点距离圆心的距离
const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);

const randomX =
(Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
const randomY =
(Math.pow(Math.random() * 4 - 2, 3) * (params.radius - distance)) / 5;
const randomZ =
(Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;

const current = i * 3;
positions[current] =
Math.cos(branchAngle + distance * params.rotateScale) * distance +
randomX;
positions[current + 1] = randomY * (1 - distance / params.radius);
positions[current + 2] =
Math.sin(branchAngle + distance * params.rotateScale) * distance +
randomZ;

//混合颜色,形成渐变
const mixColor = centerColor.clone();
mixColor.lerp(endColor, distance / params.radius);
colors[current] = mixColor.r;
colors[current + 1] = mixColor.g;
colors[current + 2] = mixColor.b;
}
// 设置点的位置
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
// 设置点的颜色
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
//设置点材质
const material = new THREE.PointsMaterial({
// color: params.color,
size: params.size * (Math.random() * 0.5 + 0.5),
// sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
map: particleTexture,
alphaMap: particleTexture,
transparent: true,
vertexColors: true, //设置顶点的颜色
});
points = new THREE.Points(geometry, material);
scene.add(points);
};

为了让星系旋转,我们需要修改渲染函数如下,让星系绕 Y 轴不断旋转

1
2
3
4
5
6
7
8
9
function animate() {
requestAnimationFrame(animate);
controls.update();
points.rotation.y += 0.005;
if (points.rotation.y > Math.PI * 2) {
points.rotation.y = 0;
}
renderer.render(scene, camera);
}

效果预览

最终代码

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
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
document.body.appendChild(renderer.domElement);
camera.position.z = 20;

const params = {
count: 10000,
size: 0.6,
color: "#2682C7",
branch: 6,
radius: 20,
rotateScale: 0.2,
endColor: "#CF2C72",
};

let points = null;

const particleTexture = new THREE.TextureLoader().load(
"https://img.hoshinagi.top/blog/post/front/three/particle/5.png"
);

const genertateGalaxy = () => {
const geometry = new THREE.BufferGeometry();
//随机生成位置
const positions = new Float32Array(params.count * 3);
//顶点颜色
const colors = new Float32Array(params.count * 3);

const centerColor = new THREE.Color(params.color);
const endColor = new THREE.Color(params.endColor);
//循环生成点
for (let i = 0; i < params.count; i++) {
//判断当前的点在那一条分支上
const branchAngle = (i % params.branch) * ((Math.PI * 2) / params.branch);
//当前点距离圆心的距离
const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);

const randomX =
(Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
const randomY =
(Math.pow(Math.random() * 4 - 2, 3) * (params.radius - distance)) / 5;
const randomZ =
(Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;

const current = i * 3;
positions[current] =
Math.cos(branchAngle + distance * params.rotateScale) * distance +
randomX;
positions[current + 1] = randomY * (1 - distance / params.radius);
positions[current + 2] =
Math.sin(branchAngle + distance * params.rotateScale) * distance +
randomZ;

//混合颜色,形成渐变
const mixColor = centerColor.clone();
mixColor.lerp(endColor, distance / params.radius);
colors[current] = mixColor.r;
colors[current + 1] = mixColor.g;
colors[current + 2] = mixColor.b;
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
//设置点材质
const material = new THREE.PointsMaterial({
// color: params.color,
size: params.size * (Math.random() * 0.5 + 0.5),
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
map: particleTexture,
alphaMap: particleTexture,
transparent: true,
vertexColors: true, //设置顶点的颜色
});
points = new THREE.Points(geometry, material);
scene.add(points);
};
genertateGalaxy();

function animate() {
requestAnimationFrame(animate);
controls.update();
points.rotation.y += 0.005;
if (points.rotation.y > Math.PI * 2) {
points.rotation.y = 0;
}
renderer.render(scene, camera);
}

animate();
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});