Chapter 08

粒子系统与特效

十万粒子星空、魔法粒子效果、PostProcessing 辉光/景深/胶片颗粒——让 3D 场景充满生命力与沉浸感

1. Points + PointsMaterial 基础粒子

Three.js 中的粒子系统通过 Points 对象实现——它把 BufferGeometry 的每个顶点渲染为一个面向屏幕的方形精灵(billboard):

// 基础粒子系统
const count = 5000;
const positions = new Float32Array(count * 3);

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

const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const mat = new THREE.PointsMaterial({
  size: 0.05,          // 粒子大小(世界单位)
  color: 0xffffff,
  transparent: true,
  opacity: 0.8,
  sizeAttenuation: true,  // 远处粒子更小(透视效果)
  depthWrite: false        // 防止粒子遮挡后面粒子
});

const particles = new THREE.Points(geo, mat);
scene.add(particles);

2. 自定义粒子纹理

// 用圆形纹理替换方形粒子(更自然的外观)
const loader = new THREE.TextureLoader();
const particleTex = loader.load('/textures/particle.png');

const mat = new THREE.PointsMaterial({
  size: 0.1,
  map: particleTex,
  transparent: true,
  alphaMap: particleTex,   // 或用 alphaMap 单独控制透明
  alphaTest: 0.001,       // 透明度低于此值的像素不渲染(代替 depthWrite)
  vertexColors: true      // 启用顶点颜色(每个粒子不同颜色)
});

// 为每个粒子指定颜色
const colors = new Float32Array(count * 3);
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
  color.setHSL(Math.random(), 1, 0.7);
  colors[i * 3]     = color.r;
  colors[i * 3 + 1] = color.g;
  colors[i * 3 + 2] = color.b;
}
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));

3. 实战:十万粒子星空

// 高性能星空:10 万颗星
const STAR_COUNT = 100000;
const positions = new Float32Array(STAR_COUNT * 3);
const colors    = new Float32Array(STAR_COUNT * 3);
const sizes     = new Float32Array(STAR_COUNT);

const color = new THREE.Color();
for (let i = 0; i < STAR_COUNT; i++) {
  const i3 = i * 3;

  // 球形分布(半径 50~500 范围内)
  const r     = 50 + Math.random() * 450;
  const theta = Math.random() * Math.PI * 2;
  const phi   = Math.acos(2 * Math.random() - 1);

  positions[i3]     = r * Math.sin(phi) * Math.cos(theta);
  positions[i3 + 1] = r * Math.sin(phi) * Math.sin(theta);
  positions[i3 + 2] = r * Math.cos(phi);

  // 随机星色(白/蓝白/黄白)
  const t = Math.random();
  if (t < 0.7) {
    color.setRGB(1, 1, 1);           // 纯白
  } else if (t < 0.9) {
    color.setRGB(0.8, 0.9, 1);      // 蓝白
  } else {
    color.setRGB(1, 0.95, 0.8);     // 黄白
  }
  colors[i3] = color.r; colors[i3+1] = color.g; colors[i3+2] = color.b;

  sizes[i] = Math.random() * 1.5 + 0.5; // 随机大小
}

const starGeo = new THREE.BufferGeometry();
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
starGeo.setAttribute('color',    new THREE.BufferAttribute(colors,    3));

const starMat = new THREE.PointsMaterial({
  size: 0.15,
  vertexColors: true,
  transparent: true,
  opacity: 0.9,
  depthWrite: false,
  blending: THREE.AdditiveBlending // 加法混合:星光叠加更亮
});

const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);

// 星空缓慢旋转
function animate() {
  requestAnimationFrame(animate);
  stars.rotation.y += 0.0001;
  renderer.render(scene, camera);
}

4. PostProcessing 后处理

后处理(Post Processing)是在 Three.js 渲染完一帧后,对画面进行额外的图像处理效果,如辉光、景深、色调等:

// 安装后处理库(three/addons 中已内置基础效果)
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
import { BokehPass } from 'three/addons/postprocessing/BokehPass.js';

// ── 创建 EffectComposer(后处理管线)──
const composer = new EffectComposer(renderer);

// 第一个 Pass 必须是 RenderPass(正常渲染场景)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

// Bloom 辉光 — 亮部泛光效果
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.2,   // strength 强度
  0.4,   // radius 扩散半径
  0.85   // threshold 亮度阈值(超过此值才发光)
);
composer.addPass(bloomPass);

// Film 胶片颗粒 — 模拟老电影感
const filmPass = new FilmPass(
  0.35,   // noise intensity
  0.025,  // scanline intensity
  648,    // scanline count
  false   // grayscale
);
composer.addPass(filmPass);

// 景深 — BokehPass
const bokehPass = new BokehPass(scene, camera, {
  focus: 5.0,    // 焦距(世界单位)
  aperture: 0.025, // 光圈(越大景深越浅)
  maxblur: 0.01
});
composer.addPass(bokehPass);

// ⚠️ 使用 composer.render() 替代 renderer.render()
function animate() {
  requestAnimationFrame(animate);
  composer.render(); // 代替 renderer.render(scene, camera)
}
⚠️

Bloom + 透明物体问题:UnrealBloomPass 会让所有物体都泛光。通常只希望特定物体发光时,可以用"选择性 Bloom"技术:将场景渲染两次,第二次只渲染发光物体,然后叠加。或使用 postprocessing npm 包(比内置 addons 更强大)。

5. 实战:魔法粒子效果

// 漂浮魔法粒子——每帧更新粒子位置
const MAGIC_COUNT = 2000;
const magicPos = new Float32Array(MAGIC_COUNT * 3);
const velocities = [];

for (let i = 0; i < MAGIC_COUNT; i++) {
  const i3 = i * 3;
  magicPos[i3]     = (Math.random() - 0.5) * 4;
  magicPos[i3 + 1] = Math.random() * 4;
  magicPos[i3 + 2] = (Math.random() - 0.5) * 4;
  velocities.push({
    x: (Math.random() - 0.5) * 0.01,
    y:  Math.random()         * 0.02,
    z: (Math.random() - 0.5) * 0.01
  });
}

const magicGeo = new THREE.BufferGeometry();
magicGeo.setAttribute('position', new THREE.BufferAttribute(magicPos, 3));

const magicMat = new THREE.PointsMaterial({
  size: 0.08, color: 0x88aaff,
  transparent: true, opacity: 0.9,
  depthWrite: false,
  blending: THREE.AdditiveBlending
});

const magic = new THREE.Points(magicGeo, magicMat);
scene.add(magic);

function animate() {
  requestAnimationFrame(animate);

  const pos = magicGeo.attributes.position;
  for (let i = 0; i < MAGIC_COUNT; i++) {
    pos.setX(i, pos.getX(i) + velocities[i].x);
    pos.setY(i, pos.getY(i) + velocities[i].y);
    pos.setZ(i, pos.getZ(i) + velocities[i].z);

    // 超出范围重置
    if (pos.getY(i) > 4) pos.setY(i, 0);
  }
  pos.needsUpdate = true; // 通知 GPU 数据已更改

  renderer.render(scene, camera);
}

本章小结:粒子系统 = BufferGeometry(位置数组)+ PointsMaterial + Points。性能关键:depthWrite: false 避免排序问题;AdditiveBlending 让光粒叠加更亮。后处理一定要用 composer.render() 替代直接渲染,Bloom 能让粒子效果瞬间升华。