一、场景搭建
搭建基础场景加载一个飞机模型:
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
}
三、着色器编写散开特效
- 使用着色器材质
将之前的点材质改为着色器材质:
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
})
}
})
}