Three.js 的 3D 粒子動畫:群星送福
”粒子動畫“ 這個詞大家可能經(jīng)常聽到,那什么是粒子動畫呢?
粒子是指原子、分子等組成物體的最小單位。在 2D 中,這種最小單位是像素,在 3D 中,最小單位是頂點。
粒子動畫不是指物體本身的動畫,而是指這些基本單位的動畫。因為是組成物體的單位的動畫,所以會有打碎重組的效果。
本文我們就來學(xué)習(xí)下 3D 的粒子動畫,做一個群星送福的效果:

思路分析
3D 世界中,物體是由頂點構(gòu)成,3 個頂點構(gòu)成一個三角形,然后給三角形貼上不同的紋理,這樣就是一個三維模型。
也就是說,3D 模型是由頂點確定的幾何體(Geometry),貼上不同的紋理(Material)所構(gòu)成的物體(Mesh 等)。
之后,把 3D 物體添加到場景(Scene)中,設(shè)置一個相機(Camera)角度去觀察,然后用渲染器(Renderer)一幀幀渲染出來,這就是 3D 渲染流程。
3D 物體是由頂點構(gòu)成,那讓這些頂點動起來就是粒子動畫了,因為基本粒子動了,自然就會有打碎重組的效果。
在“群星送福”效果中,我們由群星打碎重組成了福字,實際上就是群星的頂點運動到了福字的頂點,由一個 3D 物體變成了另一個 3D 物體。
那么群星的頂點從哪里來的?福字的頂點又怎么來呢?
群星的頂點其實是隨機生成的不同位置的點,在這些點上貼上星星的貼圖,就是群星效果。
福字的頂點是加載的一個 3D 模型,解析出它的頂點數(shù)據(jù)拿到的。
有了兩個 3D 物體的頂點數(shù)據(jù),也就是有了動畫的開始結(jié)束坐標(biāo),那么不斷的修改每個頂點的 x、y、z 屬性就可以實現(xiàn)粒子動畫。
這里的 x、y、z 屬性值的變化不要自己算,用一些動畫庫來算,它們支持加速、減速等時間函數(shù)。Three.js 的動畫庫是 Tween.js。
總之,3D 粒子動畫就是頂點的 x、y、z 屬性的變化,會用動畫庫來計算中間的屬性值。由一個物體的頂點位置、運動到另一個物體的頂點位置,會有種打碎重組的效果,這也是粒子動畫的魅力。
思路理清了,那我們來具體寫下代碼吧。
代碼實現(xiàn)
如前面所說,3D 的渲染需要一個場景(Scene)來管理所有的 3D 物體,需要一個相機(Camera)在不同角度觀察,還需要渲染器(Renderer)一幀幀渲染出來。
這部分是基礎(chǔ)代碼,先把這部分寫好:
創(chuàng)建場景:
- const scene = new THREE.Scene();
創(chuàng)建相機:
- 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 種參數(shù)。
調(diào)整下相機的位置和觀察方向:
- 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();
準(zhǔn)備工作完成,接下來就是繪制星空、福字這兩種 3D 物體,還有實現(xiàn)粒子動畫了。
繪制星空
星空不是正方體、圓柱體這種規(guī)則的幾何體,而是由一些隨機的頂點構(gòu)成的,這種任意的幾何體使用緩沖幾何體 BufferGeometry 創(chuàng)建。
為啥這種由任意頂點構(gòu)成的幾何體叫緩沖幾何體呢?
因為頂點在被 GPU 渲染之前是放在緩沖區(qū) buffer 中的,所以這種指定一堆頂點的幾何體就被叫做 BufferGeometry。
我們創(chuàng)建 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 的隨機值。
然后用這些頂點創(chuàng)建 BufferGeometry:
- const geometry = new THREE.BufferGeometry();
- geometry.setAttribute( 'position', new THREE.Float32BufferAttribute(vertices, 3));
給 BufferGeometry 對象設(shè)置頂點位置,指定 3 個數(shù)值(x、y、z)為一個坐標(biāo)。
然后創(chuàng)建這些頂點上的材質(zhì)(Material),也就是星星的貼圖:
- const star = new THREE.TextureLoader().load('img/star.png');
- const material = new THREE.PointsMaterial( { size: 10, map: star });
頂點有了,材質(zhì)有了,就可以創(chuàng)建 3D 物體了(這里的 3D 物體是 Points)。
- const points = new THREE.Points( geometry, material );
- scene.add(points);
看下渲染的效果:
靜態(tài)的沒 3D 的感覺,我們讓每一幀轉(zhuǎn)一下,改下 render 邏輯:
- function render() {
- renderer.render(scene, camera);
- scene.rotation.y += 0.001;
- requestAnimationFrame(render);
- }
再來看一下:

3D 星空的感覺有了!
接下來我們來做粒子動畫:
3D 粒子動畫
3D 粒子動畫就是頂點的動畫,也就是 x、y、z 的變化。
我們先來實現(xiàn)個最簡單的效果,讓群星都運動到 0,0,0 的位置:
起始點坐標(biāo)就是群星的的本來的位置,通過 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 坐標(biāo),也就是下標(biāo)為 i3、i3+1、i*3+2 的值,我們指定從群星的起始位置運動到 0,0,0 的位置。
然后指定了時間函數(shù)為加速(Easing.Exponential.In),3000 ms 后開始執(zhí)行動畫。
每一幀渲染的時候要調(diào)用下 Tween.update 來計算最新的值:
- function render() {
- TWEEN.update();
- renderer.render(scene, camera);
- scene.rotation.y += 0.001;
- requestAnimationFrame(render);
- }
每一幀在繪制的時候都會調(diào)用 onUpdate 的回調(diào)函數(shù),我們在回調(diào)函數(shù)里把 positions 的 needsUpdate 設(shè)置為 true,就是告訴 tween.js 在這一幀要更新為新的數(shù)值再渲染了。
第一個粒子動畫完成!
來看下效果(我把這個效果叫做萬象天引):
所有的星星粒子都集中到了一個點,這就是粒子動畫典型的打碎重組感。
接下來,只要把粒子運動到福字的頂點就是我們要做的“群星送福”效果了。
福字模型的頂點肯定不能隨機,自己畫也不現(xiàn)實,這種一般都是在建模軟件里畫好,然后導(dǎo)入到 Three.js 來渲染,
我找了這樣一個福字的 3D 模型:
模型是 fbx 格式的,使用 FBXLoader 加載:
- const loader = new THREE.FBXLoader();
- loader.load('./obj/fu.fbx', function (object) {
- const destPosition = object.children[0].geometry.getAttribute('position');
- });
回調(diào)參數(shù)就是從 fbx 模型加載的 3D 物體,它是一個 Group(多個 3D 物體的集合),取出第 0 個元素的 geometry 屬性,就是對應(yīng)的幾何體。
這樣,我們就拿到了目標(biāo)的頂點位置。
把粒子動畫的結(jié)束位置改為福字的頂點就可以了:
- 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
總結(jié)
粒子動畫是組成物體的基本單位的運動,對 3D 來說就是頂點的運動。
我們要實現(xiàn)“群星送福”的粒子動畫,也就是從群星的頂點運動到福字的頂點。
群星的頂點可以隨機生成,使用 BufferGeometry 創(chuàng)建對應(yīng)的幾何體。福字則是加載創(chuàng)建好的 3D 模型,拿到其中的頂點位置。
有了開始、結(jié)束位置,就可以實現(xiàn)粒子動畫了,過程中的 x、y、z 值使用動畫庫 Tween.js 來計算,可以指定加速、減速等時間函數(shù)。
粒子動畫有種打碎重組的感覺,可以用來做一些很炫的效果。理解了什么是粒子動畫、粒子動畫中動的是什么,就算是初步掌握了。
我摘下漫天繁星,給大家送一份福氣,新的一年一起加油!