【03】基于Three.js的炫酷粒子效果代码解析(webgl_points_sprites.html)

一、引言

在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 语句导入了必要的模块。

  1. import * as THREE from 'three';:这行代码导入了整个 THREE 库,THREE 是Three.js库的核心命名空间,后续所有关于创建3D场景、物体、相机等相关的类和函数都将从这个命名空间中获取。例如,我们后面会使用 THREE.PerspectiveCamera 来创建透视相机,这里的 PerspectiveCamera 就是通过导入的 THREE 命名空间来访问的。
  1. import Stats from 'three/addons/libs/stats.module.js';:引入了 Stats 模块,它通常用于在页面上显示性能相关的统计信息,比如帧率等,方便开发者监控场景渲染的性能表现。
  1. 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 = [];
  1. 首先声明了多个变量,camera、scene、renderer 是Three.js中构建3D场景的核心元素,camera 代表相机,用于定义观察场景的视角;scene 是整个3D场景的容器,所有的3D物体都会添加到这个场景中;renderer 则负责将场景通过WebGL渲染到页面上。stats 用于性能统计,parameters 用于存储一些和粒子材质相关的配置参数。
  1. mouseXmouseY 用于记录鼠标在页面上的坐标位置,以便后续根据鼠标移动来控制相机等元素的位置或角度。
  1. windowHalfX windowHalfY 分别是窗口宽度和高度的一半,常用于计算鼠标相对于窗口中心的偏移等操作。
  1. 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);

}
  1. 时间相关变量计算

首先通过 const time = Date.now() * 0.00005; 计算出一个基于当前时间的时间变量 time,这个变量会用于后续一些随时间变化的动画效果的计算,比如粒子的旋转速度、颜色变化等,通过将当前时间戳进行缩放,能够让这些基于时间的变化在一个合适的节奏下进行,避免变化过快或者过慢。

  1. 相机位置更新
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可视化效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jiaberrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值