一、引言
在Web开发中,利用Three.js库可以创建出令人惊叹的3D可视化效果。本文将详细解析一段使用Three.js实现的特定粒子效果的代码,帮助读者深入理解其背后的原理和实现方式,官方示例地址:https://github.com/mrdoob/three.js/blob/master/examples/webgl_points_sprites.html接下来我们逐行进行分析。
二、模块导入部分
<script type="module">
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
</script>
在代码开头,通过 import 语句导入了必要的模块。
- import * as THREE from 'three';:这行代码导入了整个 THREE 库,THREE 是Three.js库的核心命名空间,后续所有关于创建3D场景、物体、相机等相关的类和函数都将从这个命名空间中获取。例如,我们后面会使用 THREE.PerspectiveCamera 来创建透视相机,这里的 PerspectiveCamera 就是通过导入的 THREE 命名空间来访问的。
- import Stats from 'three/addons/libs/stats.module.js';:引入了 Stats 模块,它通常用于在页面上显示性能相关的统计信息,比如帧率等,方便开发者监控场景渲染的性能表现。
- import { GUI } from 'three/addons/libs/lil-gui.module.min.js';:导入了 GUI 模块,这个模块可以帮助我们创建图形用户界面,用于在运行时方便地调整场景中的一些参数,例如我们后面会用它来控制粒子纹理的显示与否。
三、全局变量声明部分
let camera, scene, renderer, stats, parameters;
let mouseX = 0, mouseY = 0;
let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;
const materials = [];
- 首先声明了多个变量,camera、scene、renderer 是Three.js中构建3D场景的核心元素,camera 代表相机,用于定义观察场景的视角;scene 是整个3D场景的容器,所有的3D物体都会添加到这个场景中;renderer 则负责将场景通过WebGL渲染到页面上。stats 用于性能统计,parameters 用于存储一些和粒子材质相关的配置参数。
- mouseX 和 mouseY 用于记录鼠标在页面上的坐标位置,以便后续根据鼠标移动来控制相机等元素的位置或角度。
- windowHalfX 和 windowHalfY 分别是窗口宽度和高度的一半,常用于计算鼠标相对于窗口中心的偏移等操作。
- const materials = []; 创建了一个空数组 materials,这个数组将会用来存储不同的粒子材质,后续会为每一种类型的粒子创建对应的材质并添加到这个数组中。
四、初始化函数 init() 解析
function init() {
// 相机初始化
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 2000);
camera.position.z = 1000;
// 场景初始化
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.0008);
// 几何体和顶点数据相关
const geometry = new THREE.BufferGeometry();
const vertices = [];
const textureLoader = new THREE.TextureLoader();
const assignSRGB = (texture) => {
texture.colorSpace = THREE.SRGBColorSpace;
};
const sprite1 = textureLoader.load('textures/sprites/snowflake1.png', assignSRGB);
const sprite2 = textureLoader.load('textures/sprites/snowflake2.png', assignSRGB);
const sprite3 = textureLoader.load('textures/sprites/snowflake3.png', assignSRGB);
const sprite4 = textureLoader.load('textures/sprites/snowflake4.png', assignSRGB);
const sprite5 = textureLoader.load('textures/sprites/snowflake5.png', assignSRGB);
for (let i = 0; i < 10000; i++) {
const x = Math.random() * 2000 - 1000;
const y = Math.random() * 2000 - 1000;
const z = Math.random() * 2000 - 1000;
vertices.push(x, y, z);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
// 粒子材质和参数配置
parameters = [
[[1.0, 0.2, 0.5], sprite2, 20],
[[0.95, 0.1, 0.5], sprite3, 15],
[[0.90, 0.05, 0.5], sprite1, 10],
[[0.85, 0, 0.5], sprite5, 8],
[[0.80, 0, 0.5], sprite4, 5]
];
for (let i = 0; i < parameters.length; i++) {
const color = parameters[i][0];
const sprite = parameters[i][1];
const size = parameters[i][2];
materials[i] = new THREE.PointsMaterial({
size: size,
map: sprite,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent: true
});
materials[i].color.setHSL(color[0], color[1], color[2], THREE.SRGBColorSpace);
const particles = new THREE.Points(geometry, materials[i]);
particles.rotation.x = Math.random() * 6;
particles.rotation.y = Math.random() * 6;
particles.rotation.z = Math.random() * 6;
scene.add(particles);
}
// 渲染器初始化
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
document.body.appendChild(renderer.domElement);
// 性能统计模块添加
stats = new Stats();
document.body.appendChild(stats.dom);
// GUI界面初始化
const gui = new GUI();
const params = {
texture: true
};
gui.add(params, 'texture').onChange(function (value) {
for (let i = 0; i < materials.length; i++) {
materials[i].map = (value === true)? parameters[i][1] : null;
materials[i].needsUpdate = true;
}
});
gui.open();
document.body.style.touchAction = 'none';
document.body.addEventListener('pointermove', onPointerMove);
// 窗口大小改变事件监听
window.addEventListener('resize', onWindowResize);
}
4.1 相机初始化
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 2000);
camera.position.z = 1000;
这里创建了一个透视相机,第一个参数 75 是视角角度(以度为单位),它决定了相机视野的宽窄程度;第二个参数是相机的宽高比,通过获取窗口的当前宽高比来设置,确保渲染出来的画面不会变形;后两个参数分别是近裁剪面和远裁剪面的距离,物体在近裁剪面以内和远裁剪面以外将不会被渲染。然后将相机沿着 z 轴移动到距离原点 1000 的位置,来确定初始的观察位置。
4.2 场景初始化
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.0008);
创建了一个空的 Scene 对象作为整个3D场景的容器。接着设置了场景的雾效果,这里使用的是指数雾(FogExp2),颜色为黑色(0x000000),雾的浓度参数为 0.0008,雾效果可以增加场景的氛围感,远处的物体看起来会更模糊,仿佛被雾笼罩一样。
4.3 几何体和纹理相关操作
const geometry = new THREE.BufferGeometry();
const vertices = [];
const textureLoader = new THREE.TextureLoader();
const assignSRGB = (texture) => {
texture.colorSpace = THREE.SRGBColorSpace;
};
const sprite1 = textureLoader.load('textures/sprites/snowflake1.png', assignSRGB);
const sprite2 = textureLoader.load('textures/sprites/snowflake2.png', assignSRGB);
const sprite3 = textureLoader.load('textures/sprites/snowflake3.png', assignSRGB);
const sprite4 = textureLoader.load('textures/sprites/snowflake4.png', assignSRGB);
const sprite5 = textureLoader.load('textures/sprites/snowflake5.png', assignSRGB);
for (let i = 0; i < 10000; i++) {
const x = Math.random() * 2000 - 1000;
const y = Math.random() * 2000 - 1000;
const z = Math.random() * 2000 - 1000;
vertices.push(x, y, z);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
- 首先创建了一个 BufferGeometry 几何体对象,它是一种高效存储和处理顶点数据等几何信息的方式。
- 然后定义了一个空数组 vertices,通过循环生成了10000个随机的三维坐标点,并将它们添加到 vertices 数组中,这些坐标点将作为粒子在3D空间中的位置信息。
- 接着创建了一个 TextureLoader 用于加载纹理图片,这里定义了一个函数 assignSRGB,它的作用是将加载的纹理的颜色空间设置为 SRGBColorSpace,以确保颜色的正确显示。然后分别加载了5张雪花纹理图片(snowflake1.png 到 snowflake5.png),并在加载时应用了 assignSRGB 函数来设置颜色空间。
- 最后,通过 geometry.setAttribute 方法将顶点数据设置到几何体的 position 属性中,告诉Three.js这些点的位置信息,其中 Float32BufferAttribute 表示每个坐标分量(x、y、z)是32位的浮点数格式,并且每3个一组对应一个顶点的坐标。
4.4 粒子材质和添加到场景
parameters = [
[[1.0, 0.2, 0.5], sprite2, 20],
[[0.95, 0.1, 0.5], sprite3, 15],
[[0.90, 0.05, 0.5], sprite1, 10],
[[0.85, 0, 0.5], sprite5, 8],
[[0.80, 0, 0.5], sprite4, 5]
];
for (let i = 0; i < parameters.length; i++) {
const color = parameters[i][0];
const sprite = parameters[i][1];
const size = parameters[i][2];
materials[i] = new THREE.PointsMaterial({
size: size,
map: sprite,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent: true
});
materials[i].color.setHSL(color[0], color[1], color[2], THREE.SRGBColorSpace);
const particles = new THREE.Points(geometry, materials[i]);
particles.rotation.x = Math.random() * 6;
particles.rotation.y = Math.random() * 6;
particles.rotation.z = Math.random() * 6;
scene.add(particles);
}
- 定义了一个 parameters 数组,数组中的每个元素包含了粒子的颜色(以HSL格式表示的数组)、对应的纹理以及粒子的大小信息,用于配置不同类型的粒子。
- 通过循环遍历 parameters 数组,为每种粒子创建对应的 PointsMaterial 材质。在材质配置中,size 设置粒子的大小,map 指定了粒子使用的纹理,blending 设置为 THREE.AdditiveBlending 表示使用加法混合模式(常用于发光等效果),depthTest 设置为 false 可以避免一些深度测试相关的问题,transparent 设置为 true 表示材质是透明的。然后根据 parameters 中的颜色信息通过 color.setHSL 方法设置材质的颜色,并将颜色空间设置为 SRGBColorSpace。
- 接着使用创建好的几何体 geometry 和对应的材质 materials[i] 创建了 THREE.Points 对象,代表粒子系统,并且给每个粒子系统设置了随机的旋转角度,最后将这些粒子系统添加到场景 scene 中,这样它们就会在渲染时显示出来。
4.5 渲染器相关设置
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
document.body.appendChild(renderer.domElement);
- 创建了一个 WebGLRenderer 对象,它是基于WebGL来渲染3D场景的渲染器。
- 通过 renderer.setPixelRatio(window.devicePixelRatio) 语句,将渲染器的像素比设置为设备的像素比,这样在高分辨率屏幕上可以保证渲染效果更清晰,避免模糊等问题。
- renderer.setSize(window.innerWidth, window.innerHeight) 则是设置渲染器的渲染尺寸为当前窗口的大小,确保场景能完整地填充窗口。
- renderer.setAnimationLoop(animate) 为渲染器设置了动画循环,指定每次渲染更新时调用 animate 函数,这样场景就可以实现动态的效果,比如粒子的旋转、颜色变化等。
- 最后将渲染器的DOM元素添加到页面的 body 标签中,这样渲染的结果才能在页面上显示出来。
4.6 性能统计和GUI界面设置
stats = new Stats();
document.body.appendChild(stats.dom);
const gui = new GUI();
const params = {
texture: true
};
gui.add(params, 'texture').onChange(function (value) {
for (let i = 0; i < materials.length; i++) {
materials[i].map = (value === true)? parameters[i][1] : null;
materials[i].needsUpdate = true;
}
});
gui.open();
- 创建了 Stats 实例用于统计性能信息,并将其DOM元素添加到页面 body 中,这样可以在页面上看到帧率等性能数据。
- 创建了 GUI 实例,定义了一个包含 texture 属性的 params 对象,通过 gui.add 方法为 texture 属性添加了一个可交互的控件,当这个控件的值改变时(通过 onChange 回调函数监听),会遍历所有的粒子材质,根据 texture 的值来决定是否显示对应的纹理(设置 materials[i].map),并且标记材质需要更新(materials[i].needsUpdate = true),以便渲染器能正确渲染出纹理变化后的效果,最后调用 gui.open 打开GUI界面,使其在页面上显示出来。
4.7 其他事件监听相关设置
document.body.style.touchAction = 'none';
document.body.addEventListener('pointermove', onPointerMove);
window.addEventListener('resize', onWindowResize);
- document.body.style.touchAction = 'none'; 这行代码禁用了页面主体的默认触摸行为,可能是为了避免触摸操作对鼠标相关交互逻辑产生干扰,确保在移动设备上也能按照期望的方式响应用户操作。
- document.body.addEventListener('pointermove', onPointerMove); 为页面主体添加了一个 pointermove 事件监听器,当鼠标在页面上移动时,会触发 onPointerMove 函数,这个函数会更新 mouseX 和 mouseY 的值,用于后续根据鼠标位置来控制相机等元素。
- window.addEventListener('resize', onWindowResize); 监听窗口大小改变事件,当窗口大小改变时,会调用 onWindowResize 函数来相应地更新相机的宽高比、渲染器的尺寸等,确保场景能自适应窗口大小变化,正常显示。
五、窗口大小改变事件处理函数 onWindowResize()
function onWindowResize() {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
在窗口大小发生变化时,更新相机
六、鼠标移动事件处理函数 onPointerMove()
function onPointerMove(event) {
if (event.isPrimary === false) return;
mouseX = event.clientX - windowHalfX;
mouseY = event.clientY - windowHalfY;
}
这个函数用于处理鼠标在页面上移动时触发的 pointermove 事件。首先通过 event.isPrimary === false 进行判断,这里是为了只处理主指针(比如鼠标的左键或者在触摸设备上的主要触摸点)的移动操作,忽略其他非主指针的情况。然后通过 event.clientX - windowHalfX 和 event.clientY - windowHalfY 来计算鼠标相对于窗口中心的偏移量,并将其分别赋值给 mouseX 和 mouseY 变量。这些偏移量后续会被用于控制相机的位置等,例如在 animate 函数中,会根据 mouseX 和 mouseY 的值来让相机跟随鼠标移动的方向进行相应的平移,从而实现通过鼠标交互来改变观察视角的效果。
七、动画更新主函数 animate()
function animate() {
render();
stats.update();
}
animate 函数是整个动画循环的核心驱动函数,它会在每一帧被调用(由之前在 init 函数中通过 renderer.setAnimationLoop(animate) 进行设置)。在这个函数内部,首先调用了 render 函数,这个 render 函数负责实际的场景渲染以及各种物体属性的更新等操作,是实现场景动态变化效果的关键。然后调用 stats.update() 来更新性能统计信息,这样就能实时在页面上看到当前的帧率等性能指标,方便开发者判断场景的渲染性能是否满足要求,以及是否存在性能瓶颈等情况。
八、渲染更新函数 render()
function render() {
const time = Date.now() * 0.00005;
camera.position.x += (mouseX - camera.position.x) * 0.05;
camera.position.y += (-mouseY - camera.position.y) * 0.05;
camera.lookAt(scene.position);
for (let i = 0; i < scene.children.length; i++) {
const object = scene.children[i];
if (object instanceof THREE.Points) {
object.rotation.y = time * (i < 4? i + 1 : -(i + 1));
}
}
for (let i = 0; i < materials.length; i++) {
const color = parameters[i][0];
const h = (360 * (color[0] + time) % 360) / 360;
materials[i].color.setHSL(h, color[1], color[2], THREE.SRGBColorSpace);
}
renderer.render(scene, camera);
}
- 时间相关变量计算:
首先通过 const time = Date.now() * 0.00005; 计算出一个基于当前时间的时间变量 time,这个变量会用于后续一些随时间变化的动画效果的计算,比如粒子的旋转速度、颜色变化等,通过将当前时间戳进行缩放,能够让这些基于时间的变化在一个合适的节奏下进行,避免变化过快或者过慢。
- 相机位置更新:
camera.position.x += (mouseX - camera.position.x) * 0.05;
camera.position.y += (-mouseY - camera.position.y) * 0.05;
根据之前在 onPointerMove 函数中获取到的鼠标相对于窗口中心的偏移量 mouseX 和 mouseY 来更新相机的位置。这里采用了一种平滑过渡的计算方式,通过当前相机位置与鼠标偏移量的差值乘以一个系数(0.05)来逐渐改变相机的 x 和 y 坐标位置,使得相机跟随鼠标移动时更加平滑自然,而不是瞬间跳跃到新的位置。
3. 相机指向设置:
camera.lookAt(scene.position); 这行代码让相机的观察方向始终指向场景的中心位置(通过 scene.position 获取场景的坐标,通常场景的中心坐标默认为 (0, 0, 0)),确保无论相机如何移动,它都是朝着场景的核心部分进行观察的。
4. 粒子旋转更新:
for (let i = 0; i < scene.children.length; i++) {
const object = scene.children[i];
if (object instanceof THREE.Points) {
object.rotation.y = time * (i < 4? i + 1 : -(i + 1));
}
}
通过遍历场景中的所有子对象(scene.children),判断如果对象是 THREE.Points 类型(也就是之前添加的粒子系统),就根据当前的时间变量 time 以及粒子在场景中的索引 i 来更新粒子系统的 y 轴旋转角度。对于前4个粒子系统,旋转角度会随着时间逐渐增加(i + 1 倍的 time),而对于其他粒子系统则是随着时间逐渐减小(-(i + 1) 倍的 time),这样就实现了不同粒子系统以不同的速度和方向进行旋转,增加了场景的动态效果和丰富性。
5. 粒子颜色更新:
for (let i = 0; i < materials.length; i++) {
const color = parameters[i][0];
const h = (360 * (color[0] + time) % 360) / 360;
materials[i].color.setHSL(h, color[1], color[2], THREE.SRGBColorSpace);
}
再次遍历所有的粒子材质(通过 materials 数组),获取每个粒子材质对应的初始颜色配置(从 parameters 数组中获取)中的色相值(color[0]),然后结合时间变量 time 来更新色相值(通过 (360 * (color[0] + time) % 360) / 360 计算新的色相值 h),并通过 materials[i].color.setHSL 方法将新的色相值以及原来的饱和度、亮度等颜色参数重新设置到材质的颜色属性中,同时指定颜色空间为 SRGBColorSpace,从而实现粒子颜色随着时间不断变化的绚丽效果。
6. 场景渲染执行:
renderer.render(scene, camera); 这是最后也是最关键的一步,通过渲染器 renderer 将当前的场景 scene 使用指定的相机 camera 进行渲染,并将渲染结果显示在页面上,完成一帧的渲染更新操作,整个动画循环不断重复这个过程,就使得我们能看到连续的、动态的粒子效果场景。
九、总结
通过对这段代码的详细解析,我们可以看到它完整地构建了一个包含粒子效果、可交互界面、性能统计以及自适应窗口大小等功能的3D场景。从初始化各个核心组件(相机、场景、渲染器等),到设置粒子的材质、位置、动画效果,再到处理各种用户交互(鼠标移动、窗口大小改变)以及性能监控,涵盖了使用Three.js进行3D开发的多个关键方面。希望读者能够通过这次解析,深入理解这些代码背后的原理和实现思路,从而能够在自己的项目中灵活运用Three.js来创建出更精彩的3D可视化效果。