Three.js 的 3D 粒子动画:群星送福

7618c7935327f53f563e83a7d85ddc81.gif

”粒子动画“ 这个词大家可能经常听到,那什么是粒子动画呢?

粒子是指原子、分子等组成物体的最小单位。在 2D 中,这种最小单位是像素,在 3D 中,最小单位是顶点。

粒子动画不是指物体本身的动画,而是指这些基本单位的动画。因为是组成物体的单位的动画,所以会有打碎重组的效果。

本文我们就来学习下 3D 的粒子动画,做一个群星送福的效果:


思路分析

3D 世界中,物体是由顶点构成,3 个顶点构成一个三角形,然后给三角形贴上不同的纹理,这样就是一个三维模型。

ee82437554cf2353d734f0363e839a74.png

也就是说,3D 模型是由顶点确定的几何体(Geometry),贴上不同的纹理(Material)所构成的物体(Mesh 等)。

之后,把 3D 物体添加到场景(Scene)中,设置一个相机(Camera)角度去观察,然后用渲染器(Renderer)一帧帧渲染出来,这就是 3D 渲染流程。

3D 物体是由顶点构成,那让这些顶点动起来就是粒子动画了,因为基本粒子动了,自然就会有打碎重组的效果。

在“群星送福”效果中,我们由群星打碎重组成了福字,实际上就是群星的顶点运动到了福字的顶点,由一个 3D 物体变成了另一个 3D 物体。

那么群星的顶点从哪里来的?福字的顶点又怎么来呢?

群星的顶点其实是随机生成的不同位置的点,在这些点上贴上星星的贴图,就是群星效果。

福字的顶点是加载的一个 3D 模型,解析出它的顶点数据拿到的。

有了两个 3D 物体的顶点数据,也就是有了动画的开始结束坐标,那么不断的修改每个顶点的 x、y、z 属性就可以实现粒子动画。

这里的 x、y、z 属性值的变化不要自己算,用一些动画库来算,它们支持加速、减速等时间函数。Three.js 的动画库是 Tween.js。

总之,3D 粒子动画就是顶点的 x、y、z 属性的变化,会用动画库来计算中间的属性值。由一个物体的顶点位置、运动到另一个物体的顶点位置,会有种打碎重组的效果,这也是粒子动画的魅力。

思路理清了,那我们来具体写下代码吧。

代码实现

如前面所说,3D 的渲染需要一个场景(Scene)来管理所有的 3D 物体,需要一个相机(Camera)在不同角度观察,还需要渲染器(Renderer)一帧帧渲染出来。

这部分是基础代码,先把这部分写好:

创建场景:

const scene = new THREE.Scene();

创建相机:

const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);

相机分为透视相机和平行相机,我们这里用的透视相机,也就是近大远小的透视效果。要指定可以看到的视野角度(45)、宽高比(width/height)、远近范围(0.1 到 1000)这 3 种参数。

调整下相机的位置和观察方向:

camera.position.set(100, 0, 400);
camera.lookAt(scene.position);

然后是渲染器:

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);

渲染器要通过 requestAnimationFrame 来一帧帧的渲染:

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

准备工作完成,接下来就是绘制星空、福字这两种 3D 物体,还有实现粒子动画了。

绘制星空

星空不是正方体、圆柱体这种规则的几何体,而是由一些随机的顶点构成的,这种任意的几何体使用缓冲几何体 BufferGeometry 创建。

为啥这种由任意顶点构成的几何体叫缓冲几何体呢?

因为顶点在被 GPU 渲染之前是放在缓冲区 buffer 中的,所以这种指定一堆顶点的几何体就被叫做 BufferGeometry。

我们创建 30000 个随机顶点:

const vertices = [];
for ( let i = 0; i < 30000; i ++ ) {
    const x = THREE.MathUtils.randFloatSpread( 2000 );
    const y = THREE.MathUtils.randFloatSpread( 2000 );
    const z = THREE.MathUtils.randFloatSpread( 2000 );
    vertices.push( x, y, z );
}

这里用了 Three.js 提供的工具 MathUtils 来生成 0 到 2000 的随机值。

然后用这些顶点创建 BufferGeometry:

const geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute(vertices, 3));

给 BufferGeometry 对象设置顶点位置,指定 3 个数值(x、y、z)为一个坐标。

然后创建这些顶点上的材质(Material),也就是星星的贴图:

4a907671581dc26a568a6168e6f33b23.png
const star = new THREE.TextureLoader().load('img/star.png');
const material = new THREE.PointsMaterial( { size: 10, map: star });

顶点有了,材质有了,就可以创建 3D 物体了(这里的 3D 物体是 Points)。

const points = new THREE.Points( geometry, material );
scene.add(points);

看下渲染的效果:

5d0f558601f0fae42c5886415bef86f8.png

静态的没 3D 的感觉,我们让每一帧转一下,改下 render 逻辑:

function render() {
    renderer.render(scene, camera);
    scene.rotation.y += 0.001;

    requestAnimationFrame(render);
}

再来看一下:

3D 星空的感觉有了!

接下来我们来做粒子动画:

3D 粒子动画

3D 粒子动画就是顶点的动画,也就是 x、y、z 的变化。

我们先来实现个最简单的效果,让群星都运动到 0,0,0 的位置:

起始点坐标就是群星的的本来的位置,通过 getAttribute('position') 来取。动画过程使用 tween.js 来计算:

const startPositions = geometry.getAttribute('position');

for(let i = 0; i< startPositions.count; i++) {
    const tween = new TWEEN.Tween(positions);

    tween.to({
        [i * 3]: 0,
        [i * 3 + 1]: 0,
        [i * 3 + 2]: 0
    }, 3000 * Math.random());

    tween.easing(TWEEN.Easing.Exponential.In);
    tween.delay(3000);
    tween.onUpdate(() => {
        startPositions.needsUpdate = true;
    });
    
    tween.start();
}

每个点都有 x、y、z 坐标,也就是下标为 i3、i3+1、i*3+2 的值,我们指定从群星的起始位置运动到 0,0,0 的位置。

然后指定了时间函数为加速(Easing.Exponential.In),3000 ms 后开始执行动画。

每一帧渲染的时候要调用下 Tween.update 来计算最新的值:

function render() {
    TWEEN.update();
    renderer.render(scene, camera);
    scene.rotation.y += 0.001;

    requestAnimationFrame(render);
}

每一帧在绘制的时候都会调用 onUpdate 的回调函数,我们在回调函数里把 positions 的 needsUpdate 设置为 true,就是告诉 tween.js 在这一帧要更新为新的数值再渲染了。

第一个粒子动画完成!

来看下效果(我把这个效果叫做万象天引):

所有的星星粒子都集中到了一个点,这就是粒子动画典型的打碎重组感。

接下来,只要把粒子运动到福字的顶点就是我们要做的“群星送福”效果了。

福字模型的顶点肯定不能随机,自己画也不现实,这种一般都是在建模软件里画好,然后导入到 Three.js 来渲染,

我找了这样一个福字的 3D 模型:

cc820f91014eda49b8ffcb7d885c6d5a.png

模型是 fbx 格式的,使用 FBXLoader 加载:

const loader = new THREE.FBXLoader();
loader.load('./obj/fu.fbx', function (object) {
    const destPosition = object.children[0].geometry.getAttribute('position');

});

回调参数就是从 fbx 模型加载的 3D 物体,它是一个 Group(多个 3D 物体的集合),取出第 0 个元素的 geometry 属性,就是对应的几何体。

这样,我们就拿到了目标的顶点位置。

把粒子动画的结束位置改为福字的顶点就可以了:

const cur = i % destPosition.count;
tween.to({
    [i * 3]: destPosition.array[cur * 3],
    [i * 3 + 1]: destPosition.array[(cur * 3 + 1)],
    [i * 3 + 2]: destPosition.array[(cur * 3 + 2)]
}, 3000 * Math.random());

如果开始顶点位置比较多,超过的部分从 0 的位置再来,所以要取余。

大功告成!

这就是我们想要的粒子效果:


完整代码上传到了 github:https://github.com/QuarkGluonPlasma/threejs-exercize

也在这里贴一份:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
    <style>
        body {
            margin: 0;
        }
    </style>
    <script src="./js/three.js"></script>
    <script src="./js/tween.js"></script>
    <script src="./js/FontLoader.js"></script>
    <script src="./js/TextGeometry.js"></script>
    <script src="./js/FBXLoader.js"></script>
    <script src="./js/fflate.js"></script>
</head>
<body>
    <script>
        const width = window.innerWidth;
        const height = window.innerHeight;
        const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);

        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();

        camera.position.set(100, 0, 400);
        camera.lookAt(scene.position);

        renderer.setSize(width, height);
        document.body.appendChild(renderer.domElement)

        function create() {
            const vertices = [];
            for ( let i = 0; i < 30000; i ++ ) {
                const x = THREE.MathUtils.randFloatSpread( 2000 );
                const y = THREE.MathUtils.randFloatSpread( 2000 );
                const z = THREE.MathUtils.randFloatSpread( 2000 );
                vertices.push( x, y, z );
            }
            const geometry = new THREE.BufferGeometry();
            geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );

            const star = new THREE.TextureLoader().load('img/star.png');
            const material = new THREE.PointsMaterial( { size: 10, map: star });

            const points = new THREE.Points( geometry, material );
            points.translateY(-100);
            scene.add(points);

            const loader = new THREE.FBXLoader();
            loader.load('./obj/fu.fbx', function (object) {
                const startPositions = geometry.getAttribute('position');
                const destPosition = object.children[0].geometry.getAttribute('position')
                for(let i = 0; i< startPositions.count; i++) {
                    const tween = new TWEEN.Tween(startPositions.array);
                    const cur = i % destPosition.count;
                    tween.to({
                        [i * 3]: destPosition.array[cur * 3],
                        [i * 3 + 1]: destPosition.array[cur * 3 + 1],
                        [i * 3 + 2]: destPosition.array[cur * 3 + 2]
                    }, 3000 * Math.random());
                    tween.easing(TWEEN.Easing.Exponential.In);
                    tween.delay(3000);

                    tween.start();

                    tween.onUpdate(() => {
                        startPositions.needsUpdate = true;
                    });
                }
            } );
        }

        function render() {
            TWEEN.update();
            renderer.render(scene, camera);
            scene.rotation.y += 0.001;

            requestAnimationFrame(render);
        }
        create();
        render();
    </script>
</body>
</html>

总结

粒子动画是组成物体的基本单位的运动,对 3D 来说就是顶点的运动。

我们要实现“群星送福”的粒子动画,也就是从群星的顶点运动到福字的顶点。

群星的顶点可以随机生成,使用 BufferGeometry 创建对应的几何体。福字则是加载创建好的 3D 模型,拿到其中的顶点位置。

有了开始、结束位置,就可以实现粒子动画了,过程中的 x、y、z 值使用动画库 Tween.js 来计算,可以指定加速、减速等时间函数。

粒子动画有种打碎重组的感觉,可以用来做一些很炫的效果。理解了什么是粒子动画、动的是什么,就算是初步掌握了。

我摘下漫天繁星,给大家送一份福气,新的一年一起加油!

RECOMMEND

推荐阅读

a4ee2e314501a46b25b65b1484876e05.png

01

35616aedc5acf31126044ca5ff8d1bc5.png

Three.js开发指南:Three.js开发指南 第3版

推荐语:本书先从基本概念和Three.js的基本模块讲起,然后伴随着大量的示例和代码,逐步扩展到更多的主题,循序渐进地讲解Three.js的各种功能,帮助你充分利用WebGL和现代浏览器的潜能,直接在浏览器中创建动态的华丽场景

10340d43cb62cd9af4c8e5b1a6ce00cf.png

02

60ac928a8a50d0a02cd2432ed24615eb.png

JavaScript权威指南(原书第7版)

推荐语:前端三大件中JavaScript是重中之重。JavaScript是Web前端之本,也是进阶全栈的基石。它的知识点非常繁杂,是前端开发知识体系中比较难的区域。对JavaScript理解的深度决定了未来的发展前景。这就很需要一本大而全的书来巩固JS知识,帮我们更好的深入理解JS。全球畅销25年的JS犀牛书已经更新到第7版!涵盖了ES2020特性,同时删去了已过时的内容。没入手的同学还是要抓紧入手一本,放在案头经常来翻阅。

ffe27f9166a73fabbadb64f7d7cff3f0.gif

更多精彩回顾

书讯 | 2月书讯(下)| 新年到,新书到!

书讯 | 2月书讯 (上)| 新年到,新书到!

资讯 | 省政协委员、南京大学人工智能学院院长周志华:科研学习探索最重要的是“兴趣”和“勤奋”

书单 | 6本书,读懂2022年最火的边缘计算

干货 | 如何用数字化构建企业的“韧性”?

收藏 | 前端应用和产品逻辑的核心:交互流

赠书 | 【第89期】推荐几本电商必读书

2a52c3aff92961883cb9815c1066a630.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值