前言
使用 Three.js 自带的 THREE.Points 可以实现一些炫酷的粒子动效,我们来看看这些效果是怎么实现的。
雪花效果
在圣诞或其他节日的时候,我们有时候需要实现一些雪花飘落的效果,而使用 Three.js 可以简单的实现出来。效果大致是这样的:
创建场景、摄像机及渲染器
这些都是使用 Three.js 固定的模板代码,其中使用的透视相机,可以让雪花看起来有层次感。
let width = window.innerWidth; // 画布的宽度
let height = window.innerHeight; // 画布的高度
// 渲染器
let renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
// 设置背景颜色
renderer.setClearColor('rgb(22,33,82)', 1.0);
let element = document.getElementById('snow');
element.appendChild(renderer.domElement);
// 场景
let scene = new THREE.Scene();
// 透视投影摄像机
let camera = new THREE.PerspectiveCamera(75, width / height, 1, 500);
camera.position.set(0, 0, 50);
camera.lookAt(scene.position);
scene.add(camera);
加载雪花图片
这里的雪花用的是一张图片,背景透明的白色圆。这里要注意的是 TextureLoader 是异步加载的,加载完成后,再根据纹理创建 PointsMaterial 。在这里可以使用 size 来调整雪花的大小。
let loader = new THREE.TextureLoader()
loader.load('../assets/snow.png', (texture) => {
let material = new THREE.PointsMaterial({
map: texture, // 纹理
transparent: true, // 透明
size: 5,
});
// 创建 THREE.Points
...
});
随机生成雪花
然后,我们需要随机生成雪花的位置和移动的方向及速度。
- 在这里我们指定的雪花个数800个。
- 位置的随机生成的 x, y, z 都在在 [-200, 200] 之间。而速度的方向默认是y轴向下,大小是 0.4,可以更改这个数值调整雪花飘落的速度,然后按x轴和Y轴做随机的旋转,使其运动的方向尽可能随机,同时保证雪花整体的运动是向下的。
// 雪花出现范围
let range = 400;
// 通过自定义几何体设置粒子位置
let geom = new THREE.Geometry();
for (let i = 0; i < 800; i++) {
// 随机生成雪花的位置
let v = new THREE.Vector3(
Math.random() * range - range / 2,
Math.random() * range - range / 2,
Math.random() * range - range / 2
);
// 随机生成雪花分别沿x、y、z轴方向移动速度
v.velocity = createVelocity();
// 添加顶点
geom.vertices.push(v);
}
points = new THREE.Points(geom, material);
scene.add(points);
// 渲染
renderer.render(scene, camera);
// 创建指定范围内的随机数
function randomRange(t, i) {
return Math.random() * (i - t) + t
}
// 创建运动方向
function createVelocity() {
// 默认向下
let velocity = new THREE.Vector3(0, -0.4, 0);
velocity.rotateX(randomRange(-45, 45));
velocity.rotateY(randomRange(0, 360));
return velocity;
}
在 THREE.Vector3 中添加了 rotateX 方法,参数为角度。按照同样的方式也添加了 rotateY 和 rotateZ 方法,具体实现可以参照完整的示例代码中的 lib/vector-util.js。
var TO_RADIANS = Math.PI / 180;
THREE.Vector3.prototype.rotateX = function (t) {
var cosRY = Math.cos(t * TO_RADIANS);
var sinRY = Math.sin(t * TO_RADIANS);
var i = this.z,
o = this.y;
this.y = o * cosRY + i * sinRY;
this.z = o * -sinRY + i * cosRY
}
让雪花动起来
最后就是让雪花动起来了:
- 粒子的运动是通过不断修改粒子本身的位置实现的。
- 这里使用了
setInterval实现的动画,每秒执行 40 次。 - 每次调用
animate时,会根据本身存储的velocity来进行位置修改。因为我们生成的粒子个数是有限的,所以,在粒子的位置超出指定的范围的话,会改变它的运动方向及y轴位置,这样粒子会一直在指定的范围内运动,雪就会不停的下了。 - 需要注意的是,
verticesNeedUpdate属性的设置,不设置的话,粒子是不会动的。
setInterval(animate, 1000 / 40);
// 动画
function animate() {
let vertices = points.geometry.vertices;
vertices.forEach(function (v, idx) {
// 计算位置
v.y = v.y + (v.velocity.y);
v.x = v.x + (v.velocity.x);
v.z = v.z + (v.velocity.z);
// 边界检查
if (v.y <= -range / 2) v.y = range / 2;
if (v.x <= -range / 2 || v.x >= range / 2) v.x = v.x * -1;
if (v.z <= -range / 2 || v.z >= range / 2) v.velocity.z = v.velocity.z * -1;
});
//重要:渲染时需要更新位置(如果没有设为true,则无法显示动画)
points.geometry.verticesNeedUpdate = true;
renderer.render(scene, camera);
};
上面的动画也可以用 requestAnimationFrame 来实现,不过需要计算一下时间差,然后根据这个时间差计算粒子在各个方向上运动的长度。
示例代码
粒子模型切换
上面的雪花效果,粒子是在随机的位置按随机方向进行运动的。但是,假如我想把形状A的粒子云改变成形状B的粒子云,甚至颜色也需要改变,改变过程还需要动画,这改怎么处理呢?
我们先看下效果:
创建场景、摄像机及渲染器
和雪花效果的代码基本一致,不过使用的是正交相机
...
let camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 1, 1000);
camera.position.set(0, 0, 10);
scene.add(camera);
...
创建两个粒子模型
看看这两个模型中粒子的位置是怎么定义的:
pointsCount都为 10000。- d3.randomNormal 会返回一个生成符合正态分布随机数的函数。为了方便生成测试数据引入的,这个不用深究。
这个只是演示用的数据,你可以生成符合你自己要求的粒子模型。
// 绿色的圆环
const rng = d3.randomNormal(0, 0.01);
for (let i = 0; i < pointsCount; i++) {
let v = new THREE.Vector3(
(rng() + Math.cos(i)) * (width / 2.5),
(rng() + Math.sin(i)) * (height / 2.5),
0
);
let c = new THREE.Color(0, 1, 0); // 绿色
}
// 蓝色的星云
const rng = d3.randomNormal(0, 0.05);
for (let i = 0; i < pointsCount; i++) {
let v = new THREE.Vector3(
rng() * width,
rng() * height,
0
);
let c = new THREE.Color(0, 0.5, 1); // 蓝色
}
模型材质
这次使用的是纯色的默认材质,并且粒子的颜色是自定义的(绿色或蓝色),所以需要设置 vertexColors 为 THREE.VertexColors 。
let material = new THREE.PointsMaterial({
size: 1.0,
vertexColors: THREE.VertexColors, // 按顶点颜色渲染
})
切换动画
动画的实现,原理同样是改变点的位置,我在这里使用了 tween.js去处理点的位置及颜色的变化。
这里定义了点的位置在 800 毫秒内移动到 targetPosition。tween 还可以指定缓动函数、设定延迟等等,可以根据需求做不同的效果。
// 切换为绿色圆环
function greenCircleLayout(geometry) {
const rng = d3.randomNormal(0, 0.01);
geometry.vertices.forEach((d, i) => {
new TWEEN.Tween(d).to({
x: (rng() + Math.cos(i)) * (width / 2.5),
y: (rng() + Math.sin(i)) * (height / 2.5),
}, 800)
// .delay(500 * Math.random())
.start()
});
geometry.colors.forEach((d, i) => {
new TWEEN.Tween(d).to({
g: 1,
b: 0,
}, 800)
.start()
});
}
// 切换为蓝色点云
function blueNormalLayout(geometry) {
// 和切换为绿色圆环代码相似,此处省略
...
}
动起来
定义一个数组记录模型,然后根据运动的时间做模型切换。
TWEEN.update执行 tween.js 的动画。- 因为要改变粒子的位置和颜色,所有要设置
verticesNeedUpdate和colorsNeedUpdate为true。
let layouts = [greenCircleLayout, blueNormalLayout];
let startTime = new Date().getTime();
let currentLayout = 0;
requestAnimationFrame(animate);
function animate() {
let now = new Date().getTime();
if (now - startTime > 1500) {
currentLayout = (currentLayout + 1) % layouts.length;
layouts[currentLayout](pointCloud.geometry);
startTime = now;
}
TWEEN.update();
pointCloud.geometry.verticesNeedUpdate = true;
pointCloud.geometry.colorsNeedUpdate = true;
renderer.render(scene, camera);
requestAnimationFrame(animate)
}