跳到主要内容前端实战:使用 Three.js 实现动态星空粒子效果 | 极客日志JavaScript大前端算法
前端实战:使用 Three.js 实现动态星空粒子效果
本案例基于 Three.js 构建了一个交互式星空粒子系统,展示了如何利用 BufferGeometry 高效管理十万级点数据,并通过自定义顶点与片元着色器实现点的颜色渐变、大小动态及空间波动效果。内容涵盖场景初始化、响应式适配、轨道控制器配置及渲染循环优化,适合希望深入理解前端 3D 图形学与 WebGL 底层原理的开发者参考实践。
忘忧1 浏览 JavaScript 的潜力往往被低估。今天通过一个 Three.js 案例,展示如何用代码构建交互式的三维星空。
在开始之前,我们需要了解几个核心概念:Three.js 是用于创建 3D 图形的 JavaScript 库,它封装了底层的 WebGL API;WebGL 负责网页中的 3D 渲染;模块化 JavaScript(ES6)让代码结构更清晰;着色器编程允许我们自定义顶点和片元逻辑;缓冲几何体(BufferGeometry)能高效管理大量点数据;而动画循环则确保流畅的渲染体验。
环境准备与初始化
首先引入必要的库并清理控制台,保持输出整洁。
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 核心库和轨道控制模块。清理控制台是为了避免后续调试信息干扰视线。
场景、相机与渲染器
搭建基础环境需要创建场景、设置背景色、配置透视相机以及初始化 WebGL 渲染器。
let scene = new THREE.Scene();
scene.background = new THREE.Color(0x160016);
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 4, 21);
let renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.);
domElement
场景背景设为深紫色以模拟宇宙氛围。相机位置设定为 (0, 4, 21),提供合适的观察视角。渲染器尺寸随窗口变化,初始时直接应用当前宽高。
响应式与控制器
为了适应不同屏幕尺寸,需监听窗口大小变化事件,同时配置轨道控制器增强交互性。
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
开启阻尼效果(enableDamping)能让相机运动更有惯性质感。注意 enablePan 需要显式赋值,此处设为 false 防止误操作平移破坏构图。
数据准备与顶点生成
接下来是核心部分:生成数万个点并赋予随机属性。我们使用全局 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
);
};
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();
}
这里生成了约 15 万个点。中心区域使用随机方向向量,外围区域利用极坐标公式均匀分布,增加视觉层次感。
几何体、材质与着色器
将数据打包进 BufferGeometry,并通过自定义着色器控制点的颜色渐变和运动轨迹。
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 = `
uniform float time;
attribute float sizes;
attribute vec4 shift;
varying vec3 vColor;
${shader.vertexShader}
`;
shader.vertexShader = 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) );`
);
}
});
着色器逻辑是关键:顶点着色器根据距离中心的远近混合橙紫两色,并利用 shift 数组驱动点在空间中波动;片元着色器处理边缘平滑度,使光点呈现柔和的圆形。
点云组装与渲染循环
最后将几何体和材质结合,加入场景,并启动动画循环。
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);
});
时钟对象提供精确的时间增量,每一帧更新全局时间变量和点云的自转角度,配合 setAnimationLoop 实现流畅的实时渲染。
完整代码
为了方便实践,以下是整合后的完整 HTML 文件代码,可直接保存运行。
<!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 Starfield</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, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 4, 21);
let renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, 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 = `
uniform float time;
attribute float sizes;
attribute vec4 shift;
varying vec3 vColor;
${shader.vertexShader}
`;
shader.vertexShader = 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>
通过这个案例,我们不仅实现了炫酷的视觉效果,还深入理解了 WebGL 渲染管线中着色器的作用。尝试修改参数,比如颜色范围或点的数量,你会发现 JS 在图形领域的上限远超想象。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- 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