从零开始学习three.js(14):一文详解three.js中的粒子系统Points

Three.js 粒子特效:从基础到高级实现

引言

在 Three.js 这个强大的 3D 图形库中,粒子特效是实现各种绚丽视觉效果的重要手段之一。无论是模拟烟花、雨雪、流星等自然现象,还是创造梦幻般的背景装饰,粒子特效都能发挥巨大作用。本文将深入剖析 Three.js 中粒子特效的相关知识和实现技巧,帮助你更好地掌握这一技能。

一、粒子特效的核心概念

Three.js中的粒子特效通过​​粒子系统​ ​(THREE.Points)实现,其核心组件包括:

  1. ​几何体(BufferGeometry)​

    用于存储粒子的顶点数据(位置、颜色等)。通过Float32BufferAttribute高效管理大量粒子的动态信息,例如在飞毯效果中,50万粒子的位置和颜色数据通过数组初始化并绑定到几何体属性。

  2. ​材质(PointsMaterial)​

    控制粒子的视觉表现,支持以下关键参数:

    • size:粒子大小(默认单位像素),结合sizeAttenuation属性可控制粒子随距离变化的透视效果。

    • alphaMap:透明贴图,用于实现粒子边缘渐变或发光效果(如使用PNG纹理)。

    • vertexColors:启用后允许每个粒子独立设置颜色,适用于星空或彩虹效果。

  3. ​动画插值​

    通过requestAnimationFrame逐帧更新粒子位置,配合数学函数(如正弦波)或物理模拟实现动态效果。例如爱心特效中,粒子通过线性插值(lerp)在随机分散与目标形状(心形/数字)间平滑过渡。

二、基础粒子系统实现

1.1 Points 核心类

Three.js 在 r125+ 版本后使用 Points 类替代旧的 ParticleSystem

复制代码
const particles = new THREE.BufferGeometry();
const count = 10000;
const positions = new Float32Array(count * 3);

// 初始化随机位置
for (let i = 0; i < count * 3; i += 3) {
  positions[i] = (Math.random() - 0.5) * 10;
  positions[i+1] = (Math.random() - 0.5) * 10;
  positions[i+2] = (Math.random() - 0.5) * 10;
}

particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));

1.2 材质配置要点

PointsMaterial 的关键参数:

复制代码
const material = new THREE.PointsMaterial({
  color: 0x00ff00,//颜色
  size: 0.1,//大小
  transparent: true,//是否透明
  opacity: 0.8,//如果transparent为true,设置透明度
  sizeAttenuation: true // 透视衰减
});

1.3 完整创建流程

复制代码
const particleSystem = new THREE.Points(particles, material);
scene.add(particleSystem);

三、动态粒子效果

2.1 CPU 动画实现

复制代码
function animate() {
  const positions = particleSystem.geometry.attributes.position.array;
  
  for (let i = 0; i < positions.length; i += 3) {
    positions[i] += Math.random() * 0.01 - 0.005;
    positions[i+1] += Math.random() * 0.01 - 0.005;
    positions[i+2] += Math.random() * 0.01 - 0.005;
  }

  particleSystem.geometry.attributes.position.needsUpdate = true;
  requestAnimationFrame(animate);
}

2.2 性能优化技巧

  • 使用 Float32Array 代替常规数组

  • 批量更新替代逐粒子更新

  • 启用几何体复用:

    const baseGeometry = new THREE.BufferGeometry();
    // 初始化基础几何体...

    function createParticleSystem(params) {
    return new THREE.Points(baseGeometry.clone(), new PointsMaterial(params));
    }

四、高级着色器特效

3.1 自定义着色器结构

复制代码
// vertexShader
varying vec3 vPosition;

void main() {
  vPosition = position;
  gl_PointSize = 8.0;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

// fragmentShader
varying vec3 vPosition;

void main() {
  float distance = length(gl_PointCoord - vec2(0.5));
  if (distance > 0.5) discard;
  
  vec3 color = mix(vec3(1.0,0,0), vec3(0,0,1.0), vPosition.z * 0.5 + 0.5);
  gl_FragColor = vec4(color, 1.0 - distance * 2.0);
}

3.2 着色器动画示例

复制代码
// 添加时间uniform
uniform float uTime;

void main() {
  vec3 animatedPos = position + vec3(
    sin(position.x * 10.0 + uTime) * 0.2,
    cos(position.y * 8.0 + uTime) * 0.2,
    0
  );
  
  // 后续变换...
}

四、性能深度优化

优化策略 CPU 占用降低 GPU 占用降低
实例化渲染 40% 35%
LOD 分级 25% 30%
空间分区 30% 20%
着色器优化 15% 40%

关键优化技术:

  1. 使用 THREE.InstancedBufferGeometry 实现实例化

  2. 基于距离的细节层级控制:

    function updateLOD() {
    const distance = camera.position.distanceTo(particleSystem.position);
    particleSystem.material.size = THREE.MathUtils.lerp(
    0.1, 2.0, 1.0 - Math.min(distance / 100, 1)
    );
    }

五、实战应用案例

5.1 星空背景

复制代码
const starCount = 5000;
const starPositions = new Float32Array(starCount * 3);

for (let i = 0; i < starCount; i++) {
  const i3 = i * 3;
  starPositions[i3] = (Math.random() - 0.5) * 200;
  starPositions[i3 + 1] = (Math.random() - 0.5) * 200;
  starPositions[i3 + 2] = (Math.random() - 0.5) * 200;
}

const starGeometry = new THREE.BufferGeometry();
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));

const starMaterial = new THREE.PointsMaterial({
  size: 0.05,
  color: 0xffffff
});

const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);

5.2 星空背景增强版

复制代码
function createStarField() {
  const vertices = new Float32Array(20000 * 3);
  // 生成球状分布
  const radius = 100;
  for (let i = 0; i < 20000; i++) {
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.acos((Math.random() * 2) - 1);
    const r = radius * Math.cbrt(Math.random());
    
    vertices[i*3] = r * Math.sin(phi) * Math.cos(theta);
    vertices[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
    vertices[i*3+2] = r * Math.cos(phi);
  }
  
  // 创建渐变材质...
}

5.3 交互式粒子系统

复制代码
document.addEventListener('mousemove', (e) => {
  const mouse = new THREE.Vector2(
    (e.clientX / window.innerWidth) * 2 - 1,
    -(e.clientY / window.innerHeight) * 2 + 1
  );
  
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObject(particleSystem);
  
  if (intersects.length > 0) {
    const index = intersects[0].index * 3;
    // 修改选中粒子的属性...
  }
});

5.4 烟花效果

复制代码
 // 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

// 存储烟花粒子系统的数组
const fireworks = [];

// 创建烟花
function createFirework() {
    const color = new THREE.Color(Math.random(), Math.random(), Math.random());
    const position = new THREE.Vector3(
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10,
        0
    );
    const particleCount = 100;
    const geometry = new THREE.BufferGeometry();
    const positions = new Float32Array(particleCount * 3);
    const colors = new Float32Array(particleCount * 3);

    for (let i = 0; i < particleCount; i++) {
        const i3 = i * 3;
        positions[i3] = position.x;
        positions[i3 + 1] = position.y;
        positions[i3 + 2] = position.z;

        colors[i3] = color.r;
        colors[i3 + 1] = color.g;
        colors[i3 + 2] = color.b;
    }

    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

    const material = new THREE.PointsMaterial({
        size: 0.1,
        vertexColors: true
    });

    const particles = new THREE.Points(geometry, material);
    scene.add(particles);

    fireworks.push({
        particles,
        velocities: new Array(particleCount).fill().map(() => {
            const speed = Math.random() * 0.1;
            const angle = Math.random() * Math.PI * 2;
            return new THREE.Vector3(
                Math.cos(angle) * speed,
                Math.sin(angle) * speed,
                0
            );
        }),
        life: 100
    });
}

// 更新烟花状态
function updateFireworks() {
    for (let i = fireworks.length - 1; i >= 0; i--) {
        const firework = fireworks[i];
        const positions = firework.particles.geometry.attributes.position.array;
        for (let j = 0; j < firework.velocities.length; j++) {
            const j3 = j * 3;
            positions[j3] += firework.velocities[j].x;
            positions[j3 + 1] += firework.velocities[j].y;
            positions[j3 + 2] += firework.velocities[j].z;
        }
        firework.particles.geometry.attributes.position.needsUpdate = true;
        firework.life--;
        if (firework.life <= 0) {
            scene.remove(firework.particles);
            fireworks.splice(i, 1);
        }
    }
}

// 动画循环
function animate() {
    requestAnimationFrame(animate);
    if (Math.random() < 0.01) {
        createFirework();
    }
    updateFireworks();
    renderer.render(scene, camera);
}

animate();

5.5 雨雪效果

复制代码
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

// 粒子数量,可根据雨雪强度调整
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const velocities = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);

for (let i = 0; i < particleCount; i++) {
    const i3 = i * 3;
    // 随机初始化粒子位置
    positions[i3] = (Math.random() - 0.5) * 20;
    positions[i3 + 1] = Math.random() * 10;
    positions[i3 + 2] = (Math.random() - 0.5) * 20;

    // 初始化粒子速度
    velocities[i3] = (Math.random() - 0.5) * 0.01; // 水平随机摆动
    velocities[i3 + 1] = -Math.random() * 0.05; // 垂直下落速度
    velocities[i3 + 2] = (Math.random() - 0.5) * 0.01;

    // 根据雨雪强度调整粒子大小
    sizes[i] = Math.random() * 0.1 + 0.05;
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('customVelocity', new THREE.BufferAttribute(velocities, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

const material = new THREE.PointsMaterial({
    color: 0xffffff,
    sizeAttenuation: true,
    transparent: true,
    opacity: 0.8 // 透明度,可根据雨雪强度调整
});

const particles = new THREE.Points(geometry, material);
scene.add(particles);

// 动画循环
function animate() {
    requestAnimationFrame(animate);

    const positions = particles.geometry.attributes.position.array;
    const velocities = particles.geometry.attributes.customVelocity.array;

    for (let i = 0; i < particleCount; i++) {
        const i3 = i * 3;
        positions[i3] += velocities[i3];
        positions[i3 + 1] += velocities[i3 + 1];
        positions[i3 + 2] += velocities[i3 + 2];

        // 如果粒子超出底部,重新初始化位置到顶部
        if (positions[i3 + 1] < -5) {
            positions[i3] = (Math.random() - 0.5) * 20;
            positions[i3 + 1] = Math.random() * 10;
            positions[i3 + 2] = (Math.random() - 0.5) * 20;
        }
    }

    particles.geometry.attributes.position.needsUpdate = true;
    renderer.render(scene, camera);
}

animate();

六、调试与问题排查

  1. 内存泄漏检测

    // 监控GPU内存
    const memInfo = renderer.getMemoryInfo();
    console.log(GPU Memory: ${memInfo.geometries} geometries, ${memInfo.textures} textures);

  2. 常见问题处理

  • 闪烁问题 :启用深度测试 material.depthTest = true
  • 尺寸异常 :检查 sizeAttenuation 和透视相机设置
  • 性能骤降 :使用 stats.js 监控FPS和绘制调用次数

总之,Three.js 中的粒子特效功能强大且灵活多样。掌握粒子系统的创建、属性设置和动画技巧,以及性能优化方法,能够让你在 3D 开发中创造出令人惊叹的视觉效果,为你的项目增添独特的魅力。无论是游戏开发、动画制作还是可视化展示,粒子特效都是一个不可或缺的工具,值得深入学习和探索。

相关推荐
liuyang___16 分钟前
vue3+ts的computed属性怎么用?
前端·javascript·vue.js
cwl7218 分钟前
Unity WebGL、js发布交互
javascript·unity·webgl
xixixiLucky20 分钟前
Selenium Web自动化测试学习笔记(一)
笔记·学习·selenium
软件技术NINI34 分钟前
html css js网页制作成品——HTML+CSS珠海网页设计网页设计(4页)附源码
javascript·css·html
爱编程的鱼37 分钟前
如何用CSS实现HTML元素的旋转效果:从基础到高阶应用
前端·css·html
大G哥1 小时前
用 Go 和 TensorFlow 实现图像验证码识别系统
开发语言·后端·golang·tensorflow·neo4j
虾球xz1 小时前
游戏引擎学习第263天:添加调试帧滑块
c++·学习·游戏引擎
钢铁男儿1 小时前
深入解析C#参数传递:值参数 vs 引用参数
java·开发语言·c#
努力努力再努力wz1 小时前
【c++深入系列】:万字详解vector(附模拟实现的vector源码)
运维·开发语言·c++·c
.YM.Z1 小时前
C语言——操作符
c语言·开发语言·算法