{T}

粒子系统

粒子系统用于创建大量小型对象的模拟效果,如雨、雪、火焰、烟雾、星空、爆炸等。Three.js 提供了 THREE.Points 类用于高效渲染大量粒子。

系统架构

code
┌─────────────────────────────────────────────────────────────────────────┐
│                        粒子系统架构                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐  │
│  │   粒子数据管理    │    │    几何体配置     │    │    材质配置      │  │
│  │                  │    │                  │    │                  │  │
│  │  • Positions     │    │  BufferGeometry  │    │  PointsMaterial  │  │
│  │  • Colors        │───▶│  - position      │───▶│  - color         │  │
│  │  • Sizes         │    │  - color         │    │  - size          │  │
│  │  • Velocities    │    │  - size          │    │  - map           │  │
│  │  • Lifetimes     │    │                  │    │  - transparent   │  │
│  └──────────────────┘    └──────────────────┘    └──────────────────┘  │
│                                                           │             │
│                                                           ▼             │
│                                               ┌──────────────────┐      │
│                                               │   THREE.Points   │      │
│                                               │                  │      │
│                                               │  高效批量渲染    │      │
│                                               └──────────────────┘      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

渲染流程:
粒子数据 → BufferGeometry → PointsMaterial → THREE.Points → 渲染器

核心组件说明

组件说明职责
THREE.Points粒子容器管理粒子对象的渲染
BufferGeometry几何体存储粒子位置、颜色、大小等属性
PointsMaterial粒子材质控制粒子外观(颜色、大小、纹理)
ShaderMaterial自定义材质实现复杂的粒子效果
BufferAttribute缓冲属性存储具体的粒子数据

概述

Three.js 的 THREE.Points 类是专门用于渲染大量粒子的优化方案。与单独创建网格相比,粒子系统有以下优势:

特性传统网格粒子系统
渲染调用每个对象一次全部一次
内存占用
适用数量少量对象数千至百万
灵活性中等
性能

适用场景

  • 自然现象:雨、雪、落叶、火焰、烟雾
  • 视觉效果:爆炸、魔法、星空、银河
  • 数据可视化:散点图、点云、流体模拟
  • 游戏特效:技能效果、粒子武器、环境粒子

Points 基础

创建基础粒子系统

javascript
import * as THREE from 'three';

// ==================== 创建粒子几何体 ====================
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();

// 位置数组(每个粒子 3 个值:x, y, z)
const positions = new Float32Array(particleCount * 3);

for (let i = 0; i < particleCount; i++) {
  positions[i * 3] = (Math.random() - 0.5) * 20;      // x
  positions[i * 3 + 1] = (Math.random() - 0.5) * 20;  // y
  positions[i * 3 + 2] = (Math.random() - 0.5) * 20;  // z
}

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

// ==================== 创建粒子材质 ====================
const material = new THREE.PointsMaterial({
  color: 0xffffff,
  size: 0.1,
  sizeAttenuation: true   // 是否随距离衰减大小
});

// ==================== 创建粒子系统 ====================
const particles = new THREE.Points(geometry, material);
scene.add(particles);

// ==================== 基本动画 ====================
function animate() {
  requestAnimationFrame(animate);
  
  // 旋转粒子系统
  particles.rotation.y += 0.001;
  
  renderer.render(scene, camera);
}

粒子颜色

javascript
// 为每个粒子设置独立颜色
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();

// 位置
const positions = new Float32Array(particleCount * 3);
// 颜色(RGB)
const colors = new Float32Array(particleCount * 3);

for (let i = 0; i < particleCount; i++) {
  // 随机位置
  positions[i * 3] = (Math.random() - 0.5) * 20;
  positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
  positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
  
  // 随机颜色(HSL 更容易控制)
  const color = new THREE.Color();
  color.setHSL(Math.random(), 1.0, 0.5);  // 随机色相,饱和度100%,亮度50%
  
  colors[i * 3] = color.r;      // R
  colors[i * 3 + 1] = color.g;  // G
  colors[i * 3 + 2] = color.b;  // B
}

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

// 材质启用顶点颜色
const material = new THREE.PointsMaterial({
  size: 0.2,
  vertexColors: true,  // 使用顶点颜色
  transparent: true,
  opacity: 0.8
});

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

渐变颜色

javascript
// 基于位置或属性的颜色渐变
function setGradientColors(positions, colors, particleCount) {
  for (let i = 0; i < particleCount; i++) {
    const y = positions[i * 3 + 1];
    
    // 基于高度的渐变
    const t = (y + 10) / 20;  // 归一化到 0-1
    
    const color = new THREE.Color();
    // 从蓝色渐变到红色
    color.setHSL(0.6 - t * 0.6, 1.0, 0.5);
    
    colors[i * 3] = color.r;
    colors[i * 3 + 1] = color.g;
    colors[i * 3 + 2] = color.b;
  }
}

粒子大小

使用 PointsMaterial 时所有粒子大小相同,使用 ShaderMaterial 可实现不同大小:

javascript
// 方法一:使用 ShaderMaterial
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const sizes = new Float32Array(particleCount);

for (let i = 0; i < particleCount; i++) {
  positions[i * 3] = (Math.random() - 0.5) * 20;
  positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
  positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
  sizes[i] = Math.random() * 0.5 + 0.1;  // 随机大小
}

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

// 自定义着色器材质
const material = new THREE.ShaderMaterial({
  uniforms: {
    color: { value: new THREE.Color(0xffffff) },
    pointTexture: { value: textureLoader.load('particle.png') }
  },
  vertexShader: `
    attribute float size;
    varying vec3 vColor;
    
    void main() {
      vColor = vec3(1.0);
      vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
      
      // 根据距离调整大小
      gl_PointSize = size * (300.0 / -mvPosition.z);
      gl_Position = projectionMatrix * mvPosition;
    }
  `,
  fragmentShader: `
    uniform vec3 color;
    uniform sampler2D pointTexture;
    varying vec3 vColor;
    
    void main() {
      vec4 texColor = texture2D(pointTexture, gl_PointCoord);
      gl_FragColor = vec4(color * vColor, 1.0) * texColor;
    }
  `,
  transparent: true,
  depthWrite: false
});

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

粒子纹理

使用纹理贴图

javascript
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('particle.png');

const material = new THREE.PointsMaterial({
  size: 0.5,
  map: texture,
  transparent: true,
  alphaTest: 0.5,      // Alpha 测试阈值
  depthWrite: false,   // 避免深度冲突
  blending: THREE.AdditiveBlending  // 叠加混合
});

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

纹理加载选项

javascript
const texture = textureLoader.load('particle.png', 
  // 加载完成回调
  (texture) => {
    texture.minFilter = THREE.LinearFilter;
    texture.magFilter = THREE.LinearFilter;
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping;
    
    material.map = texture;
    material.needsUpdate = true;
  },
  // 进度回调
  undefined,
  // 错误回调
  (error) => {
    console.error('纹理加载失败:', error);
  }
);

使用 Canvas 创建纹理

javascript
// 创建粒子纹理(圆形渐变)
function createParticleTexture() {
  const canvas = document.createElement('canvas');
  canvas.width = 64;
  canvas.height = 64;
  
  const ctx = canvas.getContext('2d');
  
  // 创建径向渐变
  const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
  gradient.addColorStop(0.3, 'rgba(255, 255, 255, 0.8)');
  gradient.addColorStop(0.7, 'rgba(255, 255, 255, 0.3)');
  gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
  
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 64, 64);
  
  return new THREE.CanvasTexture(canvas);
}

// 创建星形纹理
function createStarTexture() {
  const canvas = document.createElement('canvas');
  canvas.width = 64;
  canvas.height = 64;
  
  const ctx = canvas.getContext('2d');
  const cx = 32, cy = 32;
  const spikes = 4;
  const outerRadius = 30;
  const innerRadius = 10;
  
  ctx.beginPath();
  ctx.moveTo(cx, cy - outerRadius);
  
  for (let i = 0; i < spikes * 2; i++) {
    const radius = i % 2 === 0 ? outerRadius : innerRadius;
    const angle = (i * Math.PI) / spikes - Math.PI / 2;
    ctx.lineTo(
      cx + Math.cos(angle) * radius,
      cy + Math.sin(angle) * radius
    );
  }
  
  ctx.closePath();
  ctx.fillStyle = 'white';
  ctx.fill();
  
  return new THREE.CanvasTexture(canvas);
}

// 创建火花纹理
function createSparkTexture() {
  const canvas = document.createElement('canvas');
  canvas.width = 64;
  canvas.height = 64;
  
  const ctx = canvas.getContext('2d');
  
  // 绘制椭圆形火花
  const gradient = ctx.createRadialGradient(32, 32, 0, 32, 16, 32);
  gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
  gradient.addColorStop(0.5, 'rgba(255, 200, 100, 0.5)');
  gradient.addColorStop(1, 'rgba(255, 100, 50, 0)');
  
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 64, 64);
  
  return new THREE.CanvasTexture(canvas);
}

精灵纹理

javascript
// 使用 Sprite 格式的纹理(自动旋转)
const material = new THREE.PointsMaterial({
  size: 0.5,
  map: texture,
  transparent: true,
  sizeAttenuation: true,
  // 精灵不会随相机旋转(适用于 2D 风格)
});

粒子动画

基础动画

javascript
// 简单的粒子下落动画
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const velocities = [];

for (let i = 0; i < particleCount; i++) {
  positions[i * 3] = (Math.random() - 0.5) * 20;
  positions[i * 3 + 1] = Math.random() * 20;
  positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
  
  // 存储速度
  velocities.push({
    x: (Math.random() - 0.5) * 0.02,
    y: -Math.random() * 0.02 - 0.01,  // 向下
    z: (Math.random() - 0.5) * 0.02
  });
}

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

const material = new THREE.PointsMaterial({
  color: 0xffffff,
  size: 0.1,
  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;
  
  for (let i = 0; i < particleCount; i++) {
    // 更新位置
    positions[i * 3] += velocities[i].x;
    positions[i * 3 + 1] += velocities[i].y;
    positions[i * 3 + 2] += velocities[i].z;
    
    // 重置超出边界的粒子
    if (positions[i * 3 + 1] < -10) {
      positions[i * 3 + 1] = 10;
      positions[i * 3] = (Math.random() - 0.5) * 20;
      positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
    }
  }
  
  // 标记需要更新
  particles.geometry.attributes.position.needsUpdate = true;
  
  renderer.render(scene, camera);
}

雪花效果

javascript
function createSnowfall() {
  const particleCount = 5000;
  const geometry = new THREE.BufferGeometry();
  
  const positions = new Float32Array(particleCount * 3);
  const velocities = [];
  const sizes = new Float32Array(particleCount);
  
  for (let i = 0; i < particleCount; i++) {
    // 初始位置
    positions[i * 3] = (Math.random() - 0.5) * 100;
    positions[i * 3 + 1] = Math.random() * 50;
    positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
    
    // 随机速度和飘动
    velocities.push({
      x: (Math.random() - 0.5) * 0.05,
      y: -0.05 - Math.random() * 0.05,
      z: (Math.random() - 0.5) * 0.05,
      // 飘动参数
      amplitudeX: Math.random() * 0.05,
      amplitudeZ: Math.random() * 0.05,
      frequency: Math.random() * 2 + 1,
      phase: Math.random() * Math.PI * 2
    });
    
    sizes[i] = Math.random() * 0.3 + 0.1;
  }
  
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  
  // 雪花纹理
  const texture = createParticleTexture();
  
  const material = new THREE.PointsMaterial({
    color: 0xffffff,
    size: 0.2,
    map: texture,
    transparent: true,
    opacity: 0.8,
    depthWrite: false
  });
  
  const snow = new THREE.Points(geometry, material);
  scene.add(snow);
  
  return { particles: snow, velocities };
}

// 更新雪花
let time = 0;
function updateSnow(particles, velocities) {
  time += 0.016;
  const positions = particles.geometry.attributes.position.array;
  
  for (let i = 0; i < positions.length / 3; i++) {
    const vel = velocities[i];
    
    // 基础移动
    positions[i * 3] += vel.x;
    positions[i * 3 + 1] += vel.y;
    positions[i * 3 + 2] += vel.z;
    
    // 飘动效果
    positions[i * 3] += Math.sin(time * vel.frequency + vel.phase) * vel.amplitudeX;
    positions[i * 3 + 2] += Math.cos(time * vel.frequency + vel.phase) * vel.amplitudeZ;
    
    // 重置
    if (positions[i * 3 + 1] < 0) {
      positions[i * 3 + 1] = 50;
      positions[i * 3] = (Math.random() - 0.5) * 100;
      positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
    }
  }
  
  particles.geometry.attributes.position.needsUpdate = true;
}

火焰效果

javascript
function createFire(position = new THREE.Vector3(0, 0, 0)) {
  const particleCount = 2000;
  const geometry = new THREE.BufferGeometry();
  
  const positions = new Float32Array(particleCount * 3);
  const colors = new Float32Array(particleCount * 3);
  const sizes = new Float32Array(particleCount);
  const lifetimes = [];
  
  // 初始化粒子
  for (let i = 0; i < particleCount; i++) {
    resetFireParticle(i, positions, colors, sizes, lifetimes, position);
  }
  
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  
  const texture = createParticleTexture();
  
  const material = new THREE.PointsMaterial({
    size: 0.5,
    vertexColors: true,
    map: texture,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthWrite: false
  });
  
  const fire = new THREE.Points(geometry, material);
  fire.userData = { lifetimes, position };
  
  return fire;
}

function resetFireParticle(i, positions, colors, sizes, lifetimes, position) {
  // 位置:从底部中心发出
  const angle = Math.random() * Math.PI * 2;
  const radius = Math.random() * 0.5;
  
  positions[i * 3] = position.x + Math.cos(angle) * radius;
  positions[i * 3 + 1] = position.y;
  positions[i * 3 + 2] = position.z + Math.sin(angle) * radius;
  
  // 颜色:从黄色到红色到暗红色
  const t = Math.random();
  colors[i * 3] = 1.0;                    // R
  colors[i * 3 + 1] = t * 0.8;           // G
  colors[i * 3 + 2] = 0;                  // B
  
  // 大小
  sizes[i] = Math.random() * 0.5 + 0.5;
  
  // 生命周期
  lifetimes[i] = {
    life: Math.random() * 2,
    maxLife: 2,
    speed: 1 + Math.random() * 2,
    turbulence: Math.random() * 0.5
  };
}

function updateFire(fire, deltaTime) {
  const positions = fire.geometry.attributes.position.array;
  const colors = fire.geometry.attributes.color.array;
  const sizes = fire.geometry.attributes.size.array;
  const lifetimes = fire.userData.lifetimes;
  const position = fire.userData.position;
  
  for (let i = 0; i < positions.length / 3; i++) {
    const lifetime = lifetimes[i];
    
    lifetime.life -= deltaTime;
    
    if (lifetime.life <= 0) {
      resetFireParticle(i, positions, colors, sizes, lifetimes, position);
    } else {
      // 上升
      positions[i * 3 + 1] += lifetime.speed * deltaTime;
      
      // 湍流
      positions[i * 3] += (Math.random() - 0.5) * lifetime.turbulence * deltaTime;
      positions[i * 3 + 2] += (Math.random() - 0.5) * lifetime.turbulence * deltaTime;
      
      // 颜色变化:越往上越暗
      const alpha = lifetime.life / lifetime.maxLife;
      colors[i * 3] *= 0.99;
      colors[i * 3 + 1] *= 0.98;
      
      // 大小衰减
      sizes[i] *= 0.995;
    }
  }
  
  fire.geometry.attributes.position.needsUpdate = true;
  fire.geometry.attributes.color.needsUpdate = true;
  fire.geometry.attributes.size.needsUpdate = true;
}

爆炸效果

javascript
function createExplosion(position = new THREE.Vector3(0, 0, 0)) {
  const particleCount = 500;
  const geometry = new THREE.BufferGeometry();
  
  const positions = new Float32Array(particleCount * 3);
  const velocities = new Float32Array(particleCount * 3);
  const colors = new Float32Array(particleCount * 3);
  const lifetimes = new Float32Array(particleCount);
  
  for (let i = 0; i < particleCount; i++) {
    // 从中心点开始
    positions[i * 3] = position.x;
    positions[i * 3 + 1] = position.y;
    positions[i * 3 + 2] = position.z;
    
    // 随机方向的速度
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.acos(Math.random() * 2 - 1);
    const speed = 5 + Math.random() * 10;
    
    velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
    velocities[i * 3 + 1] = Math.sin(phi) * Math.sin(theta) * speed;
    velocities[i * 3 + 2] = Math.cos(phi) * speed;
    
    // 火焰颜色
    colors[i * 3] = 1;
    colors[i * 3 + 1] = Math.random() * 0.5 + 0.5;
    colors[i * 3 + 2] = 0;
    
    lifetimes[i] = 1 + Math.random();
  }
  
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  
  const texture = createParticleTexture();
  
  const material = new THREE.PointsMaterial({
    size: 0.5,
    vertexColors: true,
    map: texture,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthWrite: false
  });
  
  const explosion = new THREE.Points(geometry, material);
  explosion.userData = { velocities, lifetimes, age: 0 };
  
  return explosion;
}

function updateExplosion(explosion, deltaTime) {
  const positions = explosion.geometry.attributes.position.array;
  const colors = explosion.geometry.attributes.color.array;
  const velocities = explosion.userData.velocities;
  const lifetimes = explosion.userData.lifetimes;
  
  explosion.userData.age += deltaTime;
  
  for (let i = 0; i < positions.length / 3; i++) {
    // 应用速度
    positions[i * 3] += velocities[i * 3] * deltaTime;
    positions[i * 3 + 1] += velocities[i * 3 + 1] * deltaTime;
    positions[i * 3 + 2] += velocities[i * 3 + 2] * deltaTime;
    
    // 重力
    velocities[i * 3 + 1] -= 9.8 * deltaTime;
    
    // 阻力
    velocities[i * 3] *= 0.99;
    velocities[i * 3 + 1] *= 0.99;
    velocities[i * 3 + 2] *= 0.99;
    
    // 颜色衰减
    colors[i * 3] *= 0.98;
    colors[i * 3 + 1] *= 0.95;
  }
  
  explosion.geometry.attributes.position.needsUpdate = true;
  explosion.geometry.attributes.color.needsUpdate = true;
  
  // 检查是否完成
  return explosion.userData.age > 3;
}

烟雾效果

javascript
function createSmoke(position = new THREE.Vector3(0, 0, 0)) {
  const particleCount = 300;
  const geometry = new THREE.BufferGeometry();
  
  const positions = new Float32Array(particleCount * 3);
  const colors = new Float32Array(particleCount * 3);
  const sizes = new Float32Array(particleCount);
  const lifetimes = [];
  
  for (let i = 0; i < particleCount; i++) {
    resetSmokeParticle(i, positions, colors, sizes, lifetimes, position);
  }
  
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  
  const texture = createParticleTexture();
  
  const material = new THREE.PointsMaterial({
    size: 2,
    vertexColors: true,
    map: texture,
    transparent: true,
    opacity: 0.5,
    depthWrite: false,
    blending: THREE.NormalBlending
  });
  
  const smoke = new THREE.Points(geometry, material);
  smoke.userData = { lifetimes, position };
  
  return smoke;
}

function resetSmokeParticle(i, positions, colors, sizes, lifetimes, position) {
  positions[i * 3] = position.x + (Math.random() - 0.5) * 0.5;
  positions[i * 3 + 1] = position.y;
  positions[i * 3 + 2] = position.z + (Math.random() - 0.5) * 0.5;
  
  // 灰色系
  const gray = 0.3 + Math.random() * 0.3;
  colors[i * 3] = gray;
  colors[i * 3 + 1] = gray;
  colors[i * 3 + 2] = gray;
  
  sizes[i] = 0.5;
  
  lifetimes[i] = {
    life: 3 + Math.random() * 2,
    maxLife: 5,
    speedY: 0.5 + Math.random() * 0.5,
    driftX: (Math.random() - 0.5) * 0.5,
    driftZ: (Math.random() - 0.5) * 0.5,
    growthRate: 0.02 + Math.random() * 0.02
  };
}

function updateSmoke(smoke, deltaTime) {
  const positions = smoke.geometry.attributes.position.array;
  const colors = smoke.geometry.attributes.color.array;
  const sizes = smoke.geometry.attributes.size.array;
  const lifetimes = smoke.userData.lifetimes;
  const position = smoke.userData.position;
  
  for (let i = 0; i < positions.length / 3; i++) {
    const lifetime = lifetimes[i];
    
    lifetime.life -= deltaTime;
    
    if (lifetime.life <= 0) {
      resetSmokeParticle(i, positions, colors, sizes, lifetimes, position);
    } else {
      // 上升并扩散
      positions[i * 3] += lifetime.driftX * deltaTime;
      positions[i * 3 + 1] += lifetime.speedY * deltaTime;
      positions[i * 3 + 2] += lifetime.driftZ * deltaTime;
      
      // 增长大小
      sizes[i] += lifetime.growthRate;
      
      // 淡出
      const alpha = lifetime.life / lifetime.maxLife;
      colors[i * 3] *= 0.995;
      colors[i * 3 + 1] *= 0.995;
      colors[i * 3 + 2] *= 0.995;
    }
  }
  
  smoke.geometry.attributes.position.needsUpdate = true;
  smoke.geometry.attributes.color.needsUpdate = true;
  smoke.geometry.attributes.size.needsUpdate = true;
}

高级粒子系统

粒子发射器类

javascript
class ParticleEmitter {
  constructor(options = {}) {
    // 基本参数
    this.particleCount = options.particleCount || 1000;
    this.position = options.position || new THREE.Vector3();
    this.direction = options.direction || new THREE.Vector3(0, 1, 0);
    
    // 发射参数
    this.spread = options.spread || 1;              // 发散角度
    this.speed = options.speed || 1;                // 速度
    this.speedRandomness = options.speedRandomness || 0.5;
    
    // 生命周期
    this.lifetime = options.lifetime || 2;
    this.lifetimeRandomness = options.lifetimeRandomness || 0.5;
    
    // 外观
    this.size = options.size || 0.1;
    this.color = options.color || new THREE.Color(1, 1, 1);
    this.texture = options.texture || null;
    
    // 物理
    this.gravity = options.gravity || 0;
    this.drag = options.drag || 0.98;
    
    this.init();
  }
  
  init() {
    const geometry = new THREE.BufferGeometry();
    
    const positions = new Float32Array(this.particleCount * 3);
    const colors = new Float32Array(this.particleCount * 3);
    const sizes = new Float32Array(this.particleCount);
    const velocities = new Float32Array(this.particleCount * 3);
    const lifetimes = new Float32Array(this.particleCount);
    
    // 初始化所有粒子
    for (let i = 0; i < this.particleCount; i++) {
      this.resetParticle(i, positions, velocities, lifetimes, sizes, colors);
    }
    
    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
    
    // 材质
    const materialOptions = {
      size: this.size,
      vertexColors: true,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending
    };
    
    if (this.texture) {
      materialOptions.map = this.texture;
    }
    
    const material = new THREE.PointsMaterial(materialOptions);
    
    this.particles = new THREE.Points(geometry, material);
    this.particles.userData = { velocities, lifetimes };
  }
  
  resetParticle(i, positions, velocities, lifetimes, sizes, colors) {
    // 位置
    positions[i * 3] = this.position.x;
    positions[i * 3 + 1] = this.position.y;
    positions[i * 3 + 2] = this.position.z;
    
    // 方向(带随机发散)
    const theta = Math.random() * Math.PI * 2;
    const phi = this.spread * Math.random();
    
    const dir = this.direction.clone().normalize();
    const speed = this.speed * (1 + (Math.random() - 0.5) * this.speedRandomness);
    
    velocities[i * 3] = (dir.x + Math.sin(phi) * Math.cos(theta)) * speed;
    velocities[i * 3 + 1] = (dir.y + Math.sin(phi) * Math.sin(theta)) * speed;
    velocities[i * 3 + 2] = (dir.z + Math.cos(phi)) * speed;
    
    // 生命周期
    lifetimes[i] = this.lifetime * (1 + (Math.random() - 0.5) * this.lifetimeRandomness);
    
    // 大小
    sizes[i] = this.size * (0.5 + Math.random() * 0.5);
    
    // 颜色
    colors[i * 3] = this.color.r;
    colors[i * 3 + 1] = this.color.g;
    colors[i * 3 + 2] = this.color.b;
  }
  
  update(delta) {
    const positions = this.particles.geometry.attributes.position.array;
    const velocities = this.particles.userData.velocities;
    const lifetimes = this.particles.userData.lifetimes;
    const sizes = this.particles.geometry.attributes.size.array;
    const colors = this.particles.geometry.attributes.color.array;
    
    for (let i = 0; i < this.particleCount; i++) {
      lifetimes[i] -= delta;
      
      if (lifetimes[i] <= 0) {
        this.resetParticle(i, positions, velocities, lifetimes, sizes, colors);
      } else {
        // 更新位置
        positions[i * 3] += velocities[i * 3] * delta;
        positions[i * 3 + 1] += velocities[i * 3 + 1] * delta;
        positions[i * 3 + 2] += velocities[i * 3 + 2] * delta;
        
        // 重力
        velocities[i * 3 + 1] -= this.gravity * delta;
        
        // 阻力
        velocities[i * 3] *= this.drag;
        velocities[i * 3 + 1] *= this.drag;
        velocities[i * 3 + 2] *= this.drag;
      }
    }
    
    this.particles.geometry.attributes.position.needsUpdate = true;
  }
  
  // 设置位置
  setPosition(x, y, z) {
    this.position.set(x, y, z);
  }
  
  // 获取 Three.js 对象
  getObject3D() {
    return this.particles;
  }
  
  // 添加到场景
  addToScene(scene) {
    scene.add(this.particles);
  }
  
  // 从场景移除
  removeFromScene(scene) {
    scene.remove(this.particles);
  }
  
  // 释放资源
  dispose() {
    this.particles.geometry.dispose();
    this.particles.material.dispose();
  }
}

// 使用示例
const emitter = new ParticleEmitter({
  particleCount: 2000,
  position: new THREE.Vector3(0, 0, 0),
  direction: new THREE.Vector3(0, 1, 0),
  speed: 3,
  spread: 0.5,
  lifetime: 3,
  gravity: 0.5,
  color: new THREE.Color(1, 0.5, 0)
});

emitter.addToScene(scene);

function animate() {
  emitter.update(0.016);
  renderer.render(scene, camera);
}

GPU 粒子

使用着色器在 GPU 上计算粒子动画,性能更好:

javascript
function createGPUParticles(count = 10000) {
  const geometry = new THREE.BufferGeometry();
  
  // 初始位置
  const positions = new Float32Array(count * 3);
  // 随机种子(用于着色器中的随机计算)
  const seeds = new Float32Array(count);
  
  for (let i = 0; i < count; i++) {
    positions[i * 3] = (Math.random() - 0.5) * 20;
    positions[i * 3 + 1] = Math.random() * 20;
    positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
    
    seeds[i] = Math.random();
  }
  
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('seed', new THREE.BufferAttribute(seeds, 1));
  
  const material = new THREE.ShaderMaterial({
    uniforms: {
      time: { value: 0 },
      baseSize: { value: 0.1 },
      speed: { value: 2.0 },
      color1: { value: new THREE.Color(0xff6600) },
      color2: { value: new THREE.Color(0xffcc00) }
    },
    vertexShader: `
      uniform float time;
      uniform float baseSize;
      uniform float speed;
      
      attribute float seed;
      
      varying vec3 vColor;
      varying float vAlpha;
      
      // 伪随机函数
      float random(float n) {
        return fract(sin(n) * 43758.5453123);
      }
      
      void main() {
        // 基于 seed 的随机参数
        float randOffset = random(seed) * 100.0;
        float randSpeed = speed * (0.5 + random(seed + 1.0) * 1.0);
        float randSize = baseSize * (0.5 + random(seed + 2.0) * 1.0);
        
        // 计算动画位置
        vec3 pos = position;
        
        // 循环下落
        float cycleTime = 20.0 / randSpeed;
        float t = mod(time + randOffset, cycleTime) / cycleTime;
        
        pos.y = 10.0 - t * 20.0;
        
        // 飘动效果
        pos.x += sin(time * 2.0 + seed * 10.0) * 0.5;
        pos.z += cos(time * 2.0 + seed * 10.0) * 0.5;
        
        // 颜色(基于高度)
        float colorMix = (pos.y + 10.0) / 20.0;
        vColor = mix(vec3(1.0, 0.4, 0.0), vec3(1.0, 0.8, 0.0), colorMix);
        
        // 透明度(底部淡出)
        vAlpha = smoothstep(-10.0, 0.0, pos.y);
        
        vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
        gl_PointSize = randSize * (300.0 / -mvPosition.z);
        gl_Position = projectionMatrix * mvPosition;
      }
    `,
    fragmentShader: `
      varying vec3 vColor;
      varying float vAlpha;
      
      void main() {
        // 圆形粒子
        float dist = length(gl_PointCoord - vec2(0.5));
        if (dist > 0.5) discard;
        
        // 边缘柔化
        float alpha = 1.0 - smoothstep(0.3, 0.5, dist);
        alpha *= vAlpha;
        
        gl_FragColor = vec4(vColor, alpha);
      }
    `,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
  });
  
  const particles = new THREE.Points(geometry, material);
  
  return particles;
}

// 使用
const gpuParticles = createGPUParticles(50000);
scene.add(gpuParticles);

function animate() {
  gpuParticles.material.uniforms.time.value = performance.now() * 0.001;
  renderer.render(scene, camera);
}

API 参考

THREE.Points

属性/方法类型说明
geometryBufferGeometry几何体
materialMaterial材质
isPointsBoolean类型标识
computeBoundingBox()Method计算包围盒
computeBoundingSphere()Method计算包围球

PointsMaterial

属性类型默认值说明
colorColor0xffffff粒子颜色
sizeNumber1粒子大小
sizeAttenuationBooleantrue大小是否随距离衰减
mapTexturenull纹理贴图
alphaMapTexturenullAlpha 贴图
transparentBooleanfalse是否透明
opacityNumber1透明度
vertexColorsBooleanfalse是否使用顶点颜色
depthWriteBooleantrue是否写入深度
depthTestBooleantrue是否深度测试
blendingBlendingNormalBlending混合模式

BufferAttribute

方法说明
setArray(array)设置数据数组
set needsUpdate(value)标记需要更新
getX(index)获取指定索引的值
setX(index, value)设置指定索引的值
getY(index)获取 Y 分量
getZ(index)获取 Z 分量

配置参数详解

混合模式

javascript
// 正常混合
material.blending = THREE.NormalBlending;

// 叠加混合(发光效果)
material.blending = THREE.AdditiveBlending;

// 减法混合
material.blending = THREE.SubtractiveBlending;

// 乘法混合
material.blending = THREE.MultiplyBlending;

深度设置

javascript
// 透明粒子通常需要禁用深度写入
material.depthWrite = false;  // 防止前后遮挡问题

// 完全禁用深度测试(粒子始终在最前)
material.depthTest = false;

大小衰减

javascript
// 启用衰减(近大远小)
material.sizeAttenuation = true;

// 禁用衰减(所有粒子同大小)
material.sizeAttenuation = false;

// 在着色器中自定义衰减
gl_PointSize = baseSize * (constant / -mvPosition.z);

纹理配置

javascript
// 基础纹理
material.map = texture;

// Alpha 贴图
material.alphaMap = alphaTexture;

// 纹理过滤
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;

// 纹理包裹
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;

性能优化

粒子数量优化

javascript
// 根据设备性能调整粒子数量
const isMobile = /Mobile/.test(navigator.userAgent);
const particleCount = isMobile ? 1000 : 10000;

// 或使用 GPU 信息
const gl = renderer.getContext();
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const recommendedCount = maxTextureSize > 4096 ? 50000 : 10000;

使用 GPU 动画

javascript
// ❌ CPU 动画(性能差)
function animate() {
  const positions = geometry.attributes.position.array;
  for (let i = 0; i < count; i++) {
    positions[i * 3 + 1] -= 0.1;  // CPU 计算
  }
  geometry.attributes.position.needsUpdate = true;
}

// ✅ GPU 动画(性能好)
// 在着色器中计算位置
vertexShader: `
  uniform float time;
  void main() {
    vec3 pos = position;
    pos.y -= time * speed;  // GPU 计算
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
  }
`

减少更新频率

javascript
// 不是每帧都更新所有粒子
let updateIndex = 0;
function partialUpdate(count) {
  const batchSize = Math.floor(particleCount / 60);  // 分60帧更新完
  
  for (let i = 0; i < batchSize; i++) {
    const idx = (updateIndex + i) % particleCount;
    // 更新粒子 idx
  }
  
  updateIndex = (updateIndex + batchSize) % particleCount;
  geometry.attributes.position.needsUpdate = true;
}

使用 InstancedBuffer

javascript
// 对于更复杂的粒子,可以使用 InstancedBuffer
import { InstancedBufferGeometry, InstancedBufferAttribute } from 'three';

const geometry = new InstancedBufferGeometry();
// ... 配置实例化几何体

LOD 粒子系统

javascript
// 根据距离使用不同的粒子数量
const lod = new THREE.LOD();

// 近距离:高密度粒子
lod.addLevel(highDensityParticles, 0);

// 中距离:中等密度
lod.addLevel(mediumDensityParticles, 50);

// 远距离:低密度粒子
lod.addLevel(lowDensityParticles, 100);

scene.add(lod);

常见问题

Q1: 粒子显示为方块而不是圆形?

A: 需要使用纹理或在着色器中裁剪:

javascript
// 方法一:使用圆形纹理
const texture = createParticleTexture();
material.map = texture;

// 方法二:着色器裁剪
fragmentShader: `
  void main() {
    float dist = length(gl_PointCoord - vec2(0.5));
    if (dist > 0.5) discard;  // 裁剪圆形外
    gl_FragColor = vec4(color, 1.0);
  }
`

Q2: 粒子透明度有问题?

A: 检查以下设置:

javascript
// 1. 启用透明
material.transparent = true;
material.opacity = 0.8;

// 2. 禁用深度写入
material.depthWrite = false;

// 3. 使用合适的混合模式
material.blending = THREE.AdditiveBlending;

// 4. 纹理要有 alpha 通道
// Canvas 纹理需要正确设置 alpha
ctx.fillStyle = 'rgba(255, 255, 255, 0)';  // 透明背景

Q3: 粒子排序问题?

A: 粒子默认不排序,解决方案:

javascript
// 方法一:禁用深度测试(粒子始终在最前)
material.depthTest = false;

// 方法二:手动排序(性能开销大)
function sortParticles() {
  const positions = geometry.attributes.position.array;
  const cameraPosition = camera.position;
  
  // 计算每个粒子到相机的距离
  const distances = [];
  for (let i = 0; i < particleCount; i++) {
    const dx = positions[i * 3] - cameraPosition.x;
    const dy = positions[i * 3 + 1] - cameraPosition.y;
    const dz = positions[i * 3 + 2] - cameraPosition.z;
    distances.push({ index: i, distance: dx*dx + dy*dy + dz*dz });
  }
  
  // 从远到近排序
  distances.sort((a, b) => b.distance - a.distance);
  
  // 重新排列数据
  // ...
}

// 方法三:使用叠加混合避免排序问题
material.blending = THREE.AdditiveBlending;

Q4: 如何实现粒子碰撞?

A: 简单碰撞检测:

javascript
function checkCollisions(particles, boundingBox) {
  const positions = particles.geometry.attributes.position.array;
  
  for (let i = 0; i < particleCount; i++) {
    const x = positions[i * 3];
    const y = positions[i * 3 + 1];
    const z = positions[i * 3 + 2];
    
    // 边界碰撞
    if (x < boundingBox.min.x || x > boundingBox.max.x) {
      velocities[i].x *= -0.8;  // 反弹
    }
    if (y < boundingBox.min.y || y > boundingBox.max.y) {
      velocities[i].y *= -0.8;
    }
    if (z < boundingBox.min.z || z > boundingBox.max.z) {
      velocities[i].z *= -0.8;
    }
    
    // 地面碰撞
    if (y < 0) {
      positions[i * 3 + 1] = 0;
      velocities[i].y *= -0.5;
      velocities[i].x *= 0.9;  // 摩擦
      velocities[i].z *= 0.9;
    }
  }
}

Q5: 粒子数量太多导致性能问题?

A: 优化策略:

javascript
// 1. 减少粒子数量
const maxParticles = Math.min(targetCount, 10000);

// 2. 使用 GPU 着色器动画
// 参见 GPU 粒子章节

// 3. 降低更新频率
// 每隔几帧更新一次

// 4. 分块更新
// 每帧只更新一部分粒子

// 5. 使用简化效果
// 远距离减少粒子细节

// 6. 使用点精灵而非网格
// THREE.Points 已是最优方案

Q6: 如何让粒子面向相机?

A: 粒子默认就是面向相机的(Billboard),但可以自定义:

javascript
// 默认行为:粒子始终面向相机
material.sizeAttenuation = true;

// 自定义朝向(在着色器中)
vertexShader: `
  void main() {
    // 广告牌矩阵
    vec3 cameraRight = vec3(modelViewMatrix[0][0], modelViewMatrix[1][0], modelViewMatrix[2][0]);
    vec3 cameraUp = vec3(modelViewMatrix[0][1], modelViewMatrix[1][1], modelViewMatrix[2][1]);
    
    vec3 vertexPosition = position
      + cameraRight * aOffset.x
      + cameraUp * aOffset.y;
    
    gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);
  }
`

Q7: 如何实现粒子轨迹?

A: 存储历史位置:

javascript
// 方法一:使用多条线段
const trailGeometry = new THREE.BufferGeometry();
const trailPositions = new Float32Array(maxTrailLength * 3);
trailGeometry.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));

const trailMaterial = new THREE.LineBasicMaterial({
  color: 0xffffff,
  transparent: true,
  opacity: 0.5
});

const trail = new THREE.Line(trailGeometry, trailMaterial);

// 更新轨迹
function updateTrail(newPosition) {
  const positions = trailGeometry.attributes.position.array;
  
  // 移动所有点
  for (let i = positions.length - 3; i >= 3; i -= 3) {
    positions[i] = positions[i - 3];
    positions[i + 1] = positions[i - 2];
    positions[i + 2] = positions[i - 1];
  }
  
  // 添加新点
  positions[0] = newPosition.x;
  positions[1] = newPosition.y;
  positions[2] = newPosition.z;
  
  trailGeometry.attributes.position.needsUpdate = true;
}

// 方法二:使用后处理运动模糊

相关链接