用 Three.js 实现简单的粒子动画

7,191 阅读3分钟

前言

使用 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 方法,参数为角度。按照同样的方式也添加了 rotateYrotateZ 方法,具体实现可以参照完整的示例代码中的 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); // 蓝色
}

模型材质

这次使用的是纯色的默认材质,并且粒子的颜色是自定义的(绿色或蓝色),所以需要设置 vertexColorsTHREE.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 的动画。
  • 因为要改变粒子的位置和颜色,所有要设置 verticesNeedUpdatecolorsNeedUpdatetrue
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)
}

示例源码

在线示例

项目完整源码

参考