跳到主要内容前端练习:使用 Three.js 实现星辰宇宙效果 | 极客日志JavaScript大前端
前端练习:使用 Three.js 实现星辰宇宙效果
介绍如何使用 Three.js 库在前端实现星辰宇宙动画效果。内容涵盖场景相机搭建、渲染器配置、窗口自适应处理、轨道控制器设置以及自定义着色器的编写。通过 BufferGeometry 管理大量粒子点,利用顶点与片元着色器控制点的大小、颜色渐变及运动轨迹,实现流畅的 3D 渲染。文末提供完整的 HTML 示例代码,便于开发者直接运行并理解 WebGL 粒子系统的基本构建流程。
PhpPioneer28 浏览 最终效果预览:

在开始讲解这个炫酷的案例之前,先让我们了解一下本案例所需的前置知识:
Three.js:一个用于创建和显示 3D 图形的 JavaScript 库。代码中导入了 Three.js 的核心库和轨道控制库(OrbitControls),用于处理 3D 场景的创建和相机控制。
WebGL:用于在网页中绘制 3D 图形 的底层 API。Three.js 封装了 WebGL,使得 3D 渲染变得更简单。
模块化 JavaScript:使用 ES6 的模块导入语法 (import) 引入外部库,使代码结构更加清晰。
着色器编程:自定义顶点和片段着色器,通过 onBeforeCompile 方法替换默认着色器,控制点的大小、颜色和运动效果。
缓冲几何体:使用 BufferGeometry 来管理大量点的性能,提升渲染效率。
动画循环:利用 setAnimationLoop 实现流畅的渲染动画,每帧更新控制器状态和场景渲染。
响应式设计:通过 resize 事件监听器,动态调整相机和渲染器的尺寸,以适应浏览器窗口的变化。
1. 导入库和清理控制台
开始我们要先导入库和清理控制台:
import * as THREE from "https://cdn.skypack.dev/[email protected]";
import { OrbitControls } from "https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls";
console.clear();
注释:这部分代码导入了 Three.js 库及其轨道控制功能,并清理了控制台,以便后续输出信息更清晰。
2. 创建场景和相机
在导入库和清理控制台后,我们就需要创建场景和相机:
let scene = new THREE.Scene();
scene.background = new THREE.Color(0x160016);
let camera = new THREE.PerspectiveCamera(, . / ., , );
camera..(, , );
60
window
innerWidth
window
innerHeight
1
1000
position
set
0
4
21
注释:这部分代码创建了一个场景和一个透视相机,设置了相机的位置和背景颜色,为后续的渲染准备基础环境。
3. 创建渲染器
let renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
注释:这里创建了一个 WebGL 渲染器,并设置其大小为窗口的宽高,最后将渲染器的 DOM 元素添加到网页中,以显示渲染结果。
4. 处理窗口大小变化
在创建完渲染器之后,我们需要对后续的窗口的大小的变化进行处理:
window.addEventListener("resize", event => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
注释:这一部分代码监听窗口的大小变化,动态调整相机的长宽比和渲染器的大小,确保在窗口大小变化时,场景仍能正确显示。
5. 控制器设置
处理完窗口大小的变化之后,我们就需要对控制器进行设置了!
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
注释:这部分代码创建了一个轨道控制器,允许用户通过鼠标控制相机旋转和缩放,增强用户交互体验。
6. 创建全局 uniform 变量和点的属性
接下来让我们对全局的 uniform 变量和点的属性进行设置:
let gu = { time: { value: 0 } };
let sizes = [];
let shift = [];
let pushShift = () => {
shift.push(Math.random() * Math.PI, Math.random() * Math.PI * 2, (Math.random() * 0.9 + 0.1) * Math.PI * 0.1, Math.random() * 0.9 + 0.1);
};
注释:定义了一个全局 uniform 变量 gu 用于时间控制,并初始化了两个数组 sizes 和 shift 用于存储点的大小和位置信息。同时,定义了一个函数 pushShift,用于生成随机的位移数据。
7. 创建点的顶点数组
let pts = new Array(50000).fill().map(p => {
sizes.push(Math.random() * 1.5 + 0.5);
pushShift();
return new THREE.Vector3().randomDirection().multiplyScalar(Math.random() * 0.5 + 9.5);
});
注释:这里创建了一个包含 5 万个点的数组 pts,每个点都有随机的大小和方向。通过 randomDirection() 方法生成随机方向的向量,模拟球体内的点。
8. 添加围绕的点
for (let i = 0; i < 100000; i++) {
let r = 10, R = 40;
let rand = Math.pow(Math.random(), 1.5);
let radius = Math.sqrt(R * R * rand + (1 - rand) * r * r);
pts.push(new THREE.Vector3().setFromCylindricalCoords(radius, Math.random() * 2 * Math.PI, (Math.random() - 0.5) * 2));
sizes.push(Math.random() * 1.5 + 0.5);
pushShift();
}
注释:这部分代码在已有的点基础上,再添加 10 万个点。使用极坐标系生成点,使其在一定范围内均匀分布,丰富了场景中的点的数量和分布。
9. 创建几何体和材质
let g = new THREE.BufferGeometry().setFromPoints(pts);
g.setAttribute("sizes", new THREE.Float32BufferAttribute(sizes, 1));
g.setAttribute("shift", new THREE.Float32BufferAttribute(shift, 4));
let m = new THREE.PointsMaterial({
size: 0.125,
transparent: true,
depthTest: false,
blending: THREE.AdditiveBlending,
onBeforeCompile: shader => {
shader.uniforms.time = gu.time;
shader.vertexShader = `
#define PI2 6.28318530718
uniform float time;
attribute float sizes;
attribute vec4 shift;
varying vec3 vColor;
${shader.vertexShader}
`.replace(`gl_PointSize = size;`, `gl_PointSize = size * sizes;`)
.replace(`#include <color_vertex>`, `#include <color_vertex> float d = length(abs(position)/vec3(40.,10.,40)); d=clamp(d,0.,1.); vColor = mix(vec3(227.,155.,0.),vec3(100.,50.,255.),d)/255.;`)
.replace(`#include <begin_vertex>`, `#include <begin_vertex> float t = time; float moveT = mod(shift.x + shift.z * t,PI2); float moveS = mod(shift.y + shift.z * t,PI2); transformed += vec3(cos(moveS) * sin(moveT), cos(moveT), sin(moveS) * sin(moveT)) * shift.w;`);
shader.fragmentShader = `
varying vec3 vColor;
${shader.fragmentShader}
`.replace(`#include <clipping_planes_fragment>`, `#include <clipping_planes_fragment> float d = length(gl_PointCoord.xy - 0.5);`)
.replace(`vec4 diffuseColor = vec4( diffuse, opacity );`, `vec4 diffuseColor = vec4( vColor, smoothstep(0.5, 0.1, d));`);
}
});
注释:在这部分代码中,创建了一个缓冲几何体并设置了点的大小和移动信息。还定义了一个点材质,使用自定义着色器来控制点的大小、颜色和移动效果,提供了动态的视觉效果。
10. 创建点云并添加到场景
let p = new THREE.Points(g, m);
p.rotation.order = "ZYX";
p.rotation.z = 0.2;
scene.add(p);
注释:这里将创建的几何体和材质结合成一个点云对象,并设置其旋转顺序和初始旋转角度,最后将其添加到场景中以进行渲染。
11. 渲染循环
let clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
controls.update();
let t = clock.getElapsedTime() * 0.5;
gu.time.value = t * Math.PI;
p.rotation.y = t * 0.05;
renderer.render(scene, camera);
});
注释:最后,这段代码设置了渲染循环,利用时钟对象控制动画的时间,并不断更新控制器和点云的旋转,最终渲染场景。
通过上述的分层讲解之后,我们就大致的了解每一步都是如何操作并且为什么这么操作的了,为了使读者能更好的理解上述流程,这里我们在进行总结一下:
导入库:使用 Three.js 库和 OrbitControls 模块,准备进行三维场景的创建和交互。
场景和相机设置:创建一个三维场景并设置背景颜色。创建透视相机,设定相机的位置,准备从特定角度观察场景。
渲染器初始化:创建 WebGL 渲染器,设置其大小与窗口相同,并将其添加到网页中以显示内容。
窗口大小自适应:添加事件监听器,以确保在窗口大小变化时,自动调整相机的长宽比和渲染器的大小,保持渲染效果。
控制器设置:创建轨道控制器,允许用户通过鼠标控制相机的旋转和缩放,增强用户交互体验。
全局变量和点属性初始化:定义用于控制动画的全局变量和点的大小、位移数组,准备生成点的运动效果。
点的生成:生成大量随机位置的点,包括中心球体内的点和周围分布的点,以增加视觉复杂度。
几何体和材质设置:创建缓冲几何体,设置点的大小和位移信息。定义点的材质,使用自定义着色器来控制点的大小、颜色和移动效果。
点云创建和添加到场景:将几何体和材质组合成点云对象,并设置初始旋转,最后将其添加到场景中以进行渲染。
渲染循环:使用时钟对象进行动画控制,在每一帧中更新控制器、点云的旋转和动画时间,并渲染场景。
全部代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js Starry Universe</title>
<style>
body { overflow: hidden; margin: 0; }
</style>
</head>
<body>
<script type="module">
import * as THREE from "https://cdn.skypack.dev/[email protected]";
import { OrbitControls } from "https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls";
console.clear();
let scene = new THREE.Scene();
scene.background = new THREE.Color(0x160016);
let camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 4, 21);
let renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", event => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
let gu = { time: { value: 0 } };
let sizes = [];
let shift = [];
let pushShift = () => {
shift.push(Math.random() * Math.PI, Math.random() * Math.PI * 2, (Math.random() * 0.9 + 0.1) * Math.PI * 0.1, Math.random() * 0.9 + 0.1);
};
let pts = new Array(50000).fill().map(p => {
sizes.push(Math.random() * 1.5 + 0.5);
pushShift();
return new THREE.Vector3().randomDirection().multiplyScalar(Math.random() * 0.5 + 9.5);
});
for (let i = 0; i < 100000; i++) {
let r = 10, R = 40;
let rand = Math.pow(Math.random(), 1.5);
let radius = Math.sqrt(R * R * rand + (1 - rand) * r * r);
pts.push(new THREE.Vector3().setFromCylindricalCoords(radius, Math.random() * 2 * Math.PI, (Math.random() - 0.5) * 2));
sizes.push(Math.random() * 1.5 + 0.5);
pushShift();
}
let g = new THREE.BufferGeometry().setFromPoints(pts);
g.setAttribute("sizes", new THREE.Float32BufferAttribute(sizes, 1));
g.setAttribute("shift", new THREE.Float32BufferAttribute(shift, 4));
let m = new THREE.PointsMaterial({
size: 0.125,
transparent: true,
depthTest: false,
blending: THREE.AdditiveBlending,
onBeforeCompile: shader => {
shader.uniforms.time = gu.time;
shader.vertexShader = `
#define PI2 6.28318530718
uniform float time;
attribute float sizes;
attribute vec4 shift;
varying vec3 vColor;
${shader.vertexShader}
`.replace(`gl_PointSize = size;`, `gl_PointSize = size * sizes;`)
.replace(`#include <color_vertex>`, `#include <color_vertex> float d = length(abs(position)/vec3(40.,10.,40)); d=clamp(d,0.,1.); vColor = mix(vec3(227.,155.,0.),vec3(100.,50.,255.),d)/255.;`)
.replace(`#include <begin_vertex>`, `#include <begin_vertex> float t = time; float moveT = mod(shift.x + shift.z * t,PI2); float moveS = mod(shift.y + shift.z * t,PI2); transformed += vec3(cos(moveS) * sin(moveT),cos(moveT),sin(moveS)*sin(moveT)) * shift.w; `);
shader.fragmentShader = `
varying vec3 vColor;
${shader.fragmentShader}
`.replace(`#include <clipping_planes_fragment>`, `#include <clipping_planes_fragment> float d = length(gl_PointCoord.xy - 0.5); `)
.replace(`vec4 diffuseColor = vec4( diffuse, opacity );`, `vec4 diffuseColor = vec4( vColor, smoothstep(0.5, 0.1, d));`);
}
});
let p = new THREE.Points(g, m);
p.rotation.order = "ZYX";
p.rotation.z = 0.2;
scene.add(p);
let clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
controls.update();
let t = clock.getElapsedTime() * 0.5;
gu.time.value = t * Math.PI;
p.rotation.y = t * 0.05;
renderer.render(scene, camera);
});
</script>
</body>
</html>
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online