接上回繼續,今天繼續搗騰動畫,上一節咱們讓汽車的輪子動了起來,回顧一下核心代碼:
//輪子轉動 const wheelAnimation = (scene, wheels) => { //定義一個動畫,每秒30幀,繞y軸轉動 const animWheel = new BABYLON.Animation("wheelAnimation", "rotation.y", 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); //動畫關鍵幀 const wheelKeys = []; //起始幀 wheelKeys.push({ frame: 0, value: 0 }); //截止幀(即:第30幀,轉到360度) wheelKeys.push({ frame: 30, value: 2 * Math.PI }); //設置關鍵幀 animWheel.setKeys(wheelKeys); let result = []; for (let i = 0; i < wheels.length; i++) { //將wheel與動畫關聯 wheels[i].animations = []; wheels[i].animations.push(animWheel); //開始動畫,最後的true表示循環播放 result.push(scene.beginAnimation(wheels[i], 0, 30, true)); } return result; }
可能有朋友要問了,這裏返回的result有啥用?先賣個關子,後面再講。
模仿這段代碼,略做修改,我們就能讓汽車在屏幕上動起來:
//car運動 const carAnimation = (scene, car) => { const animCar = new BABYLON.Animation("carAnimation", "position.x", 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); //動畫關鍵幀 const carKeys = []; //起始幀 carKeys.push({ frame: 0, value: -1 }); carKeys.push({ frame: 30, value: -0.9 }); //截止幀(即:第120幀,走到x=8) carKeys.push({ frame: 120, value: 0.9 }); carKeys.push({ frame: 150, value: 1 }); //設置關鍵幀 animCar.setKeys(carKeys); car.animations = []; car.animations.push(animCar); //開始動畫 return scene.beginAnimation(car, 0, 150, true); }
一、如何控制動畫暫停/播放?
接下來說說scene.beginAnimation 返回值有啥用?根據官方文檔描述,這個返回對象Animatable提供了幾個有用的方法:
- pause()
- restart()
- stop()
- reset()
利用這些方法,接下來加點鼠標互動:
- ALT+鼠標單擊,暫停/繼續播放 汽車運動;
- SHIFT+鼠標單擊,暫停/繼續播放 車輪轉動
//聲明2個變量,方便控制動畫 let wheelAnimatables, carAnimatable; const createScene = () => { const scene = new BABYLON.Scene(engine); ... wheelAnimatables = wheelAnimation(scene, wheels); carAnimatable = carAnimation(scene, car); ... return scene; } ... window.addEventListener("click", (e) => { if (e.altKey) { if (carAnimatable._paused) { carAnimatable.restart(); } else { carAnimatable.pause(); } } if (e.shiftKey) { let first = wheelAnimatables[0]; if (first._paused) { wheelAnimatables.forEach(element => { element.restart(); }); } else { wheelAnimatables.forEach(element => { element.pause(); }); } } });
在線地址:https://yjmyzz.github.io/babylon_js_study/day06/01.html
爲了方便後面複用這個小汽車,我們把它在playground上導出爲babylon文件(參考前文提到的做法),核心代碼如下:
const createScene = () => { const scene = new BABYLON.Scene(engine); const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2.5, 2, new BABYLON.Vector3(0, 0, 0)); camera.attachControl(canvas, true); const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0)); const car = buildCar(); const wheels = buildWheels(car); car.rotation.x = -Math.PI / 2; wheelAnimation(scene, wheels); return scene; } const buildCar = () => { //base const outline = [ new BABYLON.Vector3(-0.3, 0, -0.1), new BABYLON.Vector3(0.2, 0, -0.1), ] //curved front for (let i = 0; i < 20; i++) { outline.push(new BABYLON.Vector3(0.2 * Math.cos(i * Math.PI / 40), 0, 0.2 * Math.sin(i * Math.PI / 40) - 0.1)); } //top outline.push(new BABYLON.Vector3(0, 0, 0.1)); outline.push(new BABYLON.Vector3(-0.3, 0, 0.1)); //car face UVs const faceUV = []; faceUV[0] = new BABYLON.Vector4(0, 0.5, 0.38, 1); faceUV[1] = new BABYLON.Vector4(0, 0, 1, 0.5); faceUV[2] = new BABYLON.Vector4(0.38, 1, 0, 0.5); //car material const carMat = new BABYLON.StandardMaterial("carMat"); carMat.diffuseTexture = new BABYLON.Texture("https://yjmyzz.github.io/babylon_js_study/assets/img/car.png"); //back formed automatically const car = BABYLON.MeshBuilder.ExtrudePolygon("car", { shape: outline, depth: 0.2, faceUV: faceUV, wrap: true }); car.material = carMat; return car; } const buildWheels = (car) => { //wheel face UVs const wheelUV = []; wheelUV[0] = new BABYLON.Vector4(0, 0, 1, 1); wheelUV[1] = new BABYLON.Vector4(0, 0.5, 0, 0.5); wheelUV[2] = new BABYLON.Vector4(0, 0, 1, 1); //car material const wheelMat = new BABYLON.StandardMaterial("wheelMat"); wheelMat.diffuseTexture = new BABYLON.Texture("https://yjmyzz.github.io/babylon_js_study/assets/img/wheel.png"); const wheelRB = BABYLON.MeshBuilder.CreateCylinder("wheelRB", { diameter: 0.125, height: 0.05, faceUV: wheelUV }) wheelRB.material = wheelMat; wheelRB.parent = car; wheelRB.position.z = -0.1; wheelRB.position.x = -0.2; wheelRB.position.y = 0.035; const wheelRF = wheelRB.clone("wheelRF"); wheelRF.position.x = 0.1; const wheelLB = wheelRB.clone("wheelLB"); wheelLB.position.y = -0.2 - 0.035; const wheelLF = wheelRF.clone("wheelLF"); wheelLF.position.y = -0.2 - 0.035; const wheels = []; wheels.push(wheelRB); wheels.push(wheelRF); wheels.push(wheelLB); wheels.push(wheelLF); return wheels; } //輪子轉動 const wheelAnimation = (scene, wheels) => { //定義一個動畫,每秒30幀,繞y軸轉動 const animWheel = new BABYLON.Animation("wheelAnimation", "rotation.y", 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); //動畫關鍵幀 const wheelKeys = []; //起始幀 wheelKeys.push({ frame: 0, value: 0 }); //截止幀(即:第30幀,轉到360度) wheelKeys.push({ frame: 30, value: 2 * Math.PI }); //設置關鍵幀 animWheel.setKeys(wheelKeys); for (let i = 0; i < wheels.length; i++) { //將wheel與動畫關聯 wheels[i].animations = []; wheels[i].animations.push(animWheel); //開始動畫,最後的true表示循環播放 scene.beginAnimation(wheels[i], 0, 30, true); } }
結合先前畫的房屋,可以讓小汽車在房屋之間動起來:
在線地址:https://yjmyzz.github.io/babylon_js_study/day06/02.html (加載的資源比較多,網頁首次打開可能比較慢)
二、如何實現複雜的運動軌跡?
2.1 理解movePOV
babylon.js提供了movePOV(rightSpeed, upSpeed, forwardSpeed)方法,可以讓對象 朝右(x軸負方向)、朝上(z軸正方向)、朝前(z軸負方向) 運動。
下面分別演示一下,基本代碼如下:
const createScene = () => { const scene = new BABYLON.Scene(engine); const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2.5, 15, new BABYLON.Vector3(0, 0, 0)); camera.attachControl(canvas, true); const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0)); BABYLON.SceneLoader.ImportMeshAsync("him", "../assets/glb/", "dude.babylon").then((result) => { var dude = result.meshes[0]; dude.scaling = new BABYLON.Vector3(0.02, 0.02, 0.02); scene.beginAnimation(result.skeletons[0], 0, 100, true, 1.0); //向前運動 const rightSpeed = 0, upSpeed = 0, forwardSpeed = 0.01; let distance = 0; //控制下一幀的行爲 scene.onBeforeRenderObservable.add(() => { //讓人物動起來 dude.movePOV(rightSpeed, upSpeed, forwardSpeed); distance += 0.002; if (distance > 1) { //走的距離>1地,復位 distance = 0; dude.position = BABYLON.Vector3.Zero(); } }); }); showAxis(4, scene); return scene; }
向前(z軸負方向):
注:此處的向前,是從人物自身的視角來看的,從觀察者(即電腦前的我們)角度來看,即爲向我們走來。
向右(x軸負方向):
//向右運動 const rightSpeed = 0.01, upSpeed = 0, forwardSpeed = 0.00;
注:由於人物動畫走路朝向的設計原因,僅設置rightSpeed有值,會讓人橫向移動(類似螃蟹),有點奇怪(馬上會講到rotate方法,可以解決這個問題)
向上(Y軸正方向):
//向上運動 const rightSpeed = 0, upSpeed = 0.01, forwardSpeed = 0.00;
2.2 理解rotate
前進過程中如何轉向呢?回想下開汽車時,我們用方向盤來轉向,babylon.js中自然也有類似方法,即:rotate方法
BABYLON.SceneLoader.ImportMeshAsync("him", "../assets/glb/", "dude.babylon").then((result) => { var dude = result.meshes[0]; dude.scaling = new BABYLON.Vector3(0.02, 0.02, 0.02); scene.beginAnimation(result.skeletons[0], 0, 100, true, 1.0); //向前運動 const rightSpeed = 0, upSpeed = 0, forwardSpeed = 0.01; let distance = 0, turnFlag = false; //控制下一幀的行爲 scene.onBeforeRenderObservable.add(() => { //讓人物動起來 dude.movePOV(rightSpeed, upSpeed, forwardSpeed); distance += 0.002; //走到0.5距離時,右轉90度 if (!turnFlag && Math.abs(distance - 0.5) <= 0.000001) { dude.rotate(BABYLON.Axis.Y, Math.PI / 2, BABYLON.Space.LOCAL); turnFlag = true; } if (distance > 1) { //走的距離>1地,復位 distance = 0; dude.position = BABYLON.Vector3.Zero(); dude.rotation = BABYLON.Vector3.Zero(); turnFlag = false; } }); });
在線地址 :https://yjmyzz.github.io/babylon_js_study/day06/03.html
3 生成複雜運動軌跡
結合movePOV 以及rotate,就能實現相對較爲複雜的運動軌跡
const createScene = () => { const scene = new BABYLON.Scene(engine); const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2.5, 15, new BABYLON.Vector3(0, 0, 0)); camera.attachControl(canvas, true); const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0)); //畫1個正方形(輔助觀察人物運動路徑) const squarePoints = [new BABYLON.Vector3(-2, 0, 2), new BABYLON.Vector3(2, 0, 2), new BABYLON.Vector3(2, 0, -2), new BABYLON.Vector3(-2, 0, -2), new BABYLON.Vector3(-2, 0, 2)]; const square = BABYLON.MeshBuilder.CreateLines("square", { points: squarePoints }); //走距離dist後,轉方向trun(角度) const trunPoint = function (turn, dist) { this.turn = turn; this.dist = dist; } //運動軌跡(關鍵轉向點) //注:因本例中的人物原型太大,加載後縮小處理了,所以這裏的距離也要回應縮小(即:*0.2) const track = [ new trunPoint(Math.PI / 2, 4 * 0.2), new trunPoint(Math.PI / 2, 8 * 0.2), new trunPoint(Math.PI / 2, 12 * 0.2), new trunPoint(Math.PI / 2, 16 * 0.2) ] BABYLON.SceneLoader.ImportMeshAsync("him", "../assets/glb/", "dude.babylon").then((result) => { var dude = result.meshes[0]; dude.scaling = new BABYLON.Vector3(0.02, 0.02, 0.02); dude.position = new BABYLON.Vector3(2, 0, 2); scene.beginAnimation(result.skeletons[0], 0, 100, true, 1.0); //向前運動 const rightSpeed = 0.00, upSpeed = 0.00, forwardSpeed = 0.01; //distance記錄走過的距離,p爲track中轉向點的下標(即:第幾個轉向點) let distance = 0, p = 0; //控制下一幀的行爲 scene.onBeforeRenderObservable.add(() => { //讓人物動起來 dude.movePOV(rightSpeed, upSpeed, forwardSpeed); distance += 0.002; //判斷關鍵轉向點 console.log(distance); if (distance > track[p].dist) { dude.rotate(BABYLON.Axis.Y, track[p].turn, BABYLON.Space.LOCAL); p += 1; p %= track.length; if (p === 0) { //走到最開始的位置時,復位 distance = 0; dude.position = new BABYLON.Vector3(2, 0, 2); dude.rotation = BABYLON.Vector3.Zero(); } } }); }); showAxis(4, scene); return scene; }
在線地址:https://yjmyzz.github.io/babylon_js_study/day06/04.html
4、碰撞檢測
每個mesh對象在babylon.js中實際佔據着一塊矩形立體空間
當2個對象碰撞到時,只要這2個立體空間有重疊即爲發生了碰撞,對應的方法爲 intersectsMesh,下面給1個簡單的演示:
BABYLON.SceneLoader.ImportMeshAsync("", "../assets/glb/", "car.babylon").then((result) => { var car1 = scene.getMeshByName("car"); car1.position = new BABYLON.Vector3(-6, 0, 0); car1.scaling = new BABYLON.Vector3(4, 4, 4); const wheelRB = scene.getMeshByName("wheelRB"); const wheelRF = scene.getMeshByName("wheelRF"); const wheelLB = scene.getMeshByName("wheelLB"); const wheelLF = scene.getMeshByName("wheelLF"); let wheels = [wheelRB, wheelRF, wheelLB, wheelLF]; let wheelAnimatables = []; for (let i = 0; i < wheels.length; i++) { wheelAnimatables.push(scene.beginAnimation(wheels[i], 0, 30, true)); } const animCar = new BABYLON.Animation("carAnimation", "position.x", 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); const carKeys = []; carKeys.push({ frame: 0, value: -6 }); carKeys.push({ frame: 120, value: 6 }); animCar.setKeys(carKeys); car1.animations = []; car1.animations.push(animCar); let carAnimable = scene.beginAnimation(car1, 0, 120, true); //複製出第2輛車,擋在路上 var car2 = car1.clone("car2"); car2.position = new BABYLON.Vector3(2, 0, 0.4); car2.rotation.y = Math.PI; car2.scaling = car1.scaling; //注:第2輛車剛複製出來的瞬間,位置與第1輛車就重合了,會導致一出場就碰了,因此要藉助ready把首次碰排除掉 let ready = false; //控制下一幀的行爲 scene.onBeforeRenderObservable.add(() => { //發生碰撞 if (car1.intersectsMesh(car2)) { if (!ready) { ready = true; return; } if (ready){ //停止動畫 carAnimable.stop(); //模擬第1輛車,被撞飛 car1.rotate(BABYLON.Axis.Y, Math.PI*0.4, BABYLON.Space.LOCAL); car1.position.x -= 0.2; car1.position.y += 0.5; return; } } }); });
在線地址 : https://yjmyzz.github.io/babylon_js_study/day06/05.html
參考文檔:
https://doc.babylonjs.com/features/introductionToFeatures/chap3/walkpath
https://doc.babylonjs.com/features/introductionToFeatures/chap4/mesh_intersect