实现一个烟花效果

 1. 首先创建一个烟花类,有烟花上升的效果,烟花爆炸的两种效果(爆炸型和球型)

 2. 创建线的属性方法,因为上升效果和爆炸效果都会用到

 3. 上升效果为了达到那种螺旋上升的效果可以通过sin函数实现一个点的偏移量

 4. 爆炸效果则是将随机生成多条半径不同的线

 5. 球形效果则是将规则的点和不规则的点结合起来实现的

 6. 每个烟花爆炸消失后重新生成下一个烟花

 7. 为了色彩值更弄,通过 EffectComposer\UnrealBloomPass添加辉光效果

/**
 * 添加烟花
 */
function addFireworks() {
  // 烟花类
  class Fireworks {
    constructor(props = {}) {
      // 烟花高度
      this.height = props.height || Math.ceil(Math.random() * 8 + 5);

      // 烟花颜色
      this.color = props.color || this.getRandomColor();

      // 烟花的起始位置
      this.startX = props.position && props.position[0] || Math.random() * 20 - 10;

      this.startY = -5;

      this.path = this.createPath();

      // 烟花绽放结束函数
      this.fireEnd = null;

      this.fire();
    }

    /**
     * 生成随机颜色值
     * @returns THREE.Color
     */
    getRandomColor() {
      const [minHue, maxHue, minSaturation, maxSaturation, minLightness, maxLightness] = [0, 1, 0, 1, 0, 1];
      // 生成随机色调
      const hue = Math.random() * (maxHue - minHue) + minHue;
      // 生成随机饱和度
      const saturation = Math.random() * (maxSaturation - minSaturation) + minSaturation;
      // 生成随机亮度
      const lightness = Math.random() * (maxLightness - minLightness) + minLightness;
  
      // 使用 HSL 颜色空间创建颜色
      const color = new THREE.Color();
      color.setRGB(hue, saturation, lightness);
  
      return color;
    }

    /**
     * 生成烟花上升的轨迹,为了模拟烟花上升左右摆动效果,增加了sin函数插入部分偏移点
     */
    createPath() {
      const widthBeta = 0.01; // 烟花轨迹摆动的范围
      const paths = []; // 轨迹点集
      const nums = 60; // 轨迹点个数
      const heigthtBeta = this.height / nums; // 烟花上升的幅度
      for (let i = 0; i < nums; i++) {
        paths.push(new THREE.Vector3(this.startX + Math.sin(heigthtBeta * i * Math.PI * 14) * widthBeta, this.startY + heigthtBeta * i, 0));
      }
      return paths;
    }

    /**
     * 爆炸效果
     */
    explode() {
      const random = Math.floor(Math.random() * 2);
      if (random === 0) {
        this.radialEffect();
      } else {
        this.sphereEffect();
      }
    }

    /**
     * 球形效果,为了模拟实际的点燃效果,首先生成一些规则的球型点,然后在球面上插入部分随机点
     */
    sphereEffect() {
      const vertex = `
        attribute float aScale;
        attribute vec3 aRandom;
        uniform float uTime; 

        uniform float uSize;
        void main() {
            vec4 modelPosition = modelMatrix * vec4(position, 1.0);
            modelPosition.xyz += 5.0*uTime;
            gl_Position = projectionMatrix * (viewMatrix * modelPosition);
            gl_PointSize = 3.0;
        }
      `;
      const frag = `
        uniform vec3 uColor;
        uniform float uOpacity; 
        void main() {
            float distanceTo = distance(gl_PointCoord , vec2(0.5));
            // 必须有这段代码,不然辉光效果不会生效
            if (distanceTo > 0.5) {
              discard;
            }
            float str = distanceTo * 2.0;
            str = 1.0 - str;
            str = pow(str,1.5);
            gl_FragColor = vec4(uColor,str * uOpacity); 
        }
      `;

      const positions = [];
      const [centerX, centerY, centerZ] = [this.startX, this.startY + this.height, 0];
      const radius = 2;
      // 生成规则的点
      const widthSegments = 24;
      const heightSegments = 24;
      for (let i = 0; i < widthSegments; i++) {
        for (let j = 0; j < heightSegments; j++) {
          const y = radius * Math.sin(j / heightSegments * Math.PI * 2);
          const innerRadius = radius * Math.cos(j / heightSegments * Math.PI * 2);
          const x = innerRadius * Math.cos(i / widthSegments * Math.PI * 2);
          const z = innerRadius * Math.sin(i / widthSegments * Math.PI * 2);
          positions.push(centerX + x, centerY + y, centerZ + z);
        }
      }
      // 生成随机的点
      const num = 360;
      for (let i = 0; i < num; i++) {
        const randomHAngle = Math.random() * Math.PI * 2 - Math.PI;
        const randomWAngle = Math.random() * Math.PI * 1;
        const y = radius * Math.sin(randomHAngle);
        const innerRadius = radius * Math.cos(randomHAngle);
        const x = innerRadius * Math.cos(randomWAngle);
        const z = innerRadius * Math.sin(randomWAngle);
        positions.push(centerX + x, centerY + y, centerZ + z);
      }

      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
      
      const material = new THREE.ShaderMaterial({
        vertexShader: vertex,
        fragmentShader: frag,
        side: THREE.DoubleSide,
        uniforms: {
          uColor: { value: this.color },
          uTime: { value: 0.01 },
          uSize: { value: 5.0 },
          uOpacity: { value: 1.0 }
        },
        transparent: true,
        depthWrite: false // 解决透明的点和点相互压盖时没有透明效果的问题
      });
      const mesh = new THREE.Points(geometry, material);
      scene.add(mesh);

      // 添加定时函数,达到透明度逐渐降低,直至消失的效果
      let i = 0;
      const interObj = setInterval(() => {
        material.uniforms.uOpacity.value -= 0.05;
        if (material.uniforms.uOpacity.value <= 0) {
          clearInterval(interObj);
          scene.remove(mesh);

          if (this.fireEnd) {
            this.fireEnd();
          }
        }
        i++;
      }, 100)
    }

    /**
     * 射线效果,随机生成不同的线长实现
     */
    radialEffect() {
      function getRandom(src) {
        const radius = 4;
        return src + Math.random() * radius - radius / 2;
      }
      const nums = 180;
      const [centerX, centerY, centerZ] = [this.startX, this.startY + this.height, 0];
      const positions = [];
      
      for (let i = 0; i < nums; i++) {
        positions.push(
          [
            new THREE.Vector3(centerX, centerY, centerZ),
            new THREE.Vector3(getRandom(centerX), getRandom(centerY), getRandom(centerZ)),
          ]
        );
      }
      positions.forEach((e, i) => {
        this.createLine(e, 3, 0.55, 1.5, null, 100, i === 0 ? () => {
          if (this.fireEnd) {
            this.fireEnd();
          }
        } : null);
      })
    }

    /**
     * 生成一条线,有箭头效果
     * @param {*} points 线的关键点,在函数内部会根据这些点通过平滑函数插入多个点
     * @param {*} lineWidth 线宽
     * @param {*} lineLength 线长
     * @param {*} uSpeed 线的移动速度
     * @param {*} color 线颜色
     * @param {*} pointNum 插入的点的个数
     * @param {*} endCallback 线移动结束的回调函数
     */
    createLine(points, lineWidth, lineLength, uSpeed, color, pointNum, endCallback) {
      const vertex = `
        attribute float aIndex;
        uniform float uTime;
        uniform float uNum; // 线上点个数
        uniform float uWidth; // 线宽
        uniform float uLength; // 线宽
        uniform float uSpeed; // 飞线速度
        varying float vIndex; // 内部点下标
        void main() {
          vec4 viewPosition = viewMatrix * modelMatrix * vec4(position, 1.0);
          gl_Position = projectionMatrix * viewPosition;
          
          vIndex = aIndex;
          // 通过时间点的增加作为点移动的下标,从末端的第一个点开始,已num点为一个轮回,往复执行运动
          float num = uNum - mod(floor(uTime * uSpeed), uNum);
          // 只绘制部分点,多余的不绘制
          if (aIndex + num >= uNum) {
            float size = (mod(aIndex + num, uNum) * (1.0 / uLength)) / uNum * uWidth;
            gl_PointSize = size;
          }
        }
      `;

      const frag = `
        varying float vIndex;
        uniform float uTime;
        uniform float uLength; // 线宽
        uniform float uNum;
        uniform float uSpeed;
        uniform vec3 uSColor;
        uniform vec3 uEColor;
        void main() {
          // 默认绘制的点是方形的,通过舍弃可以绘制成圆形
          float distance = length(gl_PointCoord - vec2(0.5, 0.5));
          if (distance > 0.5) {
            // discard;
            float glow = 1.0 - smoothstep(0.2, 0.5, distance);
            gl_FragColor = vec4(1.0, 1.0, 1.0, glow);
          } else {
            float num = uNum - mod(floor(uTime * uSpeed), uNum);
            // 根据点的下标计算渐变色值
            vec3 color = mix(uSColor, uEColor, (num + vIndex - uNum) / uNum);
            // 越靠近末端透明度越大
            float opacity = ((num + vIndex - uNum)) / uNum;
            // 根据长度计算显示点的个数,多余的透明显示
            if (vIndex + num >= uNum && vIndex + num <= uNum * (1.0 + uLength)) {
              gl_FragColor = vec4(color, opacity);
            } else {
              gl_FragColor = vec4(color, 0);
            }
          }
        }
      `;

      const nums = pointNum || 500;

      const curve = new THREE.CatmullRomCurve3(points);
      const curveArr = curve.getPoints(nums);
      const flatArr = curveArr.map(e => e.toArray());
      const lastArr = flatArr.flat();
      const indexArr = [...Array(nums + 1).keys()];
      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(lastArr), 3));
      geometry.setAttribute('aIndex', new THREE.BufferAttribute(new Float32Array(indexArr), 1));
      
      // 创建一根曲线
      const uniform = {
        uTime: { value: 0 },
        uSColor: { value: color || new THREE.Color(this.color) },
        uEColor: { value: new THREE.Color(0x000000) },
        uWidth: { value: lineWidth || 6.0 },
        uNum: { value: nums },
        uSpeed: { value: uSpeed || 2 },
        uLength: { value: lineLength || 0.25 }
      }

      const material = new THREE.ShaderMaterial({
        vertexShader: vertex,
        fragmentShader: frag,
        side: THREE.DoubleSide,
        uniforms: uniform,
        transparent: true,
        depthWrite: false
      });
      const mesh = new THREE.Points(geometry, material);
      scene.add(mesh);
      
      let i = 0;
      const interObj = setInterval(() => {
        material.uniforms.uTime.value = i;
        i++;
        if (uniform.uSpeed.value * i >= nums) {
          scene.remove(mesh);
          clearInterval(interObj);
          if (endCallback) {
            endCallback();
          }
        }
      }, 20)
    }

    /**
     * 点燃烟花
     */
    fire() {
      this.createLine(this.path, 8, 0.45, 2, null, null, () => {
        this.explode();
      });
    }

    /**
     * 销毁
     */
    destory() {
      clearInterval(this.interObj);
    }
  }

  function callback() {
    const fireObj = new Fireworks();
    fireObj.fireEnd = callback;
  }
  for (let i = 0; i < 5; i++) {
    setTimeout(() => {
      const fireObj = new Fireworks();
      fireObj.fireEnd = callback;
    }, 500 * i)
  }
}
烟花类以及添加烟花
/**
 * 添加相机等基础功能
 */
function addEnvir(lightFlag = true, axFlag = true, gridFlag = false) {
  // 初始化相机
  camera = new THREE.PerspectiveCamera(100, wWidth / wHeight, 0.01, 3000);
  camera.position.set(0, 10, 0);
  camera.lookAt(0, 0, 0);

  // 创建灯光
  // 创建环境光
  const ambientLight = new THREE.AmbientLight(0xf0f0f0, 1.0);
  ambientLight.position.set(0,0,0);
  scene.add(ambientLight);
  if (lightFlag) {
    // 创建点光源
    const pointLight = new THREE.PointLight(0xffffff, 1);
    pointLight.decay = 0.0;
    pointLight.position.set(200, 200, 50);
    scene.add(pointLight);
  }

  // 添加辅助坐标系
  if (axFlag) {
    const axesHelper = new THREE.AxesHelper(150);
    scene.add(axesHelper);
  }

  // 添加网格坐标
  if (gridFlag) {
    const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x004444);
    scene.add(gridHelper);
  }

  // 创建渲染器
  renderer = new THREE.WebGLRenderer({ antialias:true, logarithmicDepthBuffer: true });
  renderer.setClearColor(0x000000, 1);
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(wWidth, wHeight); //设置three.js渲染区域的尺寸(像素px)
  renderer.outputEncoding = THREE.sRGBEncoding;

  renderer.render(scene, camera); //执行渲染操作

  // 创建后处理效果合成器
  composer = new EffectComposer(renderer);
    
  // 添加渲染通道
  const renderPass = new RenderPass(scene, camera);
  composer.addPass(renderPass);

  const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
  bloomPass.threshold = 0;
  bloomPass.strength = 3;
  bloomPass.radius = 0.5;
  composer.addPass(bloomPass);

  controls = new OrbitControls(camera, renderer.domElement);
  // 设置拖动范围
  // controls.minPolarAngle = - Math.PI / 2;
  // controls.maxPolarAngle = Math.PI / 2 - Math.PI / 360;
  
  controls.addEventListener('change', () => {
    renderer.render(scene, camera);
  })

  gui = new dat.GUI();

  const clock = new THREE.Clock();
  function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
    const elapsedTime = clock.getElapsedTime();
    if (material && material.uniforms && material.uniforms.uTime) {
      material.uniforms.uTime.value = elapsedTime / 2;
      // material.uniforms.noiseScale.value = 10.0 + Math.sin(Date.now() * 0.001) * 5.0;
    }
    if (composer) {
      composer.render();
    }
  }
  render();

  document.getElementById('webgl').appendChild(renderer.domElement);
}
添加环境以及辉光效果

 

posted @ 2024-10-09 18:53  火星写程序  阅读(28)  评论(0编辑  收藏  举报