實現一個煙花效果

火星写程序發表於2024-10-09

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);
}
新增環境以及輝光效果

相關文章