Three.js 動效方案

本文做者 陳舒儀

圖片來源 Pixabay,做者 Arek Sochahtml

背景

Three.js(下面簡稱 Three) 做爲一個 3D 庫,不只減小了咱們學習 OpenGL 和 WebGL 的成本,還大大提高了前端在可視化上給用戶帶來更多的真實、沉浸式的體驗。衆所周知,Three 更多的是用 3D 模型 + 投影相機 + 用戶交互的方式來構建一個「3D 世界」。 前端

這張專輯,用眼睛去「聽」 活動中,在視覺在只能提供「2D 切圖」的狀況下,須要營造「3D 效果」。爲了得到最好視覺體驗,僅僅經過貼圖很難作到,因此藉此機會探索了 Three 的動效方案。git

運動每每是相對的,運動的本質多是「物體動」或「相機動」,本文將從對象動畫相機動畫上闡述對 Three 的動效探索。github

Three 基礎

Camera 相機

Three 提供多種相機,其中應用最廣的就是投影相機 (PerspectiveCamera) ,經過投影相機能夠模擬人眼所看見的效果。算法

const camera = THREE.PerspectiveCamera(fov, aspect, near, far);
參數 含義 默認值
fov fov 是視景體豎直方向上(非水平!)的張角,人類有接近180度的視角大小。該值可根據具體場景所須要的視角設置。 45
aspect             指定渲染結果的橫向尺寸和縱向尺寸的比值。該值一般設置爲窗口大小的寬高比。 window.innerWidth / window.innerHeight
near 表示能夠看到多近的物體。這個值一般很小。 0.1
far 表示能夠看到多遠的物體。這個看狀況設置,過大會致使渲染過多;過小可能又會看不到。 1000

ps: 在 Three 中是沒有「長度單位」這個概念的,它的數值都是根據比例計算得出,所以這裏提到的 0.1 或 1000 都沒有具體的含義,而是一種相對長度。canvas

相機

能夠看到,經過配置透視相機的相關參數,最終被渲染到屏幕上的,是在 nearfar 之間,根據 fov 的值和物體遠近 d 肯定渲染高度,再經過 aspect 值來肯定渲染寬度的。segmentfault

Scene 場景

有了相機,咱們還要有場景,場景是爲了讓咱們設置咱們的空間內「有什麼」和「放在哪」的。咱們能夠在場景中放置物體,光源還有相機。api

const scene = new THREE.Scene();

是的,建立場景就是這麼簡單。數組

Group

爲了以羣的維度去區分場景中的物體,咱們還能夠在場景中添加 Group。有了 Group,能夠更方便地操做一類物體。
好比建立一個 stoneGroup,並添加到場景中:app

const stoneGroup = new THREE.Group();
stoneGroup.name = 'stoneGroup';

scene.add(stoneGroup);

爲 Group 命名,容許咱們經過 name 來獲取到對應的 Group:

const group = scene.getObjectByName(name);

Geometry 幾何體

Three 提供了多種類型的幾何體,能夠分爲二維網格和三維網格。二維網格顧名思義只有兩個維度,能夠經過這種幾何體建立簡單的二維平面;三維網格容許你定義三維物體;在 Three 中定義一個幾何體十分簡單,只須要選擇須要的幾何體並傳入相應參數建立便可。

查看Three提供的幾何體

若是看到 Three 提供的幾何體,能夠看到有的幾何體中它分別提供 GeometeryBufferGeometery 版本,關於這兩個的區別,能夠看這裏 回答

大體意思就是使用 Buffer 版本的幾何體相較於普通的幾何體會將描述物體的數據存放在緩衝區中,減小內存消耗和 CPU 循環。經過它們提供的方法來看,使用 geometry 無疑是對新手友好的。

建立幾何體:

// 建立立方體,傳入長、寬和高
var cubeGeometry = new THREE.CubeGeometry(40, 40, 40);
// 建立球體,傳入半徑、寬片斷數量和高片斷數量
var sphereGeometry = new THREE.SphereGeometry(20, 100, 100);

Material 材質

定義材質能夠幫助咱們決定一個物體在各類環境狀況下的具體表現。一樣 Three 也提供了多種材質。下面列舉幾個經常使用的材質。

名稱 描述
MeshBasicMaterial 基礎材質,用它定義幾何體上的簡單顏色或線框
MeshPhongMaterial 受光照影響,用來建立光亮的物體
MeshLambertMaterial 受光照影響,用來建立不光亮的物體
MeshDepthMaterial 根據相機遠近來決定如何給網格染色

建立材質:

var basicMaterial = new THREE.MeshBasicMaterial({ color: 0x666666 });
var lambertMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 });
var phongMaterial = new THREE.MeshPhongMaterial({ color: 0x666666 });
var wireMaterial = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x666666 });

material

更多材質和相關信息,能夠查看 材質

Mesh網格對象

須要添加到場景中,還須要依賴 Mesh。Mesh 是用來定義材質和幾何體之間是如何粘合的,建立網格對象能夠應用一個或多個材質和幾何體。

建立幾何體相同材質不一樣的網格對象:

var cube = new THREE.Mesh(cubeGeometry, basicMaterial);
var cubePhong = new THREE.Mesh(cubeGeometry, phongMaterial);
scene.add(cube, cubePhong);

建立材質相同幾何體不一樣的網格對象:

var cube = new THREE.Mesh(cubeGeometry, basicMaterial);
var sphere = new THREE.Mesh(sphereGeometry, basicMaterial);
scene.add(cube, sphere);

建立擁有多個材質幾何體的網格對象:

var phongMaterial = new THREE.MeshPhongMaterial({ color: 0x666666 });
var cubeMeshPhong = new THREE.Mesh(cubeGeometry, cubePhongMaterial);
var cubeMeshWire = new THREE.Mesh(cubeGeometry, wireMaterial);
// 網格對象新增材質
cubeMeshPhong.add(cubeMeshWire);
scene.add(cubeMeshPhong);

Renderer 渲染器

有了場景和相機,咱們還須要渲染器把對應的場景用對應的相機可見渲染出來,所以渲染器須要傳入場景和相機參數。

// 抗鋸齒、canvas 是否支持 alpha 透明度、preserveDrawingBuffer 是否保存 BUFFER 直到手動清除
const renderer = new THREE.WebGLRenderer({
    antialias: true, alpha: true, preserveDrawingBuffer: true
});
renderer.setSize(this.width, this.height);
renderer.autoClear = true;
// 清除顏色,第二個參數爲 0 表示徹底透明,適用於須要透出背景的場景
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(window.devicePixelRatio);

爲了在相機更新後所看見的場景,須要在循環渲染中加上

renderer.render(scene, camera);

有了相機場景和渲染器,咱們已經能夠看到初步的效果了。但3D世界裏,靜止的物體多無趣啊。因而咱們嘗試加入動畫效果。

物體動畫

Animations

Three爲動畫提供了一系列方法。

參數 含義
AnimationMixer 做爲特定對象的動畫混合器,能夠管理該對象的全部動畫
AnimationAction             爲播放器指定對應的片斷存儲一系列行爲,用來指定動畫快慢,循環類型等
AnimationClip 表示可重用的動畫行爲片斷,用來指定一個動畫的動畫效果(放大縮小、上下移動等)
KeyframeTrack 與時間相關的幀序列,傳入時間和值,應用在指定對象的屬性上。目前有 BooleanKeyframeTrack VectorKeyframeTrack 等。

那麼如何建立一個動畫呢?下面這個例子給你們解釋如何讓網格對象進行簡單的上下移動。

建立特定對象的動畫混合器:

// 建立紋理
const texture = new THREE.TextureLoader().load(img.src);
// 使用紋理建立貼圖
const material = new THREE.SpriteMaterial({ map: texture, color: 0x666666 });
// 使用貼圖建立貼圖對象
const stone = new THREE.Sprite(material);
// 爲貼圖對象建立動畫混合器
const mixer = new THREE.AnimationMixer(stone);

建立動畫行爲片斷:

const getClip = (pos = [0, 0, 0]) => {
    const [x, y, z] = pos;
    const times = [0, 1]; // 關鍵幀時間數組,離散的時間點序列
    const values = [x, y, z, x, y + 3, z]; // 與時間點對應的值組成的數組
    // 建立位置關鍵幀對象:0時刻對應位置0, 0, 0   10時刻對應位置150, 0, 0
    const posTrack = new THREE.VectorKeyframeTrack('stone.position', times, values);
    const duration = 1;
    return new THREE.AnimationClip('stonePosClip', duration, [posTrack]);
};

建立動畫播放器,肯定動畫的表現:

const action = mixer.clipAction(getClip([x, y, z]));
action.timeScale = 1; // 動畫播放一個週期的時間
action.loop = THREE.LoopPingPong; // 動畫循環類型
action.play(); // 播放

在循環繪製中更新混合器,保證動畫的執行:

animate() {
    // 更新動畫
    const delta = this.clock.getDelta();
    mixer.update(delta);
    
    requestAnimationFrame(() => {
        animate();
    });
}

image

codepen

貼圖動畫

有了 Animation 咱們能夠很簡單地對物體的一些屬性進行操做。但一些貼圖相關的動畫就很難用 Animation 來實現了,好比:

箭頭動圖

上圖這種,沒法經過改變物體的位置、大小等屬性實現。因而,還有一種方案 —— 貼圖動畫。

相似在 CSS3 中對序列圖片使用 transform 屬性改變位置來達到的動畫效果,實際上在 Three 中也可使用貼圖位移的方式實現。

首先,咱們要有一個序列圖:

箭頭序列圖

做爲紋理加載,而且增長到場景中:

const arrowTexture = new THREE.TextureLoader().load(Arrow);
const material = new THREE.SpriteMaterial({ map: arrowTexture, color: 0xffffff });
const arrow = new THREE.Sprite(material);
scene.add(arrow);

聲明 TextAnimator 對象,實現紋理的位移:

function TextureAnimator(texture, tilesHoriz, tilesVert, numTiles, tileDispDuration) {
    // 紋理對象經過引用傳入,以後能夠直接使用update方法更新紋理位置
    this.tilesHorizontal = tilesHoriz;
    this.tilesVertical = tilesVert;
    // 序列圖中的幀數
    this.numberOfTiles = numTiles;
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(1 / this.tilesHorizontal, 1 / this.tilesVertical);

    // 每一幀停留時長
    this.tileDisplayDuration = tileDispDuration;

    // 當前幀停留時長
    this.currentDisplayTime = 0;

    // 當前幀
    this.currentTile = 0;

    // 更新函數,經過這個函數對紋理位移進行更新
    this.update = (milliSec) => {
        this.currentDisplayTime += milliSec;
        while (this.currentDisplayTime > this.tileDisplayDuration) {
            this.currentDisplayTime -= this.tileDisplayDuration;
            this.currentTile++;
            if (this.currentTile === this.numberOfTiles) { this.currentTile = 0; }
            const currentColumn = this.currentTile % this.tilesHorizontal;
            texture.offset.x = currentColumn / this.tilesHorizontal;
            const currentRow = Math.floor(this.currentTile / this.tilesHorizontal);
            texture.offset.y = currentRow / this.tilesVertical;
        }
    };
}
// 傳入一個一行裏有 13 幀的序列圖,每張序列圖停留 75ms
const arrowAni = new TextureAnimator(arrowTexture, 13, 1, 13, 75);

在循環繪製中更新,保證動畫的執行:

arrowAni.update(delta);

做爲引用傳入後,對貼圖的修改會直接體如今使用該貼圖的材質上。

codepen

粒子動畫

Three 中還提供了酷炫的粒子動畫,使用繼承自 Object3D 的 Points 類實現。有了 Points 類咱們能夠很方便地把一個幾何體渲染成一組粒子,並對它們進行控制。

建立粒子

建立粒子咱們首先須要建立粒子的材質,可使用 PointsMaterial 建立粒子材質。

const texture = new THREE.TextureLoader().load('https://p1.music.126.net/jgzbZtWZhDet2jWzED8BTw==/109951164579600342.png');

material = new THREE.PointsMaterial({
  color: 0xffffff,
  // 映射到材質上的貼圖
  map: texture,
  size: 2,
  // 粒子的大小是否和其與攝像機的距離有關,默認值 true
  sizeAttenuation: true,
});

// 開啓透明度測試,透明度低於0.5的片斷會被丟棄,解決貼圖邊緣感問題
material.alphaTest = 0.5;

有了粒子材質後,咱們能夠應用同一個材質批量建立一組粒子,只須要傳入一個簡單的幾何體。

var particles = new THREE.Points( geometry, material );

若是你傳入的是 BoxGeometry 你可能會獲得這樣的一組粒子

cube粒子

還能夠根據傳入的 Shape 獲得這樣一組粒子

fish粒子

粒子運動

但有趣的粒子毫不是靜止的,而是有活動、有過程的。但若是本身動手實現一個粒子的運動又很複雜,所以但願藉助一些第三方庫實現粒子動畫的緩動過程。

tween.js

tween.js 是一個小型的 JS 庫,咱們可使用它爲咱們的動畫聲明變化。使用 tween.js 咱們不須要關心運動的中間狀態,只須要關注粒子的:

  • 起始位置
  • 最終位置
  • 緩動效果
// srcPosition, targetPosition;
tweens.push(new TWEEN.Tween(srcPosition).easing(TWEEN.Easing.Exponential.In));
// tweens最終位置、緩動時間
tweens[0].to(targetPosition, 5000);
tweens[0].start();、

codepen

其實粒子動畫的場景還有不少,咱們能夠用他們創造雪花飄散、穿梭效果,本質都是粒子的位置變化。

相機動畫

相機在 3D 空間中充當人的眼睛,所以天然的相機動線能夠保證交互的天然流暢。

Controls

Three 提供了一系列相機控件來控制場景中的相機軌跡,這些控件適用於大部分場景。使用 Controls 開發者能夠再也不須要去關心用戶交互和相機移動的問題。

活動中也涉及到 OrbitControls 的使用,他提供了環繞物體旋轉、平移和縮放的方法,但因爲對使用二維貼圖的狀況下,旋轉和縮放都容易穿幫,須要被禁止。

// 建立軌跡
const controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
controls.enabled = !0;
controls.target = new THREE.Vector3();
controls.minDistance = 0;
controls.maxDistance = 2000;
controls.minPolarAngle = Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2;
// 禁用縮放
controls.enableZoom = !1;
// 禁用旋轉
controls.enableRotate !1;
controls.panSpeed = 2;

// 修改控件的默認觸摸選項,設置爲單指雙指都爲平移操做
controls.touches = {
    ONE: THREE.TOUCH.PAN,
    TWO: THREE.TOUCH.PAN,
};

this.scene.add(this.camera);

OrbitControl 還容許咱們設置阻尼,設置該值表現爲數值越接近 1 越難拖動,開啓阻尼後須要咱們手動 update 控件。

controls.enableDamping = !0;
controls.dampingFactor = 0.2;

查看源碼能夠看到,阻尼的實現就是依賴滑動時的 offset 乘上一個權重,在經過後續的update不斷爲 panOffset 乘上一個權重實現滑動難,撒手後再滑動一點距離。

// this method is exposed, but perhaps it would be better if we can make it private...
this.update = function () {

    // ...

    return function update() {

        // ...

        // 平移

        if ( scope.enableDamping === true ) {
            // 開啓阻尼後會在本來的位移上乘上一個權重
            scope.target.addScaledVector( panOffset, scope.dampingFactor );

        } else {

            scope.target.add( panOffset );

        }

        // ...

        if ( scope.enableDamping === true ) {

            sphericalDelta.theta *= ( 1 - scope.dampingFactor );
            sphericalDelta.phi *= ( 1 - scope.dampingFactor );

            // 若是沒有人爲操做,隨着時間推移,panOffset會愈來愈小
            panOffset.multiplyScalar( 1 - scope.dampingFactor );

        } else {

            sphericalDelta.set( 0, 0, 0 );

            panOffset.set( 0, 0, 0 );

        }

        // ...

    };

}();

官方也提供了 Controls 的 例子 供你們參考。

相機動線

若是不使用 Controls,僅僅是相機從一個點移動到另外一個點,爲了更平滑天然的相機軌跡,推薦使用貝塞爾曲線。

貝塞爾曲線是一個由起點、終點和控制點決定的一條時間相關的變化曲線。這裏以二階貝塞爾曲線爲例,實現相機的曲線移動。(三維的點有點難說明白,這裏用二維座標來解釋)

二階貝塞爾曲線

上圖中小黑點的移動軌跡能夠看作相機移動的曲線。

貝塞爾公式

從該公式來看,只須要肯定 p0、p1 和 p2 三個點,在單位時間下咱們能夠得到一條肯定的曲線。

可是,換成座標點要怎麼作呢?

// 得到貝塞爾曲線
function getBezier(p1, p2) {
    // 在指定範圍內隨機生成一個控制點
    const cp = {
        x: p1.x + Math.random() * 100 + 200,
        z: p2.z + Math.random() * 200,
    };

    let t = 0;
    // 貝塞爾曲線公式,根據時間肯定點的位置
    return (deltat) => {
        if (t >= 1) return [p2.x, p2.y];
        t += deltat;
        if (t > 1) t = 1;

        const { x: x1, z: z1 } = p1;
        const { x: cx, z: cz } = cp;
        const { x: x2, z: z2 } = p2;
        const x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2;
        const z = (1 - t) * (1 - t) * z1
            + 2 * t * (1 - t) * cz + t * t * z2;

        return [x, z];
    };
}
const bezier = getBezier(p1, p2);

爲了從簡,這裏只實現了二維座標的軌跡變化,但三維也是同理。

由於貝塞爾曲線是時間相關曲線,在每一次循環渲染中要傳入時間來更新相機位置。

animation() {
    const [x, z] = bezier(clock.getDelta());
    camera.position.x = x;
    camera.position.z = z;
    
    requestAnimationFrame(() => {
            animate();
    });
}

小結

沒遇上 Three 的熱潮,只能趁着活動需求給本身補補課了。在三維空間中,動畫可以讓空間中的物體更加生動,而相機的移動帶給用戶更強的空間感。

本文介紹了基於 Animation 實現物體的簡單運動、 Texture 實現貼圖動畫以及使用 Points 粒子化的物體動畫方案;基於 Controls 和貝塞爾曲線的相機動畫方案。

對 Three 有興趣的朋友,能夠經過 官方文檔 來學習,裏面提供的例子覆蓋了大部分場景。

以上是我在活動中涉及到的一些動畫方案,不免會出現理解誤差和表達錯誤,若是有更多的動效方案歡迎一塊兒探討~

參考資料

本文發佈自 網易雲音樂前端團隊,文章未經受權禁止任何形式的轉載。咱們一直在招人,若是你剛好準備換工做,又剛好喜歡雲音樂,那就 加入咱們
相關文章
相關標籤/搜索