最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

three.js實現(xiàn)幾個炫酷的粒子特效實例(火焰、煙霧、煙花)

 更新時間:2026年05月23日 10:12:19   作者:答案answer  
Three.js是一款非常流行的JavaScript 3D庫,它可以輕松地在網(wǎng)頁上創(chuàng)建和顯示3D圖形,在Three.js中,你可以創(chuàng)建各種形狀、動畫和粒子系統(tǒng)等,這篇文章主要介紹了three.js實現(xiàn)幾個炫酷的粒子特效的相關(guān)資料,包括火焰、煙霧、煙花,需要的朋友可以參考下

前言

都說 3D(Three.js,Babylon.js) 是前端最后的護城河,為什么這么說呢?因為隨著AI編碼工具(Cursor,Trae)和當今主流框架(React,Vue)的生態(tài)發(fā)展成熟前端的門檻似乎被拉的更低了,因為通過AI編碼工具許多非前端崗位的開發(fā)者也可以很輕松的去實現(xiàn)一些門戶頁面和后臺管理系統(tǒng)了。

而3D前端(Three.js,Babylon.js)之所以是前端最后的護城河,我想更多是因為3D相關(guān)開發(fā)的內(nèi)容有更高的門檻和更高的難度吧。

本篇給大家分享一下如何使用Three.js去實現(xiàn)火焰,煙霧,煙花這三個粒子特效

涉及的Three.js核心API方法介紹

1.THREE.PointsMaterial

THREE.PointsMaterial 是一種專門用于 點渲染 的材質(zhì)(類似于 MeshBasicMaterial 是網(wǎng)格的材質(zhì))。它定義了每一個點的外觀,比如大小、顏色、是否透明等

2.THREE.Points

THREE.Points 是繼承自 THREE.Object3D 的渲染對象,用于 將幾何體中的所有頂點作為單個點渲染出來,搭配 PointsMaterial 使用。

3.THREE.BufferGeometry

是一種通過 緩沖區(qū)(Buffer)存儲頂點數(shù)據(jù) 的幾何體結(jié)構(gòu),用于高效地構(gòu)建和管理頂點數(shù)據(jù)

創(chuàng)建火焰粒子特效方法

這里將創(chuàng)建火焰特效的方法邏輯進行單獨抽離,方法接收以下幾個參數(shù),方便我們創(chuàng)建不同效果的火焰粒子

1.meshPosition 粒子位置

2.name 粒子名稱

3.color 粒子顏色

4.size 粒子大小

5.height 粒子高度

6.range 粒子范圍

7.praticleCount 粒子數(shù)量

hexToHSL 函數(shù)方法是用于將十六進制的顏色值轉(zhuǎn)化為HSL 對象

/**
 * 將十六進制顏色字符串轉(zhuǎn)換為HSL對象
 * @param hex 十六進制顏色字符串 (#RRGGBB)
 * @returns HSL對象
 */
 function hexToHSL(hex: string): { h: number; s: number; l: number } {
  // 移除#號
  hex = hex.replace(/^#/, '');
  // 解析十六進制值
  const r = parseInt(hex.substring(0, 2), 16) / 255;
  const g = parseInt(hex.substring(2, 4), 16) / 255;
  const b = parseInt(hex.substring(4, 6), 16) / 255;
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  let h = 0,
    s = 0;
  const l = (max + min) / 2;

  if (max !== min) {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r:
        h = (g - b) / d + (g < b ? 6 : 0);
        break;
      case g:
        h = (b - r) / d + 2;
        break;
      case b:
        h = (r - g) / d + 4;
        break;
    }
    h /= 6;
  }
  return { h, s, l };
}

/**
 * 創(chuàng)建火焰特效
 * @param params 特效參數(shù)
 * meshPosition 粒子位置
 * name 粒子名稱
 * color 粒子顏色
 * size 粒子大小
 * height 粒子高度
 * range 粒子范圍
 * praticleCount 粒子數(shù)量
 */
function createFireEffect(params: EffectParams) {
  const { meshPosition, name, color,size=1, height=1, range=1, praticleCount=1} = params;
  // 處理顏色參數(shù),將十六進制顏色值轉(zhuǎn)化為 HSL 格式
  const color =this.hexToHSL(color)
   // 加載火焰貼圖
  const fireTexture = textureLoader.load(fireImage);
  // 粒子系統(tǒng)參數(shù)
  const PARTICLE_COUNT = particleCount;
  const positions = new Float32Array(PARTICLE_COUNT * 3);
  const velocities = new Float32Array(PARTICLE_COUNT * 3);
  const lifetimes = new Float32Array(PARTICLE_COUNT);
  const startTimes = new Float32Array(PARTICLE_COUNT);
  const sizes = new Float32Array(PARTICLE_COUNT);
  const colors = new Float32Array(PARTICLE_COUNT * 3);
  const maxLifetime = 2.0; // 粒子最大存活時間(秒)
  // 初始化粒子
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    // 初始都放在發(fā)射點
    positions[3 * i] = 0;
    positions[3 * i + 1] = 0;
    positions[3 * i + 2] = 0;

    // 隨機速度,主要向上,加一點隨機散射
    velocities[3 * i] = (Math.random() - 0.5) * 0.5 * range;
    velocities[3 * i + 1] = (Math.random() * 1.5 + 1.0) * height;
    velocities[3 * i + 2] = (Math.random() - 0.5) * 0.5 * range;

    // 生命周期
    lifetimes[i] = Math.random() * maxLifetime;
    startTimes[i] = 0; // 會在動畫中重置

    // 底部火焰更小,應(yīng)用size參數(shù)
    sizes[i] = (Math.random() * 0.2 + 0.2) * size;

    // 應(yīng)用自定義顏色
    const c = new THREE.Color().setHSL(
      color.h + Math.random() * 0.03,
      color.s,
      color.l
    );
    colors[3 * i] = c.r;
    colors[3 * i + 1] = c.g;
    colors[3 * i + 2] = c.b;
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
  geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
  geometry.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
  geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

  const material = new THREE.PointsMaterial({
    size: 0.2 * size,
    map: fireTexture,
    vertexColors: true,
    transparent: true,
    depthWrite: false, // 禁止寫入深度,讓重疊粒子都可見
    blending: THREE.AdditiveBlending, // 加法混合
  });
  // 釋放紋理資源
  fireTexture.dispose();
  const particles = new THREE.Points(geometry, material);
  particles.name = name;
  // 添加粒子方法標識
  particles.useData.effectMethod='CreateFireEffect'
  // 設(shè)置特效位置
  particles.position.copy(meshPosition);
  // 添加到場景
  this?.scene?.add(particles) 
  // 存儲到火焰特效集合中,便于后續(xù)更新
  this.effectsMap.set(particles.uuid, particles);
}

好了,這樣一個創(chuàng)建粒子火焰效果的方法就寫好了

但這個時候的火焰效果是靜態(tài)的,所有的點材質(zhì)內(nèi)容的位置都是固定的,這里為了實現(xiàn)一個動態(tài)的火焰效果,我們就需要通過 requestAnimationFrame 動畫循環(huán)的方式不間斷的更新點材質(zhì)的位置,顏色等參數(shù)值,從而實現(xiàn)一個動態(tài)的火焰粒子效果。

后面的煙霧粒子和煙花粒子的實現(xiàn)邏輯方式也是類似,都是通過不間斷的去修改點材質(zhì)的位置和顏色等值來實現(xiàn)的,后續(xù)就不過多闡述了。

以下是實現(xiàn)動態(tài)火焰粒子方法的代碼邏輯??

 /**
   * 更新火焰粒子動畫
   * @param effect 特效
   * @param delta 時間差
   * @param elapsed 經(jīng)過的時間
   */
  animateFireEffect(effect: THREE.Points, delta: number, elapsed: number) {
    const geometry = effect.geometry;
    const posAttr = geometry.getAttribute('position');
    const velAttr = geometry.getAttribute('velocity');
    const startAttr = geometry.getAttribute('startTime');
    const lifeAttr = geometry.getAttribute('lifetime');
    const sizeAttr = geometry.getAttribute('size');
    const colorAttr = geometry.getAttribute('color');

    const count = posAttr.count;
    for (let i = 0; i < count; i++) {
      let age = elapsed - startAttr.getX(i);

      // 如果粒子已超出生命周期,則重置它
      if (age > lifeAttr.getX(i)) {
        startAttr.setX(i, elapsed);
        age = 0;
        // 重置位置
        posAttr.setXYZ(i, 0, 0, 0);
      }

      // 更新位置:p += v * dt
      const vx = velAttr.getX(i);
      const vy = velAttr.getY(i);
      const vz = velAttr.getZ(i);

      posAttr.setXYZ(
        i,
        posAttr.getX(i) + vx * delta,
        posAttr.getY(i) + vy * delta,
        posAttr.getZ(i) + vz * delta
      );

      // 隨年齡衰減大小
      const t = age / lifeAttr.getX(i);
      sizeAttr.setX(i, (1 - t) * sizeAttr.getX(i));

      // 根據(jù)高度調(diào)整顏色 - 越高的粒子略微變亮
      const height = posAttr.getY(i);
      if (height > 0.5) {
        const baseColor = new THREE.Color(
          colorAttr.getX(i),
          colorAttr.getY(i),
          colorAttr.getZ(i)
        );
        const hsl: { h: number; s: number; l: number } = { h: 0, s: 0, l: 0 };
        baseColor.getHSL(hsl as THREE.HSL);

        const newColor = new THREE.Color().setHSL(
          hsl.h,
          hsl.s,
          Math.min(hsl.l + height * 0.1, 0.6) // 亮度隨高度增加但有上限
        );

        colorAttr.setXYZ(i, newColor.r, newColor.g, newColor.b);
      }
    }
    // 標記屬性已更新
    posAttr.needsUpdate = true;
    colorAttr.needsUpdate = true;
    startAttr.needsUpdate = true;
    sizeAttr.needsUpdate = true;
    velAttr.needsUpdate = true;
  }
  
  /**
   * 更新粒子動畫
   */
  renderEffectAnimation() {
    if (this.effectsMap.size === 0) return;
    const delta = clock.getDelta();
    const elapsed = clock.getElapsedTime();
    /根據(jù)不同類型的粒子特效調(diào)用對應(yīng)的方法
    this.effectsMap.forEach((effect) => {
      switch (effect.userData.effectMethod) {
       // 火焰特效
        case EFFECT_METHOD.CreateFireEffect:
          this.animateFireEffect(effect, delta, elapsed);
          break;
        // 煙霧特效
        case EFFECT_METHOD.CreateSmokeEffect:
          this.animateSmokeEffect(effect, delta, elapsed);
          break;
        // 煙花特效
        case EFFECT_METHOD.CreateFireworkEffect:
          this.animateFireworkEffect(effect, delta, elapsed);
          break;
        default:
          break;
      }
    });
  }
  
   /**
   * 場景動畫循環(huán)
   */
  sceneAnimation(): void {
    if (!this.controls || !this.renderer || !this.scene || !this.camera) return;
    // 確保動畫循環(huán)持續(xù)進行
    this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation());
    // 渲染粒子特效
    this.renderEffectAnimation()
  }

創(chuàng)建兩個顏色大小不同的火焰粒子

創(chuàng)建煙霧粒子特效方法

煙霧粒子方法的實現(xiàn)方式和火焰粒子類似,需要有兩個函數(shù)方法

  1. 創(chuàng)建煙霧粒子方法 createSmokeEffect
  2. 煙霧粒子動畫方法 animateSmokeEffect
/**
 * 創(chuàng)建煙霧特效
 * @param params 特效參數(shù)
 */
 
function createSmokeEffect(params: EffectParams) {
 const { meshPosition, name, color='#888888',size=1, height=1, range=1, praticleCount=2000} = params;

  // 加載煙霧紋理
  const smokeTexture = textureLoader.load(smokeImage);

  // 粒子系統(tǒng)參數(shù)
  const PARTICLE_COUNT = particleCount;
  const positions = new Float32Array(PARTICLE_COUNT * 3);
  const velocities = new Float32Array(PARTICLE_COUNT * 3);
  const lifetimes = new Float32Array(PARTICLE_COUNT);
  const startTimes = new Float32Array(PARTICLE_COUNT);
  const sizes = new Float32Array(PARTICLE_COUNT);
  const colors = new Float32Array(PARTICLE_COUNT * 3);

  const maxLifetime = 8.0; // 煙霧粒子最大存活時間(秒)

  // 初始化粒子
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    // 在小范圍內(nèi)隨機分布,形成煙霧源
    const emitRadius = Math.random() * 0.8; // 增大初始發(fā)射半徑,從0.5到0.8
    const emitAngle = Math.random() * Math.PI * 2;

    positions[3 * i] = Math.cos(emitAngle) * emitRadius;
    positions[3 * i + 1] = -0.2 + Math.random() * 0.2; // 略低于地面
    positions[3 * i + 2] = Math.sin(emitAngle) * emitRadius;

    // 速度:主要向上,帶有輕微的水平漂移
    const upwardSpeed = (0.05 + Math.random() * 0.1) * height; // 上升速度
    const horizontalSpeed = (0.04 + Math.random() * 0.03) * range; // 增大水平擴散速度

    velocities[3 * i] = (Math.random() - 0.5) * horizontalSpeed;
    velocities[3 * i + 1] = upwardSpeed;
    velocities[3 * i + 2] = (Math.random() - 0.5) * horizontalSpeed;

    // 生命周期,錯開時間讓煙霧連續(xù)
    lifetimes[i] = maxLifetime * (0.7 + Math.random() * 0.3);

    // 錯開起始時間,確保煙霧從一開始就存在
    const currentTime = clock.getElapsedTime();
    startTimes[i] = currentTime - Math.random() * maxLifetime;

    // 初始粒子較小,應(yīng)用size參數(shù)
    sizes[i] = (1.0 + Math.random() * 1.0) * size;

    // 煙霧顏色 - 默認灰色
    // 處理顏色參數(shù),支持十六進制字符串
    let colorValue: { r: number; g: number; b: number };
    if (typeof color === 'string') {
      const c = new THREE.Color(color);
      colorValue = { r: c.r, g: c.g, b: c.b };
    } else {
      colorValue = color as { r: number; g: number; b: number };
    }

    // 設(shè)置初始灰度
    const shade = 0.8 + Math.random() * 0.2;
    colors[3 * i] = colorValue.r * shade;
    colors[3 * i + 1] = colorValue.g * shade;
    colors[3 * i + 2] = colorValue.b * shade;
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
  geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
  geometry.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
  geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

  const material = new THREE.PointsMaterial({
    size: 4.0 * size, // 增大基礎(chǔ)大小,從3.0到4.0
    map: smokeTexture,
    vertexColors: true,
    transparent: true,
    depthWrite: false,
    opacity: 0.6, // 稍微降低不透明度,從0.7到0.6
    blending: THREE.NormalBlending,
  });
  // 釋放紋理資源
  smokeTexture.dispose();

  const particles = new THREE.Points(geometry, material);
  particles.name = name;
  particles.userData.effectMethod = 'CreateSmokeEffect'
  particles.position.copy(meshPosition);
  // 添加到場景
  this?.scene?.add(particles)
  // 存儲到特效集合中,便于后續(xù)更新
  this.effectsMap.set(particles.uuid, particles);
}

  /**
   * 更新煙霧粒子動畫
   * @param effect 特效
   * @param delta 時間差
   * @param elapsed 經(jīng)過的時間
   */
  animateSmokeEffect(effect: THREE.Points, delta: number, elapsed: number) {
    const geometry = effect.geometry;
    const posAttr = geometry.getAttribute('position');
    const velAttr = geometry.getAttribute('velocity');
    const startAttr = geometry.getAttribute('startTime');
    const lifeAttr = geometry.getAttribute('lifetime');
    const sizeAttr = geometry.getAttribute('size');
    const colorAttr = geometry.getAttribute('color');

    const count = posAttr.count;
    for (let i = 0; i < count; i++) {
      let age = elapsed - startAttr.getX(i);
      const lifetime = lifeAttr.getX(i);

      // 如果粒子已超出生命周期,則重置它
      if (age > lifetime) {
        startAttr.setX(i, elapsed);
        age = 0;
        // 重置位置到底部發(fā)射區(qū)
        const emitRadius = Math.random() * 0.8; // 增大初始發(fā)射半徑,從0.5到0.8
        const emitAngle = Math.random() * Math.PI * 2;

        posAttr.setXYZ(
          i,
          Math.cos(emitAngle) * emitRadius,
          -0.2 + Math.random() * 0.2,
          Math.sin(emitAngle) * emitRadius
        );

        // 重置速度 - 煙霧上升較慢,但水平擴散更大
        const upwardSpeed = 0.05 + Math.random() * 0.1;
        const horizontalSpeed = 0.04 + Math.random() * 0.03; // 增大水平初始速度,從0.02+0.02到0.04+0.03

        velAttr.setXYZ(
          i,
          (Math.random() - 0.5) * horizontalSpeed,
          upwardSpeed,
          (Math.random() - 0.5) * horizontalSpeed
        );

        // 重置大小
        sizeAttr.setX(i, 1.0 + Math.random() * 1.0);

        // 重置顏色 - 煙霧為灰色
        const shade = 0.8 + Math.random() * 0.2;
        colorAttr.setXYZ(i, shade, shade, shade);
      }

      // 更新位置:p += v * dt + 微弱擺動
      const swayX = Math.sin(elapsed * 0.3 + i * 0.05) * 0.004; // 增大擺動幅度,從0.002到0.004
      const swayZ = Math.cos(elapsed * 0.2 + i * 0.07) * 0.004; // 增大擺動幅度,從0.002到0.004

      posAttr.setXYZ(
        i,
        posAttr.getX(i) + velAttr.getX(i) * delta + swayX,
        posAttr.getY(i) + velAttr.getY(i) * delta,
        posAttr.getZ(i) + velAttr.getZ(i) * delta + swayZ
      );

      // 獲取當前高度用于計算
      const currentHeight = posAttr.getY(i);
      const t = age / lifetime; // 生命周期進度

      // 隨高度調(diào)整速度 - 越高水平擴散越大
      if (currentHeight > 0.5) {
        const heightFactor = Math.min(1, currentHeight / 3.0);
        const horizontalSpread = 0.02 * heightFactor; // 增大擴散系數(shù),從0.01到0.02

        // 向上速度隨高度降低,水平擴散增加
        velAttr.setXYZ(
          i,
          velAttr.getX(i) + (Math.random() - 0.5) * horizontalSpread * delta,
          velAttr.getY(i) * (1 - 0.02 * delta), // 減緩上升速度減少率,從0.03到0.02,讓煙霧上升更高
          velAttr.getZ(i) + (Math.random() - 0.5) * horizontalSpread * delta
        );
      }

      // 隨高度增大粒子尺寸 - 模擬擴散
      const growthFactor = 1 + (0.15 + currentHeight * 0.15) * delta; // 增大生長因子,從0.1到0.15
      sizeAttr.setX(i, sizeAttr.getX(i) * growthFactor);

      // 隨高度和年齡調(diào)整顏色 - 煙霧越高越淡
      if (currentHeight > 0.3) {
        const heightRatio = Math.min(1, currentHeight / 3.0);
        // 隨著高度,顏色稍微變暗
        const newShade = Math.max(0.7, 0.8 - heightRatio * 0.1);
        colorAttr.setXYZ(i, newShade, newShade, newShade);
      }
    }

    // 標記屬性已更新
    posAttr.needsUpdate = true;
    colorAttr.needsUpdate = true;
    startAttr.needsUpdate = true;
    sizeAttr.needsUpdate = true;
    velAttr.needsUpdate = true;
  }

創(chuàng)建幾個不同顏色的煙霧粒子

創(chuàng)建煙花粒子特效方法

和上面兩個方法一樣,煙花粒子也需要兩個函數(shù)方法

1.createFireworkEffect

2.animateFireworkEffect

/**
 * 創(chuàng)建煙花特效
 * @param params 特效參數(shù)
 */
function createFireworkEffect(params: EffectParams) {
  const { meshPosition, name,size=1, height=1, range=1, praticleCount=0} = params;
  // 大幅增加粒子數(shù)量
  const PARTICLE_COUNT = particleCount > 0 ? particleCount : 1200 + Math.floor(Math.random() * 500);
  const positions = new Float32Array(PARTICLE_COUNT * 3);
  const velocities = new Float32Array(PARTICLE_COUNT * 3);
  const lifetimes = new Float32Array(PARTICLE_COUNT);
  const startTimes = new Float32Array(PARTICLE_COUNT);
  const sizes = new Float32Array(PARTICLE_COUNT);
  const colors = new Float32Array(PARTICLE_COUNT * 3);
  const originalColors = new Float32Array(PARTICLE_COUNT * 3); // 新增:原始顏色

  // 保持當前的生命周期
  const maxLifetime = 3.0 + Math.random() * 1.5; // 3-4.5秒
  const currentTime = clock.getElapsedTime();

  // 初始化粒子
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    // 初始都放在發(fā)射點
    positions[i * 3] = 0;
    positions[i * 3 + 1] = 0;
    positions[i * 3 + 2] = 0;

    // 顯著減小速度范圍,創(chuàng)造更緊湊的爆炸效果
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.random() * Math.PI;
    const speed = (2 + Math.random() * 3) * range; // 速度范圍縮小到2-5
    
    velocities[i * 3] = Math.sin(phi) * Math.cos(theta) * speed;
    velocities[i * 3 + 1] = Math.cos(phi) * speed * height;
    velocities[i * 3 + 2] = Math.sin(phi) * Math.sin(theta) * speed;

    // 生命周期
    lifetimes[i] = maxLifetime;
    startTimes[i] = currentTime;

    // 減小粒子大小
    sizes[i] = 2 * size; // 從3降至2

    // 使用HSL色彩空間
    let r, g, b;
    if (!color) {
      const threeColor = new THREE.Color().setHSL(Math.random(), 1.0, 0.5);
      r = threeColor.r;
      g = threeColor.g;
      b = threeColor.b;
    } else {
      // 使用指定顏色或顏色范圍
      let colorValue: { r: number; g: number; b: number };
      if (typeof color === 'string') {
        const c = new THREE.Color(color);
        colorValue = { r: c.r, g: c.g, b: c.b };
      } else {
        colorValue = color as { r: number; g: number; b: number };
      }
      r = colorValue.r;
      g = colorValue.g;
      b = colorValue.b;
    }
    
    // 設(shè)置當前顏色和原始顏色
    colors[i * 3] = r;
    colors[i * 3 + 1] = g;
    colors[i * 3 + 2] = b;
    
    originalColors[i * 3] = r;
    originalColors[i * 3 + 1] = g;
    originalColors[i * 3 + 2] = b;
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
  geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
  geometry.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
  geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geometry.setAttribute('originalColor', new THREE.BufferAttribute(originalColors, 3)); // 添加原始顏色屬性

  const fireworksTexture = textureLoader.load(fireworksImage);
  // 使用與示例相同的材質(zhì)參數(shù),但調(diào)整粒子大小
  const material = new THREE.PointsMaterial({
    map: fireworksTexture,
    size: 2 * size, // 從3降至2
    vertexColors: true,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
    alphaTest: 0.01
  });
  fireworksTexture.dispose();

  const particles = new THREE.Points(geometry, material);

  particles.name = name;
  particles.userData.effectMethod = 'CreateFireworkEffect'
  // 設(shè)置特效位置
  particles.position.copy(meshPosition);
  // 添加到場景
  this?.scene?.add(particles);
  // 存儲到特效集合中,便于后續(xù)更新
  this.effectsMap.set(particles.uuid, particles);
}

 /**
   * 更新煙花粒子動畫
   * @param effect 特效
   * @param delta 時間差
   * @param elapsed 經(jīng)過的時間
   */
  animateFireworkEffect(effect: THREE.Points, delta: number, elapsed: number) {
    const geometry = effect.geometry;
    const posAttr = geometry.getAttribute('position');
    const velAttr = geometry.getAttribute('velocity');
    const startAttr = geometry.getAttribute('startTime');
    const lifeAttr = geometry.getAttribute('lifetime');
    const colorAttr = geometry.getAttribute('color');
    // 新增:原始顏色屬性,如果不存在則創(chuàng)建
    let originalColorAttr = geometry.getAttribute('originalColor');
    if (!originalColorAttr) {
      // 如果原始顏色屬性不存在,創(chuàng)建并復(fù)制當前顏色
      const originalColors = new Float32Array(colorAttr.array.length);
      for (let i = 0; i < originalColors.length; i++) {
        originalColors[i] = colorAttr.array[i];
      }
      originalColorAttr = new THREE.BufferAttribute(originalColors, 3);
      geometry.setAttribute('originalColor', originalColorAttr);
    }

    // 保持當前的重力和速度
    const GRAVITY = -4.0;
    const count = posAttr.count;

    for (let i = 0; i < count; i++) {
      let age = elapsed - startAttr.getX(i);
      const lifetime = lifeAttr.getX(i);

      // 如果粒子已超出生命周期,重新設(shè)置
      if (age > lifetime) {
        // 重置為中心位置
        posAttr.setXYZ(i, 0, 0, 0);

        // 進一步減小爆炸范圍,更緊湊的效果
        const theta = Math.random() * Math.PI * 2;
        const phi = Math.random() * Math.PI;
        // 更小的速度范圍,爆炸范圍更緊湊
        const speed = 2 + Math.random() * 3;
        
        velAttr.setXYZ(
          i,
          Math.sin(phi) * Math.cos(theta) * speed,
          Math.cos(phi) * speed,
          Math.sin(phi) * Math.sin(theta) * speed
        );

        // 重置生命周期和開始時間
        startAttr.setX(i, elapsed);
        age = 0;

        // 重置顏色為隨機HSL
        const color = new THREE.Color().setHSL(Math.random(), 1.0, 0.5);
        colorAttr.setXYZ(i, color.r, color.g, color.b);
        // 同時更新原始顏色
        originalColorAttr.setXYZ(i, color.r, color.g, color.b);
      }

      // 應(yīng)用重力
      velAttr.setY(i, velAttr.getY(i) + GRAVITY * delta);

      // 保持當前的速度更新因子
      const speedFactor = 0.6;
      posAttr.setXYZ(
        i,
        posAttr.getX(i) + velAttr.getX(i) * delta * speedFactor,
        posAttr.getY(i) + velAttr.getY(i) * delta * speedFactor,
        posAttr.getZ(i) + velAttr.getZ(i) * delta * speedFactor
      );

      // 顏色周期變化 - 在生命周期內(nèi)周期性地返回到原始顏色
      const lifeRatio = age / lifetime;
      const colorCycle = Math.sin(lifeRatio * Math.PI * 3) * 0.5 + 0.5; // 0到1之間的正弦波,頻率為3次

      // 獲取當前顏色
      const currentColor = new THREE.Color(
        colorAttr.getX(i), 
        colorAttr.getY(i), 
        colorAttr.getZ(i)
      );

      // 獲取原始顏色
      const originalColor = new THREE.Color(
        originalColorAttr.getX(i),
        originalColorAttr.getY(i),
        originalColorAttr.getZ(i)
      );

      // 混合當前顏色和原始顏色
      const mixedColor = currentColor.clone().lerp(originalColor, colorCycle);
      
      colorAttr.setXYZ(i, mixedColor.r, mixedColor.g, mixedColor.b);
    }

    // 基于生命周期調(diào)整不透明度
    if (effect.material instanceof THREE.PointsMaterial) {
      const maxLife = Math.max(...Array.from({length: count}, (_, i) => lifeAttr.getX(i)));
      const currentMaxAge = Math.max(...Array.from({length: count}, (_, i) => elapsed - startAttr.getX(i)));
      effect.material.opacity = Math.max(0, 1 - (currentMaxAge / maxLife) * 0.8);
    }

    // 標記屬性已更新
    posAttr.needsUpdate = true;
    velAttr.needsUpdate = true;
    colorAttr.needsUpdate = true;
    originalColorAttr.needsUpdate = true;
  }

創(chuàng)建多個煙花粒子效果

結(jié)語

ok,以上就是作者個人基于 Three.js 實現(xiàn)的三種粒子特效的方法,如果你有更好的實現(xiàn)方式歡迎留言溝通

相關(guān)文章

最新評論

获嘉县| 双牌县| 灌云县| 兴国县| 温州市| 宁津县| 满城县| 子洲县| 阿克苏市| 沾化县| 东乌珠穆沁旗| 共和县| 武宣县| 安泽县| 河北省| 东乡| 高清| 申扎县| 枣阳市| 柳河县| 寿宁县| 礼泉县| 沧州市| 鄢陵县| 攀枝花市| 辽源市| 沙洋县| 台中县| 沂南县| 治多县| 博野县| 绥芬河市| 淅川县| 大足县| 泸西县| 庆阳市| 靖远县| 尉氏县| 安丘市| 长春市| 分宜县|