【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中文文档

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

相关推荐
会发光的猪。1 分钟前
vue中el-select选择框带搜索和输入,根据用户输入的值显示下拉列表
前端·javascript·vue.js·elementui
旺旺大力包18 分钟前
【 Git 】git 的安装和使用
前端·笔记·git
雪落满地香34 分钟前
前端:改变鼠标点击物体的颜色
前端
余生H1 小时前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿1 小时前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~1 小时前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫1 小时前
高德地图自定义折线矢量图形
前端·vue.js·vue
m0_748250931 小时前
html 通用错误页面
前端·html
来吧~2 小时前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
han_2 小时前
不是哥们,我的console.log突然打印不出东西了!
前端·javascript·chrome