threejs实现粒子拖尾效果

323 阅读2分钟

拖尾.png 视频:threejs实现粒子拖尾效果-CSDN博客

 1、定义ParticlesTail粒子拖尾类

本效果粒子发射器最基本的的属性有:粒子发射速度、最大粒子数

一个粒子最基本的的属性有:初始位置、出生时间、粒子持续时间、粒子大小和粒子颜色。

初始位置:需要添加拖尾效果的物体实现位置所在半径r的圆范围里随机生成;

出生时间、粒子持续时间就是字面意思;

粒子当前进程:(当前时间-出生时间)/粒子持续时间

粒子大小:根据最大粒子大小×(1-粒子当前进程),实现粒子越来越小,这是最简单的线性减小,你可以根据需要自己改变公式;

粒子颜色:mix(粒子颜色1,粒子颜色2,粒子当前进程)实现渐变,这是最基础的线性混合,你可自定义实现颜色;

class ParticlesTail {
  #count = 0
  #isStart = true
  constructor(options = {}) {

    const defaultOptions = Object.assign({
      color1: 0x3366bb,
      color2: 0xff2266,
      opacity: 1,
      map: new THREE.TextureLoader().load(`${VITE_PUBLIC_PATH || '/public/'}textures/sprites/circle.png`),
      maxSize: 10,
      maxCount: 30000,
      lastTime: 4000,
      emitSpeed: 0.5,
    }, options)

    this.options = new Proxy(Object.assign(defaultOptions, options), {
      set: (target, key, value) => {
        switch (key) {
          case 'emitSpeed':
            this.maxCount = this.options.lastTime * value
            this.#isStart = true
            this.#count = 0
            break;
          case 'lastTime':
            this.maxCount = value * this.options.emitSpeed
            this.#isStart = true
            this.#count = 0
            break;
          case 'maxSize':
            this.uniforms.u_maxSize.value = value
            break;
          case 'opacity':
            this.uniforms.u_opacity.value = value
            break;
          case 'color1':
            this.uniforms.u_color1.value = new THREE.Color(value)
            break;
          case 'color2':
            this.uniforms.u_color2.value = new THREE.Color(value)
            break;
        }
        target[key] = value;
        return true;
      },
      get: (target, key) => {
        return target[key];
      }
    });
    this.maxCount = this.options.lastTime * this.options.emitSpeed
    this.init()
  }

}

2、初始化粒子

使用BufferGeometry生成粒子只需要位置position和出生时间bornTime属性,粒子的颜色和大小因为需要根据粒子当前进程决定所以最好的办法就是使用ShaderMaterial进行控住,在顶点着色器上这里只做了一个简单的曲线运动,推荐使用柏林噪声函数或噪声贴图再加一点当前物体位置对粒子的影响让粒子运动更加合理

init() {
    const particleCount = this.options.maxCount;
    const particles = new THREE.BufferGeometry();
    const positions = new Float32Array(particleCount * 3);
    const bornTime = new Float32Array(particleCount);


    particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    particles.setAttribute('bornTime', new THREE.BufferAttribute(bornTime, 1));
    particles.setDrawRange(0, 0)
    this.uniforms = {
      u_time: { value: 0 },
      u_lastTime: { value: this.options.lastTime },
      u_position: { value: new THREE.Vector3() },
      u_maxSize: { value: this.options.maxSize },

      u_opacity: { value: this.options.opacity },
      u_texture: { value: this.options.map },
      u_color1: { value: new THREE.Color(this.options.color1) },
      u_color2: { value: new THREE.Color(this.options.color2) },
    }
    const material = new THREE.ShaderMaterial({

      uniforms: this.uniforms,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      depthTest: false,

      vertexShader: `
        attribute float bornTime;
        
        uniform float u_time;
        uniform vec3 u_position;
        uniform float u_lastTime;
        uniform float u_maxSize;
        

        varying float v_radio;

        float random(float x)
        {
          float y = fract(sin(x)*100000.0);
          return y;
        }
        
        void main() {
          float t = (u_time - bornTime)*0.001;

          v_radio = t*1000.0 / u_lastTime;
   
          float length = random(position.x);
          vec3 _position = vec3(position.x, position.y, position.z);
          _position.x = position.x + sin(t) * length;
          _position.y = position.y + cos(t) * length;
          _position.z = position.z + sin(t) * length;
          vec4 mvPosition = modelViewMatrix * vec4(_position, 1.0);
          gl_Position = projectionMatrix * mvPosition; 
          float size = u_maxSize * pow((1.5 - v_radio),1.0) ;
          gl_PointSize =  30.0 / (-mvPosition.z)*size;
        }
        `,
      fragmentShader: `
        uniform vec3 u_color1;
        uniform vec3 u_color2;
        uniform float u_opacity;
        uniform sampler2D u_texture;
        
        varying float v_radio;
        void main() {
          vec3 color = mix(u_color1,u_color2, v_radio);
          gl_FragColor = vec4(color, u_opacity*(1.2-pow(v_radio,2.0)));
          gl_FragColor = gl_FragColor * texture2D( u_texture, gl_PointCoord );
        }
        `,
    });
    this.points = new THREE.Points(particles, material);
  }

3、粒子更新函数

这里需要关注的是:更新材质中的时间、随机位置的r需要开三次方确保空间均匀,动画是分2个过程的开始阶段:粒子数量从0到最大粒子数(最大粒子数有粒子发射速度和粒子持续时间决定,不是最开始定义的最大粒子数),维持阶段:达到最大粒子数,新增的粒子根据this.#count(粒子更新所处positions数组位置)替换粒子当前进程接近1的久粒子,这样就可以实现粒子的生生息息

update(delta, position) {
    const positions = this.points.geometry.attributes.position.array;
    const bornTime = this.points.geometry.attributes.bornTime.array;
    const count = Math.floor(delta * this.options.emitSpeed)
    const time = performance.now()
    this.uniforms.u_time.value = time
    for (let i = this.#count; i < this.#count + count; i++) {
      const r = Math.cbrt(Math.random());
      const angle1 = Math.random() * Math.PI * 2;
      const angle2 = Math.random() * Math.PI - Math.PI / 2;
      positions[i * 3] = position.x + r * Math.cos(angle1) * Math.cos(angle2);
      positions[i * 3 + 1] = position.y + r * Math.sin(angle2);
      positions[i * 3 + 2] = position.z + r * Math.sin(angle1) * Math.cos(angle2);

      bornTime[i] = time
    }
    this.points.geometry.attributes.position.needsUpdate = true;
    this.points.geometry.attributes.bornTime.needsUpdate = true;
    this.#count += count
    if (this.#count >= this.maxCount) {
      this.#count = 0
      this.points.geometry.setDrawRange(0, this.maxCount)
      this.#isStart = false
    } else {
      this.#isStart && this.points.geometry.setDrawRange(0, this.#count);
    }
  }

4总代码

class ParticlesTail {
  #count = 0
  #isStart = true
  constructor(options = {}) {

    const defaultOptions = Object.assign({
      color1: 0x3366bb,
      color2: 0xff2266,
      opacity: 1,
      map: new THREE.TextureLoader().load(`${VITE_PUBLIC_PATH || '/public/'}textures/sprites/circle.png`),
      maxSize: 10,
      maxCount: 30000,
      lastTime: 4000,
      emitSpeed: 0.5,
    }, options)

    this.options = new Proxy(Object.assign(defaultOptions, options), {
      set: (target, key, value) => {
        switch (key) {
          case 'emitSpeed':
            this.maxCount = this.options.lastTime * value
            this.#isStart = true
            this.#count = 0
            break;
          case 'lastTime':
            this.maxCount = value * this.options.emitSpeed
            this.#isStart = true
            this.#count = 0
            break;
          case 'maxSize':
            this.uniforms.u_maxSize.value = value
            break;
          case 'opacity':
            this.uniforms.u_opacity.value = value
            break;
          case 'color1':
            this.uniforms.u_color1.value = new THREE.Color(value)
            break;
          case 'color2':
            this.uniforms.u_color2.value = new THREE.Color(value)
            break;
        }
        target[key] = value;
        return true;
      },
      get: (target, key) => {
        return target[key];
      }
    });
    this.maxCount = this.options.lastTime * this.options.emitSpeed
    this.init()
  }
  init() {
    const particleCount = this.options.maxCount;
    const particles = new THREE.BufferGeometry();
    const positions = new Float32Array(particleCount * 3);
    const bornTime = new Float32Array(particleCount);


    particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    particles.setAttribute('bornTime', new THREE.BufferAttribute(bornTime, 1));
    particles.setDrawRange(0, 0)
    this.uniforms = {
      u_time: { value: 0 },
      u_lastTime: { value: this.options.lastTime },
      u_position: { value: new THREE.Vector3() },
      u_maxSize: { value: this.options.maxSize },

      u_opacity: { value: this.options.opacity },
      u_texture: { value: this.options.map },
      u_color1: { value: new THREE.Color(this.options.color1) },
      u_color2: { value: new THREE.Color(this.options.color2) },
    }
    const material = new THREE.ShaderMaterial({

      uniforms: this.uniforms,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      depthTest: false,

      vertexShader: `
        attribute float bornTime;
        
        uniform float u_time;
        uniform vec3 u_position;
        uniform float u_lastTime;
        uniform float u_maxSize;
        

        varying float v_radio;

        float random(float x)
        {
          float y = fract(sin(x)*100000.0);
          return y;
        }
        
        void main() {
          float t = (u_time - bornTime)*0.001;

          v_radio = t*1000.0 / u_lastTime;
   
          float length = random(position.x);
          vec3 _position = vec3(position.x, position.y, position.z);
          _position.x = position.x + sin(t) * length;
          _position.y = position.y + cos(t) * length;
          _position.z = position.z + sin(t) * length;
          vec4 mvPosition = modelViewMatrix * vec4(_position, 1.0);
          gl_Position = projectionMatrix * mvPosition; 
          float size = u_maxSize * pow((1.5 - v_radio),1.0) ;
          gl_PointSize =  30.0 / (-mvPosition.z)*size;
        }
        `,
      fragmentShader: `
        uniform vec3 u_color1;
        uniform vec3 u_color2;
        uniform float u_opacity;
        uniform sampler2D u_texture;
        
        varying float v_radio;
        void main() {
          vec3 color = mix(u_color1,u_color2, v_radio);
          gl_FragColor = vec4(color, u_opacity*(1.2-pow(v_radio,2.0)));
          gl_FragColor = gl_FragColor * texture2D( u_texture, gl_PointCoord );
        }
        `,
    });
    this.points = new THREE.Points(particles, material);
  }
  update(delta, position) {
    const positions = this.points.geometry.attributes.position.array;
    const bornTime = this.points.geometry.attributes.bornTime.array;
    const count = Math.floor(delta * this.options.emitSpeed)
    const time = performance.now()
    this.uniforms.u_time.value = time
    for (let i = this.#count; i < this.#count + count; i++) {
      const r = Math.cbrt(Math.random());
      const angle1 = Math.random() * Math.PI * 2;
      const angle2 = Math.random() * Math.PI - Math.PI / 2;
      positions[i * 3] = position.x + r * Math.cos(angle1) * Math.cos(angle2);
      positions[i * 3 + 1] = position.y + r * Math.sin(angle2);
      positions[i * 3 + 2] = position.z + r * Math.sin(angle1) * Math.cos(angle2);

      bornTime[i] = time
    }
    this.points.geometry.attributes.position.needsUpdate = true;
    this.points.geometry.attributes.bornTime.needsUpdate = true;
    this.#count += count
    if (this.#count >= this.maxCount) {
      this.#count = 0
      this.points.geometry.setDrawRange(0, this.maxCount)
      this.#isStart = false
    } else {
      this.#isStart && this.points.geometry.setDrawRange(0, this.#count);
    }
  }
}