Threejs fireworks special effects

particle rendering

For particle rendering of fireworks, select points + PointsMaterial + BufferGeometry. The focus here is to manually construct the data information of the geometry, including point coordinates, speed, color, and weight.

pointsMaterial is the default material for rendering points, used as follows

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

The key is to dynamically generate geometry data, the code j structure is as follows

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);
};

Generate bufferGeometry, loop to generate position, the number of loops indicates the number of generated points, the position of each point is (0,0,0), the position of each point is the same, but the velocity is different, the velocity array is passed by the caller input, different caller velocities have different generation methods. The color array is all white, and the size of each value in the weight array is set to be proportional to the vertical velocity component.

particle update

Above we wrote a function that accepts the number and speed array, can create a mesh and return it, and then add the mesh to the scene to render the point, the effect is as follows. Because all the points are in one position, only one point can be seen, and the size is set to 100 for the convenience of viewing the following figure

Next, write the state update logic, which is encapsulated as a Particle class. This class calls the above function to obtain the actual rendered mesh, and the update method of the class handles the update of the position of each point in the 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;

  }

}

The logic of the update method is to take out the properties of the geometry, traverse all the particles, update the speed at each position of the speed array, then use the speed to update the position, take out the color component, and attenuate the transparency component. It should be noted that the two auxiliary functions getOffsetXYZ and getOffsetRGBA convert particle numbers and array subscripts.

Firework Particles

Fireworks are divided into a lift-off part and an explosion part. Both are particles, the difference lies in the speed of the particles, the former is roughly the same, emitting upwards; the latter is circular divergence. The fireworks class is the upper class that coordinates and manages the launch particles and the explosion particles. At the beginning, a launch particle class is created, that is, an upward speed array is generated to create the emitted particles, and then the update method of the emitted particles is called to update the state of the particles. ;When the particle reaches the top (the velocity decays to 0), delete the emitted particle and create an explosion particle, that is, generate an array of velocities that diverge around the direction, which is used to create the explosion particle, and then call the update method of the explosion particle to update. At the same time, record the life state of the fireworks, and when the decay reaches 0, delete the fireworks from the scene.

//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);
      }
    
    

custom 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;
        }

Guess you like

Origin juejin.im/post/7256010930017632293