Three.js實(shí)現(xiàn)簡單開門動畫

Three.js實(shí)現(xiàn)簡單開門動畫

先來看一下簡單的開門動畫實(shí)現(xiàn)效果寓涨,如下圖所示:


可以看得出這個開門動畫還是比較簡單的盯串,主要關(guān)鍵點(diǎn)有2個地方:

  1. 實(shí)現(xiàn)門圍繞右門框旋轉(zhuǎn)。
  2. 利用關(guān)鍵幀實(shí)現(xiàn)動畫戒良。

1.實(shí)現(xiàn)門圍繞右門框旋轉(zhuǎn)

Three.js中物體的默認(rèn)旋轉(zhuǎn)中心為物體自身的中心体捏,例如有一扇門(實(shí)際上是一個寬為10,高為20糯崎,深度為0.5的立方體)几缭,如果讓它繞Y軸旋轉(zhuǎn)90度,也就是設(shè)置rotation.y屬性為Math.PI/2沃呢,結(jié)果如下圖所示:


很顯然年栓,大多數(shù)門不應(yīng)該這樣旋轉(zhuǎn)。那么我們應(yīng)該怎么做才能讓門繞著指定軸旋轉(zhuǎn)呢樟插?這時候我們可以在這個對象外面包一層Object3D對象韵洋,讓外層物體的旋轉(zhuǎn)中心位置在原本想要旋轉(zhuǎn)的指定位置上,再調(diào)整門在外層物體中的相對位置黄锤,使門的右邊緣在外層物體的中心位置上搪缨。

// 創(chuàng)建門
const door = new THREE.BoxBufferGeometry(10, 20, 0.5);
const doorMaterial = new THREE.MeshLambertMaterial({ color: 0xd88c00 });
const doorMesh = new THREE.Mesh(door, doorMaterial);
// 實(shí)現(xiàn)門圍繞特定軸旋轉(zhuǎn)
const group = new THREE.Group(); // 外層對象
group.position.set(5, 10, 0); // 設(shè)置外層對象的中心為原本想要旋轉(zhuǎn)的位置
group.add(doorMesh); // 把'門'添加進(jìn)外層對象中
doorMesh.position.set(-5, 0, 0); // 調(diào)整門在外層對象中的相對位置

Group對象和Object3D對象幾乎是相同的,其目的是使得組中對象在語法上的結(jié)構(gòu)更加清晰鸵熟。

2.利用關(guān)鍵幀實(shí)現(xiàn)動畫

先給用于關(guān)鍵幀動畫的網(wǎng)格模型命名副编。

group.name = 'door'; // 外層網(wǎng)格模型命名為door

動畫將作用于外層對象group上,因?yàn)?code>group包含doorMesh對象流强,所以group旋轉(zhuǎn)時doorMesh也會旋轉(zhuǎn)痹届。

接著需要編輯關(guān)鍵幀,需要用到關(guān)鍵幀軌道(KeyframeTrack) API打月。

const times = [0, 3]; // 關(guān)鍵幀時間數(shù)組队腐,單位'秒'
const rotationValues = [0, -Math.PI / 2]; // 需要轉(zhuǎn)動的角度
 // 創(chuàng)建關(guān)鍵幀軌道
const rotationTrack = new THREE.KeyframeTrack(
    'door.rotation[y]', // 指定對象中的變形目標(biāo)為Y軸旋轉(zhuǎn)屬性
    times, // 關(guān)鍵幀的時間數(shù)組
    rotationValues // 與時間數(shù)組中的時間點(diǎn)相關(guān)的值組成的數(shù)組
);

然后使用AnimationClip API剪輯動畫。

const duration = 3; // 持續(xù)時間奏篙,單位'秒'
// 動畫剪輯
const clip = new THREE.AnimationClip(
    'open', // 此剪輯的名稱
    duration, // 如果傳入負(fù)數(shù)柴淘,持續(xù)時間將會從傳入的數(shù)組中計(jì)算得到
    [rotationTrack] // 一個由關(guān)鍵幀軌道(KeyframeTracks)組成的數(shù)組迫淹。
);

duration參數(shù)決定了動畫的播放時間,偏小則編輯的幀動畫將不能完全播放为严,偏大則幀動畫播放完后會繼續(xù)空播放敛熬,所以一般設(shè)置為關(guān)鍵幀時間數(shù)組的最大值。
第三個參數(shù)為數(shù)組第股,可以剪輯多個關(guān)鍵幀軌道应民,意思是可以同時進(jìn)行多個不同的動畫,比如在物體旋轉(zhuǎn)的同時可以改變材質(zhì)的顏色或者改變幾何體的大小等夕吻。

最后就是播放編輯好的關(guān)鍵幀數(shù)據(jù)诲锹,需要用到動畫混合器AnimationMixer API。

const mixer = new THREE.AnimationMixer(group); // 動畫混合器
const AnimationAction = mixer.clipAction(clip); // 返回所傳入的剪輯參數(shù)的AnimationAction
AnimationAction.timeScale = 1; // 可以調(diào)節(jié)播放速度梭冠,默認(rèn)是1辕狰。為0時動畫暫停。值為負(fù)數(shù)時動畫會反向執(zhí)行控漠。
AnimationAction.play(); // 開始播放

AnimationMixer需要傳入的參數(shù)為混合器播放的動畫所屬的對象,在這里也就是外層對象group悬钳。
clipAction方法返回所傳入的剪輯參數(shù)的 AnimationAction盐捷,第一個參數(shù)可以是動畫剪輯(AnimationClip)對象或者動畫剪輯的名稱,所以這里也可以傳入剪輯的名稱“open”默勾。

設(shè)置完以上所有內(nèi)容后碉渡,你會發(fā)現(xiàn)動畫還是沒有變化,那是因?yàn)檫€有一個很重要的步驟沒有做母剥。我們需要在渲染函數(shù)中render()調(diào)用動畫混合器mixerupdate方法滞诺,update方法需要傳入兩幀渲染的間隔時間,需要用到Clock對象的getDelta方法环疼。

const clock = new THREE.Clock(); // 創(chuàng)建時鐘

在render方法中添加:

mixer.update(clock.getDelta()); // 更新動畫

現(xiàn)在我們的動畫就已經(jīng)動了起來习霹。


以下是完整代碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js實(shí)現(xiàn)簡單開門動畫</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <script type="module">
      import * as THREE from 'https://threejs.org/build/three.module.js';
      import Stats from 'https://threejs.org/examples/jsm/libs/stats.module.js';
      import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

      const stats = new Stats(); // 性能監(jiān)控器,用來查看Three.js渲染幀率

      // 創(chuàng)建div
      const container = document.createElement('div');
      document.body.appendChild(container);

      // 創(chuàng)建場景
      const scene = new THREE.Scene();

      // 創(chuàng)建時鐘
      const clock = new THREE.Clock();

      // 創(chuàng)建相機(jī)
      const camera = new THREE.PerspectiveCamera( // 透視投影相機(jī)
        40, // 視場炫隶,表示能夠看到的角度范圍
        window.innerWidth / window.innerHeight, // 渲染窗口的長寬比淋叶,設(shè)置為瀏覽器窗口的長寬比
        0.1, // 從距離相機(jī)多遠(yuǎn)的位置開始渲染
        2000 // 距離相機(jī)多遠(yuǎn)的位置截止渲染
      );
      camera.position.set(-20, 60, 30); // 設(shè)置相機(jī)位置

      // 創(chuàng)建渲染器
      const renderer = new THREE.WebGLRenderer({
        antialias: true, // 是否執(zhí)行抗鋸齒
      });
      renderer.setPixelRatio(window.devicePixelRatio); // 設(shè)置設(shè)備像素比率。通常用于HiDPI設(shè)備伪阶,以防止輸出畫布模糊煞檩。
      renderer.setSize(window.innerWidth, window.innerHeight); // 設(shè)置渲染器大小
      renderer.shadowMap.enabled = true;
      container.appendChild(renderer.domElement);

      // 創(chuàng)建控制器
      const controls = new OrbitControls(camera, renderer.domElement);

      // 創(chuàng)建平面
      const planeGeometry = new THREE.PlaneGeometry(300, 300); // 生成平面幾何
      const planeMaterial = new THREE.MeshLambertMaterial({
        // 生成材質(zhì)
        color: 0xcccccc,
      });
      const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial); // 生成平面網(wǎng)格
      planeMesh.rotation.x = -Math.PI / 2; //繞X軸旋轉(zhuǎn)90度
      scene.add(planeMesh); // 添加到場景中

      // 創(chuàng)建平行光源
      const light = new THREE.DirectionalLight(0xffffff, 1); // 平行光,顏色為白色栅贴,強(qiáng)度為1
      light.position.set(-40, 40, 20); // 設(shè)置燈源位置
      scene.add(light); // 添加到場景中

      // 創(chuàng)建門框
      const doorFrameSide = new THREE.BoxBufferGeometry(1, 20, 2); // 側(cè)邊門框
      const doorFrameTop = new THREE.BoxBufferGeometry(12, 1, 2); // 上門框
      const doorFrameMaterial = new THREE.MeshLambertMaterial({
        color: 0xad4800,
      });
      const doorFrameLeftMesh = new THREE.Mesh(
        doorFrameSide,
        doorFrameMaterial
      );
      const doorFrameTopMesh = new THREE.Mesh(doorFrameTop, doorFrameMaterial);
      const doorFrameRightMesh = doorFrameLeftMesh.clone();
      doorFrameLeftMesh.position.set(-5.5, 10, 0);
      doorFrameRightMesh.position.set(5.5, 10, 0);
      doorFrameTopMesh.position.set(0, 20.5, 0);
      scene.add(doorFrameLeftMesh);
      scene.add(doorFrameRightMesh);
      scene.add(doorFrameTopMesh);

      // 創(chuàng)建門
      const door = new THREE.BoxBufferGeometry(10, 20, 0.5);
      const doorMaterial = new THREE.MeshLambertMaterial({ color: 0xd88c00 });
      const doorMesh = new THREE.Mesh(door, doorMaterial);

      // 實(shí)現(xiàn)門圍繞特定軸旋轉(zhuǎn)
      const group = new THREE.Group();
      group.position.set(5, 10, 0); // 設(shè)置外層對象的中心為原本想要旋轉(zhuǎn)的位置
      group.add(doorMesh);
      group.name = 'door';
      doorMesh.position.set(-5, 0, 0);
      scene.add(group);

      const times = [0, 3]; // 關(guān)鍵幀時間數(shù)組
      const rotationValues = [0, -Math.PI / 2];
      const rotationTrack = new THREE.KeyframeTrack(
        'door.rotation[y]',
        times,
        rotationValues
      ); // 關(guān)鍵幀軌道
      const duration = 3; // 持續(xù)時間 (單位秒)
      const clip = new THREE.AnimationClip('open', duration, [rotationTrack]); // 動畫剪輯

      // 播放編輯好的關(guān)鍵幀數(shù)據(jù)
      const mixer = new THREE.AnimationMixer(group); // 動畫混合器
      const AnimationAction = mixer.clipAction(clip); // 返回所傳入的剪輯參數(shù)的AnimationAction
      AnimationAction.timeScale = 1; // 可以調(diào)節(jié)播放速度斟湃,默認(rèn)是1。為0時動畫暫停檐薯。值為負(fù)數(shù)時動畫會反向執(zhí)行凝赛。
      AnimationAction.play(); // 開始播放

      render();

      function render() {
        requestAnimationFrame(render);
        stats.begin();
        renderer.render(scene, camera);
        mixer.update(clock.getDelta());
        stats.end();
      }
    </script>
  </body>
</html>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子哄酝,更是在濱河造成了極大的恐慌友存,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陶衅,死亡現(xiàn)場離奇詭異屡立,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)搀军,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門膨俐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罩句,你說我怎么就攤上這事焚刺。” “怎么了门烂?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵乳愉,是天一觀的道長。 經(jīng)常有香客問我屯远,道長蔓姚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任慨丐,我火速辦了婚禮坡脐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘房揭。我一直安慰自己备闲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布捅暴。 她就那樣靜靜地躺著恬砂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伶唯。 梳的紋絲不亂的頭發(fā)上觉既,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音乳幸,去河邊找鬼瞪讼。 笑死,一個胖子當(dāng)著我的面吹牛粹断,可吹牛的內(nèi)容都是我干的符欠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼瓶埋,長吁一口氣:“原來是場噩夢啊……” “哼希柿!你這毒婦竟也來了诊沪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤曾撤,失蹤者是張志新(化名)和其女友劉穎端姚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挤悉,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡渐裸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了装悲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昏鹃。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诀诊,靈堂內(nèi)的尸體忽然破棺而出洞渤,到底是詐尸還是另有隱情,我是刑警寧澤属瓣,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布载迄,位于F島的核電站,受9級特大地震影響奠涌,放射性物質(zhì)發(fā)生泄漏宪巨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一溜畅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧极祸,春花似錦慈格、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至稿械,卻和暖如春选泻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背美莫。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工页眯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厢呵。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓窝撵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親襟铭。 傳聞我的和親對象是個殘疾皇子碌奉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容