Threejs烟花特效

粒子渲染

烟花的粒子渲染选择points + PointsMaterial + BufferGeometry。这里重点手动构建几何体的数据信息,包括点坐标,速度,颜色,重量。

pointsMaterial是渲染点的默认材质,如下使用

  const material = new THREE.PointsMaterial({
    size: 1, // 点大小
    color: 0xffffff,
    opacity: 1, // 透明度
    vertexColors: true,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthTest: false,
});

关键需要动态生成几何体的数据,代码j结构如下

const getPointMesh = (num, vels, type) => {
  // geometry
  const bufferGeometry = new THREE.BufferGeometry();
  const vertices = [];
  const velocities = [];
  const colors = [];
  const masses = [];

  for (let i = 0; i < num; i++) {
    const pos = new THREE.Vector3(0, 0, 0);
    vertices.push(pos.x, pos.y, pos.z);
    velocities.push(vels[i].x, vels[i].y, vels[i].z);
      let size= Math.pow(vels[i].y, 2) * 0.04;
      if (i === 0) size *= 1.1;
      masses.push(size * 0.017);
      colors.push(1.0, 1.0, 1.0, 1.0);
  }
  bufferGeometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3).setDynamic(true));
  bufferGeometry.addAttribute('velocity', new THREE.Float32BufferAttribute(velocities, 3).setDynamic(true));
  bufferGeometry.addAttribute('color', new THREE.Float32BufferAttribute(colors, 4).setDynamic(true));
  bufferGeometry.addAttribute('mass', new THREE.Float32BufferAttribute(masses, 1).setDynamic(true));

  const material = new THREE.PointsMaterial({
    size: 1,
    color: 0xffffff,
    opacity: 1,
    vertexColors: true,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthTest: false,
});

  return new THREE.Points(bufferGeometry, material);
};

生成bufferGeometry,循环生成position,循环次数表示生成的点的个数,每一个点的位置都是(0,0,0),每个点的位置相同,但是速度velocity不同,velocity数组由调用方传入,不同的调用方速度有不同的生成方法。颜色数组都是白色,重量数组每个值的大小设置为垂直速度分量成正比。

粒子更新

上面我们写了一个函数,接受数量和速度数组,可以创建出mesh返回,然后将该mesh加到场景中可以渲染出点,效果如下。因为所有点都在一个位置,所以只能看到一个点,同时为了方便查看下图将size设为100

接下来写状态更新部分逻辑,这里封装为Particle类,该类调用上面的函数获得实际渲染的mesh,类的update方法处理mesh中各个点位置的更新。

class ParticleMesh {
  constructor(num, vels, type) {
    this.particleNum = num;
    this.timerStartFading = 10;
    this.mesh = getPointMesh(num, vels, type);
  }
  disposeAll() {
    this.mesh.geometry.dispose();
    this.mesh.material.dispose();
  }

  update(gravity) {
    if (this.timerStartFading > 0) this.timerStartFading -= 0.3;
    const { position, velocity, color, mass } = this.mesh.geometry.attributes;
    const decrementRandom = () => (Math.random() > 0.5 ? 0.98 : 0.96);
    const decrementByVel = v => (Math.random() > 0.5 ? 0 : (1 - v) * 0.1);
    for (let i = 0; i < this.particleNum; i++) {
      const { x, y, z } = getOffsetXYZ(i);
      velocity.array[y] += gravity.y - mass.array[i];
      velocity.array[x] *= friction;
      velocity.array[z] *= friction;
      velocity.array[y] *= friction;
      position.array[x] += velocity.array[x];
      position.array[y] += velocity.array[y];
      position.array[z] += velocity.array[z];
      const { a } = getOffsetRGBA(i);
      if (this.timerStartFading <= 0) {
        color.array[a] *= decrementRandom() - decrementByVel(color.array[a]);
        if (color.array[a] < 0.001) color.array[a] = 0;
      }
    }
    position.needsUpdate = true;
    velocity.needsUpdate = true;
    color.needsUpdate = true;

  }

}

update方法的逻辑是取出几何体的属性,遍历所有的粒子,在速度数组的每个位置上更新速度,然后使用速度更新位置,取出颜色分量,对透明度分量做衰减。需要注意两个辅助函数getOffsetXYZ和getOffsetRGBA,是转换粒子编号和数组下标的。

烟花粒子类

烟花分为升空部分和爆炸部分。两者都是粒子,区别在粒子的速度,前者是大致相同,向上发射;后者是圆形发散。烟花类是协调管理升空粒子和爆炸粒子的上层类,初始时创建一个升空粒子类,也就是生成一个方向向上的速度数组用来创建发射粒子,然后调用发射粒子的update方法,更新粒子状态;当粒子到顶后(速度衰减为0),删除发射粒子,创建爆炸粒子,也就是生成方向四周发散的速度数组,用来创建爆炸粒子,然后调用爆炸粒子的update方法更新。同时记录烟花寿命状态,当衰减到0,从场景中删除烟花。

//update伪代码

update(gravity) {
    if (!this.isExploed) {
        this.seed.update(gravity)
    } else {
        this.flower.update(gravity);
        if (this.life > 0) this.life -= 1;
    }
}


// 生成升空粒子

    const num = 40;
    const vels = [];
    for (let i = 0; i < num; i++) {
      const vx = 0;
      const vy = i === 0 ? Math.random() * 2.5 + 0.9 : Math.random() * 2.0 + 0.4;
      const vz = 0;
      vels.push(new THREE.Vector3(vx, vy, vz));
    }
    // 生成速度,只有y方向有值,随机生成。
    const pm = new ParticleSeedMesh(num, vels);
    const x = Math.random() * 80 - 40;
    const y = -50;
    const z = Math.random() * 80 - 40;
    pm.mesh.position.set(x, y, z);
    // 设置mesh的位置,移动的发射初始位置
    
    
   
   // 使用极坐标生成爆炸速度
   
   for (let i = 0; i < num; i++) {
        radius = getRandomNum(120, 60) * 0.01;
        const theta = THREE.Math.degToRad(Math.random() * 180);
        const phi = THREE.Math.degToRad(Math.random() * 360);
        const vx = Math.sin(theta) * Math.cos(phi) * radius;
        const vy = Math.sin(theta) * Math.sin(phi) * radius;
        const vz = Math.cos(theta) * radius;
        const vel = new THREE.Vector3(vx, vy, vz);
        vel.multiplyScalar(this.flowerSizeRate);
        vels.push(vel);
      }
    
    

自定义shader

上面使用的是PointsMaterial,它画出来的点大小都是一样的,通过自定义shader可以控制每个点的大小能有区别

        // vertex
        precision mediump float;
        attribute vec3 position;
        uniform mat4 projectionMatrix;
        uniform mat4 modelViewMatrix;
        uniform float size;
        attribute float adjustSize;
        uniform vec3 cameraPosition;
        varying float distanceCamera;
        attribute vec3 velocity;
        attribute vec4 color;
        varying vec4 vColor;
        void main() {
            vColor = color;
            vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
            gl_PointSize = size * adjustSize * (100.0 / length(modelViewPosition.xyz));
            gl_Position = projectionMatrix * modelViewPosition;
        }

通过gl_PointSize控制每个点的大小,adjustSize是随机生成的数组,让点大小有随机性,length(modelViewPosition.xyz)则是让点大小和相机距离成反比。

这样子点的大小就会有所区别。

现在的问题是视觉上方块太明显,这里的解决方案是通过纹理给定一个颜色乘积系数,让点更模糊一点。

生成纹理

const drawRadialGradation = (ctx, canvasRadius, canvasW, canvasH) => {
  ctx.save();
  const gradient = ctx.createRadialGradient(canvasRadius, canvasRadius, 0, canvasRadius, canvasRadius, canvasRadius);
  gradient.addColorStop(0.0, 'rgba(255,255,255,1.0)');
  gradient.addColorStop(0.5, 'rgba(255,255,255,0.5)');
  gradient.addColorStop(1.0, 'rgba(255,255,255,0)');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, canvasW, canvasH);
  ctx.restore();
};

使用纹理

       // fragement
        precision mediump float;
        uniform sampler2D texture;
        varying vec4 vColor;
        void main() {

            vec4 color = vec4(texture2D(texture, gl_PointCoord));
            gl_FragColor = color * vColor;
        }

猜你喜欢

转载自juejin.im/post/7256010930017632293