babylon.js 學習筆記(6)

上回繼續,今天繼續搗騰動畫,上一節咱們讓汽車的輪子動了起來,回顧一下核心代碼:

//輪子轉動
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章