前言
今天我们将完成三个粒子动效,来结束three.js粒子动画篇章(当然还有别的篇章啦)。其中,我们会学习一些有趣的技巧来提示我们的动效完整程度。
介绍
本次的三个例子分别为example3,example4,example5^1^。
我们的最终目的是为了完成一个对标UP2017腾讯互动娱乐年度发布会 - 腾讯互动娱乐 (qq.com)的最小的完整demo。
效果图
example5(镂空半圆):
可以点击文末脚注查看具体动效。
一一道来
example3
在3中我们完成了一个快速右旋转并后退的圆球体粒子图动效。
这一例中,我们主要需要完成的是将几何体(如果不理解几何体可以翻阅初学者学习THREE.js的一些心得[2] - 掘金 (juejin.cn))自旋,以及我们将介绍一个致命的需优化点。
自旋代码
ts
let distence = 0.1
const tick = () => {
controls.update()
camera.position.z += distence;
distence = distence * 0.99;
sphereGeometry.rotateY(distence)
renderer.render(scene, camera)
f && requestAnimationFrame(tick)
}
抛开上章介绍过的固定代码,我们可以看到,自旋主要是依靠了distence乘固定系数来达到类似二次函数的曲线效果,类似的效果还有Math.sqrt。
这类操作能极其简单的构造一个有规律的函数曲线,来达到让动画效果更加流畅真实的效果。
细心的同学可以看到在我们请求下一个关键帧前有一个f,这就是我们要说的优化点。
ts
let f = true
onBeforeUnmount(() => {
f = false
})
没错,仅是在组件销毁前将f变为false,如果不这样做,那么requestAnimationFrame将持续执行。
想象一下,我们辛苦搬运了shadertoy上一个极其精美的作品作为登录页背景,但用户登录进后依旧gpu拉满,这是十分恐怖的(后续可能会出一篇关于浏览器调优性能的文章,到时候链接放这)。
因此这个简单的优化点就是不论我们在哪里使用了requestAnimationFrame递归,一定要记得在不需要的时候结束它。
example4
例子4中,我们完成了一个空心球体,话不多说,上关键代码:
ts
const generatePointPosition = (i: number, positions: Float32Array) => {
const x = (Math.random() - 0.5) * 6;
const y = (Math.random() - 0.5) * 6;
const z = (Math.random() - 0.5) * 6;
const distance = Math.sqrt(x * x + y * y + z * z);
if (distance <= 2.5 && distance >= 2.49) { //如果距离小于等于半径,则该点在圆内
[positions[i * 3 + 0], positions[i * 3 + 1], positions[i * 3 + 2]] = [x, y, z]; //将该点的坐标存入points数组中
} else {
generatePointPosition(i, positions);
}
}
可以看到还是我们熟悉的生成随机点位函数,但其中不一样的是我们增加了一个判断 distance >= 2.49
,就此完成了空心设计。
由此我们可以总结,不论粒子需要的是什么形状,重点就在于随机点位时如何给予判断公式。
(另有一种方式为直接将点放于几何体端点,该方法也十分简单,感兴趣可以百度尝试)。
至此我们完成了空心球的制作。
example5
该例中,我们将会总结前面所有学到的东西,完成一个圆球内随机点,各自 以不同速度找到位置并组合成镂空半圆的效果。
我们需要解决最关键的问题在于 如何完成对每个点位的控制。
再用前面的套路?
绝对不行了,硬写会导致很多无意义的性能浪费。为了满足要求我们有很多计算工作,例如计算初始位置与结束位置的路径,每次移动后的点。
所以为了让代码可读性更高,动效完成度更高,我们必须引入一个轻量级库TWEEN。
且我们要改变我们的思路,从给几何体一个点位,变成一个几何体就是一个点,以此来达到更方便的控制(当然也可以不用,但理解上可能稍微更难一些)。
首先我们要改造点位生成函数,让它仅给一个点位。
ts
const generatePointPosition = (positions: Float32Array, Tdistance: number, doHalf: boolean = false) => {
const x = (Math.random() - 0.5) * Tdistance * 2;
const y = (Math.random() - 0.5) * Tdistance * 2;
const z = (Math.random() - 0.5) * Tdistance * 2;
const distance = Math.sqrt(x * x + y * y + z * z);
if (distance <= Tdistance && distance >= Tdistance - 0.1) { //如果距离小于等于半径,则该点在圆内
if (doHalf) {
if (y < 0) {
generatePointPosition(positions, Tdistance, doHalf);
return;
}
}
[positions[0], positions[1], positions[2]] = [x, y, z]; //将该点的坐标存入points数组中
} else {
generatePointPosition(positions, Tdistance, doHalf);
}
}
可以看到,其中还有一个dohalf参数,用来生成半圆随机点。
接着我们会来介绍为什么必须用TWEEN来完成该本次动效。
ts
for (let i = 0; i < count; i++) {
generatePointPosition(positions, 3);
const sphereGeometry = new THREE.BufferGeometry()
sphereGeometry.setAttribute('position', new THREE.BufferAttribute(originPositions, 3))
const particles = new THREE.Points(sphereGeometry, pointMaterial)
particles.position.set(positions[0], positions[1], positions[2])
scene.add(particles)
let tween = new TWEEN.Tween(particles.position)
generatePointPosition(positions, 2, true);
tween.to({
x: positions[0],
y: positions[1],
z: positions[2]
}, Math.random() * 10000)
tween.easing(TWEEN.Easing.Quadratic.Out)
tween.start();
}
由于引入TWEEN,我们不用在计算点位,仅告诉tween我们需要这个点从初始位置到目标位置,以及一个随机时间和移动的速度曲线(有没有类似headlessUI思想),就完成了所有工作(着实方便啊)。
注意在关键帧中间调用一个tween的update方法,更新一下数字。
至此我们完成了所有的工作,一个简单的从圆形,到镂空半圆的粒子动画也就完成了,相信大家一定能从这个粒子慢慢摸索出上面提到的腾讯官网的动效了吧。
写在最后
通过本章,我们从一点终于完成了一个完整最小实例的编写,当然如果没有写出效果可以查看本项目的源码Bayn-Web/three (github.com)。
后面的计划
后续我们会再介绍如何实现'现实世界的物体',以及如何将Shadertoy BETA上的shader运用在我们的web项目中。
Footnotes
- 网站来自这里(learn3d-neon.vercel.app)] ↩