ThreeJS模型粒子打散特效

2024-06-14 11:59 月霖 219


一、场景搭建

搭建基础场景加载一个飞机模型:

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'

// 创建场景
const scene = new THREE.Scene()

// 创建相机
const camera = new THREE.PerspectiveCamera(
  45,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
)
camera.position.z = 80
camera.lookAt(0, 0, 0)

// 创建渲染器
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
// 设置色调映射
renderer.toneMapping = THREE.ACESFilmicToneMapping
// 设置色调映射曝光度
renderer.toneMappingExposure = 0.5;
document.body.appendChild(renderer.domElement)

// 加载环境贴图
const rgbeLoader = new RGBELoader()
rgbeLoader.load('./texture/night.hdr', (envMap) => {
  envMap.mapping = THREE.EquirectangularReflectionMapping
  scene.environment = envMap
  scene.background = envMap
})

// 实例化gltf加载器
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)
// 加载模型
gltfLoader.load(
  './model/fighter.glb',
  (gltf) => {
    scene.add(gltf.scene)
  }
)

// 坐标辅助器
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.1

// 渲染场景
function render() {
  requestAnimationFrame(render)

  controls.update()
  renderer.render(scene, camera)
}

render()

window.addEventListener('resize', () => {
  // 重置渲染器宽高比
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 重置相机宽高比
  camera.aspect = window.innerWidth / window.innerHeight
  // 更新相机投影矩阵
  camera.updateProjectionMatrix()
})

二、将模型转换为粒子效果

我们通过一个递归函数,遍历这个模型中的所有Mesh,并使用点材质重新进行创建:

gltfLoader.load(
  './model/fighter.glb',
  (gltf) => {
    // scene.add(gltf.scene)

    const fighterPointGroup = transformPoints(gltf.scene)
    scene.add(fighterPointGroup)
  }
)

function transformPoints(object3d) {
  const texture = new THREE.TextureLoader().load("./texture/particles/1.png")

  const group = new THREE.Group()
  recursion(object3d, group)

  // 递归生成Mesh
  function recursion(object, parent) {
    // 生成随机色
    const color = new THREE.Color(
      0,
      0,
      Math.random() * 255
    )

    object.children.forEach(child => {
      if (child.isMesh) {
        // 创建点材质
        const material = new THREE.PointsMaterial({
          size: 0.1,
          color: color,
          map: texture,
          transparent: true,
          depthTest: false
        })

        // 创建Mesh
        const pointsMesh = new THREE.Points(child.geometry, material)

        // 复制原物体的位置、旋转、缩放
        pointsMesh.position.copy(child.position)
        pointsMesh.rotation.copy(child.rotation)
        pointsMesh.scale.copy(child.scale)

        parent.add(pointsMesh)

        recursion(child, pointsMesh)
      }
    })
  }

  return group
}

三、着色器编写散开特效

  1. 使用着色器材质

将之前的点材质改为着色器材质:

import vertexShader from './fighter/vertexShader.glsl?raw'
import fragmentShader from './fighter/fragmentShader.glsl?raw'

// ...

function transformPoints(object3d) {
  const texture = new THREE.TextureLoader().load("./texture/particles/1.png")

  const group = new THREE.Group()
  recursion(object3d, group)

  function recursion(object, parent) {
    // 生成随机色
    const color = new THREE.Color(
      0,
      Math.random(),
      Math.random()
    )

    object.children.forEach(child => {
      if (child.isMesh) {
        const material = new THREE.ShaderMaterial({
          uniforms: {
            uColor: { value: color },
            uTexture: { value: texture }
          },
          vertexShader: vertexShader,
          fragmentShader: fragmentShader,
          transparent: true,
          depthTest: false,
          blending: THREE.AdditiveBlending
        })

        // 创建Mesh
        const pointsMesh = new THREE.Points(child.geometry, material)

        // 复制原物体的位置、旋转、缩放
        pointsMesh.position.copy(child.position)
        pointsMesh.rotation.copy(child.rotation)
        pointsMesh.scale.copy(child.scale)

        parent.add(pointsMesh)

        recursion(child, pointsMesh)
      }
    })
  }

  return group
}

// ...

这里引入了顶点着色器 vertexShader.glsl 和片元着色器 fragmentShader.glsl ,并将颜色和贴图作为全局变量传给着色器。

顶点着色器:

void main() {
    vec4 vPosition = viewMatrix * modelMatrix * vec4(position, 1.0);
    gl_Position = projectionMatrix * vPosition;
    gl_PointSize = -10.0 / vPosition.z;
}

片元着色器:

uniform sampler2D uTexture;
uniform vec3 uColor;

void main() {
    vec4 textureColor = texture2D(uTexture, gl_PointCoord);
    gl_FragColor = vec4(uColor, textureColor.x);
}

2.粒子打散效果

接着,我们想要把这些粒子打散,这就需要在着色器中修改这些粒子的位置,我们先生成一些随机坐标作为粒子最终的位置坐标,然后将其传递给顶点着色器:

function pointsBlast(pointGroup) {
  pointGroup.traverse(child => {
    if (child.isPoints) {
      const count = child.geometry.attributes.position.count
      
      // 生成随机坐标
      const randomPositionArr = new Float32Array(count * 3)
      for (let i = 0; i < count; i++) {
        randomPositionArr[i * 3 + 0] = (Math.random() * 2 - 1) * 10
        randomPositionArr[i * 3 + 1] = (Math.random() * 2 - 1) * 10
        randomPositionArr[i * 3 + 2] = (Math.random() * 2 - 1) * 10
      }

      // 将这些坐标设为几何体的属性
      child.geometry.setAttribute(
        'aPosition',
        new THREE.BufferAttribute(randomPositionArr, 3)
      )
    }
      })
}

接着,我们就可以在着色器中修改粒子的位置,此外,我们希望这个过程是有动画效果的,因此我们向着色器传入一个uTime变量来进行控制:

const material = new THREE.ShaderMaterial({
  uniforms: {
    uColor: { value: color },
    uTexture: { value: texture },
    uTime: { value: 0 } // 传入uTime变量,之后可以通过改变这个值实现动画效果
  },
  vertexShader: vertexShader,
  fragmentShader: fragmentShader,
  transparent: true,
  depthTest: false,
  blending: THREE.AdditiveBlending
})

看看顶点着色器的部分,这里将顶点坐标随着时间从当前位置移动到目标位置:

attribute vec3 aPosition;
uniform float uTime;

void main() {
    vec4 curPosition = modelMatrix * vec4(position, 1.0);
    vec3 direction = aPosition - curPosition.xyz;
    vec3 targetPosition = curPosition.xyz + direction * uTime * 0.1;
    vec4 vPosition = viewMatrix * vec4(targetPosition, 1.0);

    gl_Position = projectionMatrix * vPosition;
    gl_PointSize = -10.0 / vPosition.z;
}
现在加上动画效果:

现在加上动画效果:

pointGroup.traverse(child => {
  if (child.isPoints) {
    // 修改uTime
    gsap.to(child.material.uniforms.uTime, {
      value: 10,
      duration: 5,
      yoyo: true,
      repeat: 1
    })
  }
})

3.变成另一种物体

刚才目标点是随机生成的,也可以使用其他模型的顶点,比如让这些粒子组合成一只小鸭子:

gltfLoader.load(
  './model/duck.glb',
  (duck) => {
    const geometry = duck.scene.children[0].children[0].geometry
    geometry.scale(0.05, 0.05, 0.05)
    pointsBlast(fighterPointGroup, duck.scene.children[0].children[0].geometry)
  }
)

function pointsBlast(pointGroup, geometry) {
  console.log(pointGroup, geometry)
  const posArr = geometry.attributes.position.array
  const posCount = geometry.attributes.position.count

  let index = 0
  pointGroup.traverse(child => {
    if (child.isPoints) {
      const count = child.geometry.attributes.position.count
      const randomPositionArr = new Float32Array(count * 3)
      for (let i = 0; i < count; i++) {
        randomPositionArr[i * 3 + 0] = posArr[index]
        randomPositionArr[i * 3 + 1] = posArr[index + 1]
        randomPositionArr[i * 3 + 2] = posArr[index + 2]
        index += 3

        if (index >= posCount * 3) {
          index = 0
        }
      }

      child.geometry.setAttribute(
        'aPosition',
        new THREE.BufferAttribute(randomPositionArr, 3)
      )

      gsap.to(child.material.uniforms.uTime, {
        value: 10,
        duration: 5
      })
    }
  })
}