【Babylon】工业级3D框架,机器人!前进!(2)

移动盒子

项目依赖等见我上一篇文章# babylon.js入门

本篇文章新增了以下依赖

bash 复制代码
"@babylonjs/core": "^6.18.0"
"@babylonjs/gui": "^6.18.0"
"@babylonjs/havok": "^1.1.4"
"@babylonjs/loaders": "^6.18.0"
"@mdi/font": "^7.2.96"

babylon引入

先搞一个小盒放这里

html 复制代码
<script setup lang="ts">
import { ref, shallowRef, onMounted } from 'vue';
import '@babylonjs/loaders/glTF';
import {
    Engine, // 引擎
    Scene, // 场景
    ArcRotateCamera, // 角旋转相机
    Vector3, // 三维向量
    HemisphericLight, // 方向光
    MeshBuilder,
    StandardMaterial,
    Color3,
    HavokPlugin,
    TransformNode,
    PhysicsAggregate,
    PhysicsShapeType,
} from '@babylonjs/core';
import '@babylonjs/loaders/glTF';
import HavokPhysics from '@babylonjs/havok';

const canvasRef = ref();

const engineValue = shallowRef<Engine>();
const sceneValue = shallowRef<Scene>();
const cameraRef = shallowRef<ArcRotateCamera>();

async function init() {
    const havokPlugin = await HavokPhysics();

    const physicsPlugin = new HavokPlugin(true, havokPlugin);

    const engine = new Engine(canvasRef.value, true);
    engineValue.value = engine;
    const scene = new Scene(engine);
    sceneValue.value = scene;

    scene.enablePhysics(undefined, physicsPlugin);

    addCamera(canvasRef.value);

    // 定义一个方向光, 传入光源名称,方向向量,所属场景
    const light = new HemisphericLight('light1', new Vector3(3, 2, 1), scene);

    run();

    addGround();

    addBox();
}

function addBox() {
    // 以下是创建立方体的步骤
    // 创建一个立方体,传入尺寸参数和所属场景
    const box = MeshBuilder.CreateBox(
        'box1',
        {
            size: 5,
        },
        sceneValue.value
    );
    // 创建一个材质,传入所属场景
    const material = new StandardMaterial('boxMat', sceneValue.value);
    // 设置材质的漫反射颜色
    material.diffuseColor = Color3.Blue();
    // 将立方体的材质设为上面定义的材质
    box.material = material;
    box.position = new Vector3(0, 2.5, 0);
}

// 地板
function addGround() {
    const ground = MeshBuilder.CreateGround(
        'ground',
        { width: 500, height: 500 },
        sceneValue.value
    );
    const material = new StandardMaterial('material', sceneValue.value);
    material.diffuseColor = new Color3(0.5, 1, 0.5);
    ground.material = material;
    ground.checkCollisions = true;
    ground.receiveShadows = true;
    // ground.position.y = -0.01;
    ground.position.z = 15;
    addPhysicsAggregate(ground);
}

function addPhysicsAggregate(meshe: TransformNode) {
    const res = new PhysicsAggregate(
        meshe,
        PhysicsShapeType.BOX,
        { mass: 0, friction: 0.5 },
        sceneValue.value
    );
    // this.physicsViewer.showBody(res.body);
    return res;
}

function run() {
    // 调用引擎的循环渲染函数,在函数中调用场景的渲染函数
    engineValue.value!.runRenderLoop(() => {
        sceneValue.value!.render();
    });
    // 监听窗口变化,调用resize函数
    window.addEventListener('resize', () => {
        engineValue.value!.resize();
    });
}

function addCamera(canvas: HTMLCanvasElement) {
    const camera = new ArcRotateCamera(
        'arcCamera1',
        Math.PI / 2,
        Math.PI / 4,
        10,
        new Vector3(0, 0, 0),
        sceneValue.value
    );

    cameraRef.value = camera;

    camera.attachControl(canvas, true);

    camera.setPosition(new Vector3(0, 8.14, -9.26));
    camera.lowerRadiusLimit = 3; // 最小缩放;
    // this.camera.upperRadiusLimit = 8; // 最大缩放

    // 锁定鼠标指针
    const isLocked = false;
    sceneValue.value!.onPointerDown = () => {
        if (!isLocked) {
            canvas.requestPointerLock =
                canvas.requestPointerLock ||
                canvas.msRequestPointerLock ||
                canvas.mozRequestPointerLock ||
                canvas.webkitRequestPointerLock ||
                false;
            if (canvas.requestPointerLock) {
                // isLocked = true;
                canvas.requestPointerLock();
            }
        }
    };
}

onMounted(() => {
    init();
});
</script>
<template>
    <div class="canvas-wrapper">
        <canvas id="canvas" ref="canvasRef" width="800px" height="800px" />
    </div>
</template>

<style>
.canvas-wrapper {
    width: 500px;
    height: 500px;
}
</style>

小盒移动

ts 复制代码
// 更新一下引入的依赖
import {
    Engine, // 引擎
    Scene, // 场景
    ArcRotateCamera, // 角旋转相机
    Vector3, // 三维向量
    HemisphericLight, // 方向光
    MeshBuilder,
    StandardMaterial,
    Color3,
    HavokPlugin,
    TransformNode,
    PhysicsAggregate,
    PhysicsShapeType,
    ActionManager,
    ExecuteCodeAction,
    Matrix,
    Mesh,
} from '@babylonjs/core';

function bindBox() {
    const inputMap: Record<string, any> = {};
    
    const scene = sceneValue.value!;
    
    const camera = cameraRef.value!;

    const box = userBoxRef.value!;
    
    // 注册监听键盘事件
    scene.actionManager = new ActionManager(scene);
    // 按键按下
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        })
    );
    // 按键抬起
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        })
    );

    // 每帧更新盒子的位置
    scene.registerBeforeRender(function () {
        // 计算盒子的移动速度
        const speed = 0.1;
        const direction = new Vector3(0, 0, 0);

        // 根据按键更新移动方向
        if (inputMap['w'] || inputMap['ArrowUp']) {
            direction.z = -1;
        }
        if (inputMap['s'] || inputMap['ArrowDown']) {
            direction.z = 1;
        }
        if (inputMap['a'] || inputMap['ArrowLeft']) {
            direction.x = 1;
        }
        if (inputMap['d'] || inputMap['ArrowRight']) {
            direction.x = -1;
        }

        // 将方向向量转换为相机坐标系下的方向
        let forward = new Vector3(direction.x, 0, direction.z);
        forward = Vector3.TransformNormal(forward, Matrix.RotationY(box.rotation.y));

        // 更新盒子的位置,移动起来
        userBoxRef.value!.position.addInPlace(forward.scale(speed));
    });
}

这里讲一下移动的核心代码

ts 复制代码
// 这一行是将用户输入的按键录入到向量中,x是左右移动,z是前后移动,可以看成一个与Y轴垂直的向量
let forward = new Vector3(direction.x, 0, direction.z);
// Matrix.RotationY(box.rotation.y))创建了一个绕Y轴旋转的旋转矩阵
// 然后使用 Vector3.TransformNormal 方法将 forward 向量从局部坐标系变换到世界坐标系中
// 以匹配 box 的旋转
forward = Vector3.TransformNormal(forward, Matrix.RotationY(box.rotation.y));

用大白话解释一下就是,本来我按了wd键,然后这个时候box要向左前方向移动,但是因为box的可能会绕着y轴转向,即面向东南西北旋转,所以要根据box.rotation.y的值做一个矩阵变换,把旋转之前的左前,变成旋转之后的左前

移动视角

但是这样看起来怪怪的,一般我们玩游戏都是摄像头跟随主角移动的,所以还需要让摄像头动起来。

typescript 复制代码
function bindBox(){
   ...忽略上方重复代码

    // 每帧更新盒子的位置
    scene.registerBeforeRender(function () {
        // 计算盒子的移动速度
        const speed = 0.1;
        const direction = new Vector3(0, 0, 0);

        // 根据按键更新移动方向
        if (inputMap['w'] || inputMap['ArrowUp']) {
            direction.z = -1;
        }
        if (inputMap['s'] || inputMap['ArrowDown']) {
            direction.z = 1;
        }
        if (inputMap['a'] || inputMap['ArrowLeft']) {
            direction.x = 1;
        }
        if (inputMap['d'] || inputMap['ArrowRight']) {
            direction.x = -1;
        }

        // 将方向向量转换为相机坐标系下的方向
        let forward = new Vector3(direction.x, 0, direction.z);
        forward = Vector3.TransformNormal(forward, Matrix.RotationY(box.rotation.y));

        // 更新盒子的位置
        userBoxRef.value!.position.addInPlace(forward.scale(speed));

        const boxPosition = userBoxRef.value!.position;

        // 更新相机的位置
        camera.target = new Vector3(boxPosition.x, 10, boxPosition.z);

        camera.position = new Vector3(
            boxPosition.x + 10 * Math.sin(userBoxRef.value!.rotation.y),
            12,
            boxPosition.z + 10 * Math.cos(userBoxRef.value!.rotation.y)
        );
    });
}
        

细说一下固定摄像头位置的代码

ts 复制代码
camera.target = new Vector3(boxPosition.x, 10, boxPosition.z);

camera.position = new Vector3(
    boxPosition.x + 10 * Math.sin(userBoxRef.value!.rotation.y),
    12,
    boxPosition.z + 10 * Math.cos(userBoxRef.value!.rotation.y)
);

camera.target即摄像头的朝向,摄像头锁定一个目标点聚焦,这里直接用boxxz位置,然后把高度y 稍微提高一点,做那个第三视角的感觉。

camera.position即摄像头所处的位置,这里把摄像头放在box的后方,用boxPosition.x + 10 * Math.sin(userBoxRef.value!.rotation.y)boxPosition.z + 10 * Math.cos(userBoxRef.value!.rotation.y)算出box当前朝向的后方一点的位置,然后高度y也设置偏高一点。

看起来动了,又好像没动。看不出来动了,懂了,得加个参考系。

ts 复制代码
async function init() {
    ... 忽略以上代码
    addBox();
+   addOtherBox();  
}

function addOtherBox() {
    const box = MeshBuilder.CreateBox(
        'box2',
        {
            size: 5,
        },
        sceneValue.value
    );
    // 创建一个材质,传入所属场景
    const material = new StandardMaterial('boxMat', sceneValue.value);
    // 设置材质的漫反射颜色
    material.diffuseColor = Color3.Red();
    // 将立方体的材质设为上面定义的材质
    box.material = material;
    box.isPickable = true;

    box.position = new Vector3(10, 2.5, 0);

    const box3 = MeshBuilder.CreateBox(
        'box3',
        {
            size: 5,
        },
        sceneValue.value
    );
    box3.material = material;
    box3.isPickable = true;

    box3.position = new Vector3(30, 2.5, 0);
}

这样看起来我们的移动小盒子已经可以前进后退了,但是还有点不足,我们的视角现在是固定朝一个方向的,我想让他可以跟随鼠标方向移动。

鼠标控制方向

ts 复制代码
function moveBoxDirection() {
    const canvas = canvasRef.value;

    const box = userBoxRef.value!;

    const updateBoxTarget = (evt) => {
        const mouseX = evt.movementX || evt.mozMovementX || 0;
        // const mouseY = evt.movementY || evt.mozMovementY || 0;

        // 计算盒子的旋转角度
        const sensitivity = -0.001; // 鼠标灵敏度
        box.rotation.y -= mouseX * sensitivity;
        // box.rotation.x -= mouseY * sensitivity;
    };

    document.addEventListener('pointerlockchange', function () {
        if (document.pointerLockElement === canvas) {
            // 如果鼠标被锁定,则继续监听鼠标移动事件
            canvas.addEventListener('mousemove', updateBoxTarget);
        } else {
            // 如果鼠标未被锁定,则取消监听鼠标移动事件
            canvas.removeEventListener('mousemove', updateBoxTarget);
        }
    });
}

因为最开始的时候,我们是锁定鼠标了,所以这里鼠标移动时提供的事件,就不是鼠标的位置了,而是鼠标每次移动的距离x和y,这里我们忽略x的值,即忽略鼠标上下移动,只取鼠标左右移动,只让盒子方向左右移动就好了。

box.rotation.y -= mouseX * sensitivity来修改盒子的朝向,rotation即盒子的旋转属性,sensitivity是鼠标灵敏度。

有点内味了,上小人试试

机器人

加载模型

从网上找了个小人模型

ts 复制代码
async function init() {
    ... 忽略上方代码

    await loadMesh();

    run();

    addGround();

    addBox();

    addOtherBox();
}


async function loadMesh() {
    // 加载模型
    const container = await loadAsset('/textures/', 'x-bot.glb', () => {
        console.log('---');
    });

    // 这里对小人放大一下,不然显得太小了
    const scaleFactor = 1.2;
    container.meshes.forEach(function (mesh) {
        mesh.scaling = new Vector3(scaleFactor, scaleFactor, scaleFactor);
        mesh.isPickable = false;
    });

    container.addAllToScene();
    
    return container;
}

// 封装一下加载的方法
function loadAsset(
    rootUrl: string,
    sceneFilename: string,
    callback?: (event: ISceneLoaderProgressEvent) => void
): Promise<AssetContainer> {
    const scene = sceneValue.value;
    return new Promise((resolve, reject) => {
        SceneLoader.LoadAssetContainer(
            import.meta.env.BASE_URL + rootUrl,
            sceneFilename,
            scene,
            (container) => {
                resolve(container);
            },
            (evt) => {
                callback && callback(evt);
            },
            (scense, message, error) => {
                console.error(message);
                console.error(error);
                console.log('nul');
                reject(null);
                // resolve(e);
            }
        );
    });
}

这小人还自带攀爬动画呢,我们可以用setWeightForAllAnimatables(0)来控制动画比重,让小人播放我们当前想要的状态。

console.log(container.animationGroups)打印一下看看带了哪些动画

看名字的话,idle应该是站着不动的状态,这里让小人播放idle的动画。

ts 复制代码
// 打印一下模型带了哪些动画
console.log(container.animationGroups);
container.animationGroups.forEach((item, index) => {
    item.play(true);
    if (index === 2) {
        item.setWeightForAllAnimatables(1);
    } else {
        item.setWeightForAllAnimatables(0);
    }
});

机器人移动

我们把上面移动的box换成加载的模型试试

ts 复制代码
async function init() {
    const havokPlugin = await HavokPhysics();

    const physicsPlugin = new HavokPlugin(true, havokPlugin);

    const engine = new Engine(canvasRef.value, true);
    engineValue.value = engine;
    const scene = new Scene(engine);
    sceneValue.value = scene;

    scene.enablePhysics(undefined, physicsPlugin);

    addCamera(canvasRef.value);

    // 定义一个方向光, 传入光源名称,方向向量,所属场景
    const light = new HemisphericLight('light1', new Vector3(3, 2, 1), scene);

    const container = await loadMesh();

    run();

    addGround();
    
    // 修改,把人物模型传进去
    addBox(container.meshes[0] as Mesh);

    addOtherBox();
}

function addBox(box: Mesh) {
    // 以下是创建立方体的步骤
    // 创建一个立方体,传入尺寸参数和所属场景
    // const box = MeshBuilder.CreateBox(
    //     'box1',
    //     {
    //         size: 5,
    //     },
    //     sceneValue.value
    // );
    // // 创建一个材质,传入所属场景
    // const material = new StandardMaterial('boxMat', sceneValue.value);
    // // 设置材质的漫反射颜色
    // material.diffuseColor = Color3.Blue();
    // // 将立方体的材质设为上面定义的材质
    // box.material = material;
    // 这里的位置做一下小修改,不然模型会悬空,同样的,摄像机视角也要修改一下
    box.position = new Vector3(0, 0, 0);

    userBoxRef.value = box;

    bindBox();

    moveBoxDirection();
}

function bindBox() {
    // 监听键盘事件
    const inputMap: Record<string, any> = {};
    const scene = sceneValue.value!;
    scene.actionManager = new ActionManager(scene);
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        })
    );
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        })
    );

    const camera = cameraRef.value!;

    const box = userBoxRef.value!;

    // 每帧更新盒子的位置
    scene.registerBeforeRender(function () {
        // 计算盒子的移动速度
        const speed = 0.1;
        const direction = new Vector3(0, 0, 0);

        // 根据按键更新移动方向
        if (inputMap['w'] || inputMap['ArrowUp']) {
            direction.z = -1;
        }
        if (inputMap['s'] || inputMap['ArrowDown']) {
            direction.z = 1;
        }
        if (inputMap['a'] || inputMap['ArrowLeft']) {
            direction.x = 1;
        }
        if (inputMap['d'] || inputMap['ArrowRight']) {
            direction.x = -1;
        }

        // 将方向向量转换为相机坐标系下的方向
        let forward = new Vector3(direction.x, 0, direction.z);
        forward = Vector3.TransformNormal(forward, Matrix.RotationY(box.rotation.y));

        // 更新盒子的位置
        userBoxRef.value!.position.addInPlace(forward.scale(speed));

        const boxPosition = userBoxRef.value!.position;

        // 修改摄像机的位置,y改成5就好了
        camera.target = new Vector3(boxPosition.x, 5, boxPosition.z);

        camera.position = new Vector3(
            boxPosition.x + 10 * Math.sin(userBoxRef.value!.rotation.y),
            5,
            boxPosition.z + 10 * Math.cos(userBoxRef.value!.rotation.y)
        );
    });
}

看看效果

移动是可以移动的,但是人物模型的脸不跟随我鼠标移动呢。

人物跟随鼠标转向

问chatgpt才知道,模型是需要用lookAt控制朝向的。

ts 复制代码
function bindBox() {
    // 监听键盘事件
    const inputMap: Record<string, any> = {};
    const scene = sceneValue.value!;
    scene.actionManager = new ActionManager(scene);
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        })
    );
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        })
    );

    const camera = cameraRef.value!;

    const box = userBoxRef.value!;

    // 每帧更新盒子的位置
    scene.registerBeforeRender(function () {
        // 计算盒子的移动速度
        const speed = 0.1;
        const direction = new Vector3(0, 0, 0);

        // 根据按键更新移动方向
        if (inputMap['w'] || inputMap['ArrowUp']) {
            direction.z = -1;
        }
        if (inputMap['s'] || inputMap['ArrowDown']) {
            direction.z = 1;
        }
        if (inputMap['a'] || inputMap['ArrowLeft']) {
            direction.x = 1;
        }
        if (inputMap['d'] || inputMap['ArrowRight']) {
            direction.x = -1;
        }

        // 将方向向量转换为相机坐标系下的方向
        let forward = new Vector3(direction.x, 0, direction.z);
        forward = Vector3.TransformNormal(forward, Matrix.RotationY(box.rotation.y));

        // 更新盒子的位置
        userBoxRef.value!.position.addInPlace(forward.scale(speed));

        const boxPosition = userBoxRef.value!.position;

        // 更新相机的位置
        camera.target = new Vector3(boxPosition.x, 5, boxPosition.z);

        camera.position = new Vector3(
            boxPosition.x + 10 * Math.sin(userBoxRef.value!.rotation.y),
            5,
            boxPosition.z + 10 * Math.cos(userBoxRef.value!.rotation.y)
        );

        const origin = box.position.clone();

        const boxDirection = camera!.getTarget().subtract(camera!.position).normalize();

        box.lookAt(origin.add(boxDirection));
    });
}

这里又涉及到一个向量的问题

ts 复制代码
// 获取box的位置
const origin = box.position.clone();
// 获取摄像机的位置,并且这俩位置相减,然后normalize归一化
const boxDirection = camera!.getTarget().subtract(camera!.position).normalize();
// 模型lookAt
box.lookAt(origin.add(boxDirection));

看看效果

现在旋转起来是没啥问题了,就是没有走路的动画,再加上走路的动画。

走路动画

声明个变量保存一下走路的动画

ts 复制代码
const workAnim = shallowRef<any>();

// 控制动画播放
function playWalkAnimation() {
    workAnim.value.setWeightForAllAnimatables(1);
    workAnim.value.restart();
}
// 动画暂停
function stopWalkAnimation() {
    workAnim.value.setWeightForAllAnimatables(0);
    workAnim.value.pause();
}

async function loadMesh() {
    // 加载模型
    const container = await loadAsset('/textures/', 'x-bot.glb', () => {
        console.log('---');
    });

    // 放大一下模型
    const scaleFactor = 1.2;
    container.meshes.forEach(function (mesh) {
        mesh.scaling = new Vector3(scaleFactor, scaleFactor, scaleFactor);
        mesh.isPickable = false;
    });

    container.addAllToScene();

    console.log(container.animationGroups);

    container.animationGroups.forEach((item, index) => {
        item.play(true);
        if (index === 2) {
            item.setWeightForAllAnimatables(1);
        } else {
            item.setWeightForAllAnimatables(0);
        }
    });

    // 动画变量赋值
    const walkAnimation = container.animationGroups[4];

    workAnim.value = walkAnimation;

    return container;
}

声明一个变量控制当前是否在移动,移动的话就播放移动动画,否则就停止

ts 复制代码
let isMoving = false;

const moveKeys = ['w', 'a', 's', 'd'];

在按键按下和抬起时修改变量

ts 复制代码
scene.actionManager.registerAction(
    new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, function (evt) {
        inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        if (moveKeys.includes(evt.sourceEvent.key)) {
            isMoving = true;
        }
    })
);
scene.actionManager.registerAction(
    new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, function (evt) {
        inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
        const allUp = Object.values(inputMap).every((e) => e === false);

        if (allUp) {
            isMoving = false;
        }
    })
);

最后,在渲染时播放或暂停动画

ts 复制代码
// 每帧更新盒子的位置
scene.registerBeforeRender(function () {
    // 计算盒子的移动速度
    const speed = 0.1;
    const direction = new Vector3(0, 0, 0);

    // 根据按键更新移动方向
    if (inputMap['w'] || inputMap['ArrowUp']) {
        direction.z = -1;
    }
    if (inputMap['s'] || inputMap['ArrowDown']) {
        direction.z = 1;
    }
    if (inputMap['a'] || inputMap['ArrowLeft']) {
        direction.x = 1;
    }
    if (inputMap['d'] || inputMap['ArrowRight']) {
        direction.x = -1;
    }

    // 将方向向量转换为相机坐标系下的方向
    let forward = new Vector3(direction.x, 0, direction.z);
    forward = Vector3.TransformNormal(forward, Matrix.RotationY(box.rotation.y));

    // 更新盒子的位置
    userBoxRef.value!.position.addInPlace(forward.scale(speed));

    const boxPosition = userBoxRef.value!.position;

    // 更新相机的位置
    camera.target = new Vector3(boxPosition.x, 5, boxPosition.z);

    camera.position = new Vector3(
        boxPosition.x + 10 * Math.sin(userBoxRef.value!.rotation.y),
        5,
        boxPosition.z + 10 * Math.cos(userBoxRef.value!.rotation.y)
    );

    const origin = box.position.clone();

    const boxDirection = camera!.getTarget().subtract(camera!.position).normalize();

    box.lookAt(origin.add(boxDirection));

    // 控制动画是否播放
    if (isMoving) {
        playWalkAnimation();
    } else {
        stopWalkAnimation();
    }
});

可以看到,最后效果虽然略有生硬,但是基本要求我们已经做到了。

最后,呈上我们全部代码

html 复制代码
<script setup lang="ts">
import { ref, shallowRef, onMounted } from 'vue';
import '@babylonjs/loaders/glTF';
import {
    Engine, // 引擎
    Scene, // 场景
    ArcRotateCamera, // 角旋转相机
    Vector3, // 三维向量
    HemisphericLight, // 方向光
    MeshBuilder,
    StandardMaterial,
    Color3,
    HavokPlugin,
    TransformNode,
    PhysicsAggregate,
    PhysicsShapeType,
    ActionManager,
    ExecuteCodeAction,
    Matrix,
    Mesh,
    AssetContainer,
    SceneLoader,
    type ISceneLoaderProgressEvent,
} from '@babylonjs/core';
import '@babylonjs/loaders/glTF';
import HavokPhysics from '@babylonjs/havok';

const canvasRef = ref();

const engineValue = shallowRef<Engine>();
const sceneValue = shallowRef<Scene>();
const cameraRef = shallowRef<ArcRotateCamera>();

const workAnim = shallowRef<any>();

// 控制动画播放
function playWalkAnimation() {
    workAnim.value.setWeightForAllAnimatables(1);
    workAnim.value.restart();
}
// 动画暂停
function stopWalkAnimation() {
    workAnim.value.setWeightForAllAnimatables(0);
    workAnim.value.pause();
}

async function init() {
    const havokPlugin = await HavokPhysics();

    const physicsPlugin = new HavokPlugin(true, havokPlugin);

    const engine = new Engine(canvasRef.value, true);
    engineValue.value = engine;
    const scene = new Scene(engine);
    sceneValue.value = scene;

    scene.enablePhysics(undefined, physicsPlugin);

    addCamera(canvasRef.value);

    // 定义一个方向光, 传入光源名称,方向向量,所属场景
    const light = new HemisphericLight('light1', new Vector3(3, 2, 1), scene);

    const container = await loadMesh();

    run();

    addGround();

    addBox(container.meshes[0] as Mesh);

    addOtherBox();
}

async function loadMesh() {
    // 加载模型
    const container = await loadAsset('/textures/', 'x-bot.glb', () => {
        console.log('---');
    });

    // 放大一下模型
    const scaleFactor = 1.2;
    container.meshes.forEach(function (mesh) {
        mesh.scaling = new Vector3(scaleFactor, scaleFactor, scaleFactor);
        mesh.isPickable = false;
    });

    container.addAllToScene();

    console.log(container.animationGroups);

    container.animationGroups.forEach((item, index) => {
        item.play(true);
        if (index === 2) {
            item.setWeightForAllAnimatables(1);
        } else {
            item.setWeightForAllAnimatables(0);
        }
    });

    const walkAnimation = container.animationGroups[4]; // 假设走路动画在动画范围[0, 30]内

    workAnim.value = walkAnimation;

    return container;
}

const userBoxRef = shallowRef<Mesh>();

function addBox(box: Mesh) {
    // 以下是创建立方体的步骤
    // 创建一个立方体,传入尺寸参数和所属场景
    // const box = MeshBuilder.CreateBox(
    //     'box1',
    //     {
    //         size: 5,
    //     },
    //     sceneValue.value
    // );
    // // 创建一个材质,传入所属场景
    // const material = new StandardMaterial('boxMat', sceneValue.value);
    // // 设置材质的漫反射颜色
    // material.diffuseColor = Color3.Blue();
    // // 将立方体的材质设为上面定义的材质
    // box.material = material;
    box.position = new Vector3(0, 0, 0);

    userBoxRef.value = box;

    bindBox();

    moveBoxDirection();
}

function addGround() {
    const ground = MeshBuilder.CreateGround(
        'ground',
        { width: 500, height: 500 },
        sceneValue.value
    );
    const material = new StandardMaterial('material', sceneValue.value);
    material.diffuseColor = new Color3(0.5, 1, 0.5);
    ground.material = material;
    ground.checkCollisions = true;
    ground.receiveShadows = true;
    // ground.position.y = -0.01;
    ground.position.z = 15;
    addPhysicsAggregate(ground);
}

function addPhysicsAggregate(meshe: TransformNode) {
    const res = new PhysicsAggregate(
        meshe,
        PhysicsShapeType.BOX,
        { mass: 0, friction: 0.5 },
        sceneValue.value
    );
    // this.physicsViewer.showBody(res.body);
    return res;
}

function run() {
    // 调用引擎的循环渲染函数,在函数中调用场景的渲染函数
    engineValue.value!.runRenderLoop(() => {
        sceneValue.value!.render();
    });
    // 监听窗口变化,调用resize函数
    window.addEventListener('resize', () => {
        engineValue.value!.resize();
    });
}

function addCamera(canvas: HTMLCanvasElement) {
    const camera = new ArcRotateCamera(
        'arcCamera1',
        Math.PI / 2,
        Math.PI / 4,
        10,
        new Vector3(0, 0, 0),
        sceneValue.value
    );

    cameraRef.value = camera;

    camera.attachControl(canvas, true);

    camera.setPosition(new Vector3(0, 8.14, -9.26));
    camera.lowerRadiusLimit = 3; // 最小缩放;
    // this.camera.upperRadiusLimit = 8; // 最大缩放

    // 锁定鼠标指针
    const isLocked = false;
    sceneValue.value!.onPointerDown = () => {
        if (!isLocked) {
            canvas.requestPointerLock =
                canvas.requestPointerLock ||
                canvas.msRequestPointerLock ||
                canvas.mozRequestPointerLock ||
                canvas.webkitRequestPointerLock ||
                false;
            if (canvas.requestPointerLock) {
                // isLocked = true;
                canvas.requestPointerLock();
            }
        }
    };
}

let isMoving = false;

const moveKeys = ['w', 'a', 's', 'd'];

function bindBox() {
    // 监听键盘事件
    const inputMap: Record<string, any> = {};
    const scene = sceneValue.value!;
    scene.actionManager = new ActionManager(scene);
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
            if (moveKeys.includes(evt.sourceEvent.key)) {
                isMoving = true;
            }
        })
    );
    scene.actionManager.registerAction(
        new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, function (evt) {
            inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown';
            const allUp = Object.values(inputMap).every((e) => e === false);

            if (allUp) {
                isMoving = false;
            }
        })
    );

    const camera = cameraRef.value!;

    const box = userBoxRef.value!;

    // 每帧更新盒子的位置
    scene.registerBeforeRender(function () {
        // 计算盒子的移动速度
        const speed = 0.1;
        const direction = new Vector3(0, 0, 0);

        // 根据按键更新移动方向
        if (inputMap['w'] || inputMap['ArrowUp']) {
            direction.z = -1;
        }
        if (inputMap['s'] || inputMap['ArrowDown']) {
            direction.z = 1;
        }
        if (inputMap['a'] || inputMap['ArrowLeft']) {
            direction.x = 1;
        }
        if (inputMap['d'] || inputMap['ArrowRight']) {
            direction.x = -1;
        }

        // 将方向向量转换为相机坐标系下的方向
        let forward = new Vector3(direction.x, 0, direction.z);
        forward = Vector3.TransformNormal(forward, Matrix.RotationY(box.rotation.y));

        // 更新盒子的位置
        userBoxRef.value!.position.addInPlace(forward.scale(speed));

        const boxPosition = userBoxRef.value!.position;

        // 更新相机的位置
        camera.target = new Vector3(boxPosition.x, 5, boxPosition.z);

        camera.position = new Vector3(
            boxPosition.x + 10 * Math.sin(userBoxRef.value!.rotation.y),
            5,
            boxPosition.z + 10 * Math.cos(userBoxRef.value!.rotation.y)
        );

        const origin = box.position.clone();

        const boxDirection = camera!.getTarget().subtract(camera!.position).normalize();

        box.lookAt(origin.add(boxDirection));

        if (isMoving) {
            playWalkAnimation();
        } else {
            stopWalkAnimation();
        }
    });
}

function moveBoxDirection() {
    const canvas = canvasRef.value;

    const box = userBoxRef.value!;

    const updateBoxTarget = (evt) => {
        const mouseX = evt.movementX || evt.mozMovementX || 0;
        // const mouseY = evt.movementY || evt.mozMovementY || 0;

        // 计算盒子的旋转角度
        const sensitivity = -0.001; // 鼠标灵敏度
        box.rotation.y -= mouseX * sensitivity;
        // box.rotation.x -= mouseY * sensitivity;
    };

    document.addEventListener('pointerlockchange', function () {
        if (document.pointerLockElement === canvas) {
            // 如果鼠标被锁定,则继续监听鼠标移动事件
            canvas.addEventListener('mousemove', updateBoxTarget);
        } else {
            // 如果鼠标未被锁定,则取消监听鼠标移动事件
            canvas.removeEventListener('mousemove', updateBoxTarget);
        }
    });
}

function addOtherBox() {
    // 以上是之前的构造函数
    // 以下是创建立方体的步骤
    // 创建一个立方体,传入尺寸参数和所属场景
    const box = MeshBuilder.CreateBox(
        'box2',
        {
            size: 5,
        },
        sceneValue.value
    );
    // 创建一个材质,传入所属场景
    const material = new StandardMaterial('boxMat', sceneValue.value);
    // 设置材质的漫反射颜色
    material.diffuseColor = Color3.Red();
    // 将立方体的材质设为上面定义的材质
    box.material = material;
    box.isPickable = true;

    box.position = new Vector3(10, 2.5, 0);

    const box3 = MeshBuilder.CreateBox(
        'box3',
        {
            size: 5,
        },
        sceneValue.value
    );
    box3.material = material;
    box3.isPickable = true;

    box3.position = new Vector3(30, 2.5, 0);
}

function loadAsset(
    rootUrl: string,
    sceneFilename: string,
    callback?: (event: ISceneLoaderProgressEvent) => void
): Promise<AssetContainer> {
    const scene = sceneValue.value;
    return new Promise((resolve, reject) => {
        SceneLoader.LoadAssetContainer(
            import.meta.env.BASE_URL + rootUrl,
            sceneFilename,
            scene,
            (container) => {
                resolve(container);
            },
            (evt) => {
                callback && callback(evt);
            },
            (scense, message, error) => {
                console.error(message);
                console.error(error);
                console.log('nul');
                reject(null);
                // resolve(e);
            }
        );
    });
}

onMounted(() => {
    init();
});
</script>
<template>
    <div class="canvas-wrapper">
        <canvas id="canvas" ref="canvasRef" width="800px" height="800px" />
    </div>
</template>

<style>
.canvas-wrapper {
    width: 500px;
    height: 500px;
}
</style>

参考项目和文档

大佬开源的项目第三视角移动项目,模型就是从这里取得,函数也有一些借鉴。

babylonjs中文文档

下篇文章研究研究射击,狠狠的射!

相关推荐
susu10830189111 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端
程序猿阿伟3 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒3 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript