移动盒子
项目依赖等见我上一篇文章# 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));
用大白话解释一下就是,本来我按了w
和d
键,然后这个时候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
即摄像头的朝向,摄像头锁定一个目标点聚焦,这里直接用box
的x
和z
位置,然后把高度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中文文档
下篇文章研究研究射击,狠狠的射!