feat(All):Initial

This commit is contained in:
2025-10-04 23:36:07 +08:00
commit 2b4e5d2668
1321 changed files with 415958 additions and 0 deletions
+709
View File
@@ -0,0 +1,709 @@
/**
* @author ErSan
* @email mlt131220@163.com
* @date 2024/08/28
* @description 漫游类,使用BVH检测碰撞,人物模型必须包含动画:Enter,Idle, Walking, WalkingBackward,Jumping
*/
import * as THREE from 'three';
import CameraControls from 'camera-controls';
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
import {RoundedBoxGeometry} from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
import {GenerateMeshBVHWorker} from '@/workers/bvh/GenerateMeshBVHWorker.js';
import {useDispatchSignal} from "@/hooks";
import {getMeshByInstancedMesh} from "@/utils";
import {RoamingStatus} from "./RoamingStatus";
import Loader from "@/core/loader/Loader";
import App from "@/core/app/App";
import Viewer from "@/core/viewer/Viewer";
import MergeGeometriesWorker from "@/workers/mergeGeometries.worker.ts?worker&url";
let keyDownFn, keyUpFn;
class Roaming {
private viewer: Viewer;
private controls: CameraControls;
group: THREE.Group;
private collider: THREE.Mesh | undefined; // 碰撞器
private player: THREE.Mesh | undefined; // 碰撞胶囊体
person: THREE.Group | undefined; // 人物
private playerIsOnGround = true;
private playerVelocity = new THREE.Vector3();
private gravity = -25; // 重力
private playerSpeed = 3; // 人物移动速度
playerInitPos = new THREE.Vector3(0, 0, 0); // 人物初始位置
private firstPerson = true; // 是否第一人称
// 按键监听
private fwdPressed = false;
private bkdPressed = false;
private lftPressed = false;
private rgtPressed = false;
private upVector = new THREE.Vector3(0, 1, 0);
private tempVector = new THREE.Vector3();
private tempVector2 = new THREE.Vector3();
private tempBox = new THREE.Box3();
private tempMat = new THREE.Matrix4();
private tempSegment = new THREE.Line3();
public isRoaming = false; // 是否在漫游
mergeWorker: Worker;
private generateMeshBVHWorker: GenerateMeshBVHWorker;
private personStatus: RoamingStatus | null = null;
constructor(viewer: Viewer) {
this.viewer = viewer;
this.controls = viewer.modules.controls;
keyDownFn = this.keyDown.bind(this);
window.addEventListener('keydown', keyDownFn);
keyUpFn = this.keyUp.bind(this);
window.addEventListener('keyup', keyUpFn);
this.group = new THREE.Group();
this.group.name = "es-3d-roaming-group";
this.group.visible = false;
this.group.ignore = true;
this.mergeWorker = new Worker(MergeGeometriesWorker, {type: 'module'});
this.generateMeshBVHWorker = new GenerateMeshBVHWorker();
this.addPlayer();
}
keyDown(e: KeyboardEvent) {
if (!this.isRoaming || e.repeat) return;
switch (e.code) {
case 'KeyW':
this.fwdPressed = true;
this.personStatus?.setStatus("w", true);
break;
case 'KeyS':
this.bkdPressed = true;
this.personStatus?.setStatus("s", true);
break;
case 'KeyD':
this.rgtPressed = true;
this.personStatus?.setStatus("d", true);
break;
case 'KeyA':
this.lftPressed = true;
this.personStatus?.setStatus("a", true);
break;
case 'Space':
if (this.personStatus?.keyDownStatus.space) return;
if (this.playerIsOnGround) {
// 跳跃动画有30FPS准备动作
setTimeout(() => {
this.playerVelocity.y = 10.0;
this.playerIsOnGround = false;
}, (30 / App.FPS) * 1000)
}
this.personStatus?.setStatus("space", true);
break;
case "ShiftLeft":
case "ShiftRight":
if (this.personStatus?.isWalkingForward) {
this.playerSpeed = 6;
this.personStatus?.setStatus("shift", true);
}
break;
case 'KeyV': // 切换第一/第三人称视角
this.firstPerson = !this.firstPerson;
if (this.firstPerson) { //人称切换
// 第一人称
this.controls.maxPolarAngle = Math.PI / 2;
this.controls.minDistance = 0.8;
this.controls.maxDistance = 0.8;
this.controls.distance = 0.8;
} else {
this.controls.maxPolarAngle = Math.PI / 2;
this.controls.minDistance = 6;
this.controls.maxDistance = 6;
this.controls.distance = 6;
}
break;
}
}
keyUp(e: KeyboardEvent) {
if (!this.isRoaming || e.repeat) return;
switch (e.code) {
case 'KeyW':
this.personStatus?.setStatus("w", false);
this.fwdPressed = false;
break;
case 'KeyS':
this.personStatus?.setStatus("s", false);
this.bkdPressed = false;
break;
case 'KeyD':
this.personStatus?.setStatus("d", false);
this.rgtPressed = false;
break;
case 'KeyA':
this.personStatus?.setStatus("a", false);
this.lftPressed = false;
break;
case "ShiftLeft":
case "ShiftRight":
this.playerSpeed = 3;
this.personStatus?.setStatus("shift", false);
break;
}
}
// 添加漫游所需人物模型
addPlayer(){
// 几何圆柱体 用于碰撞检测
const cylinder = new THREE.Mesh(
new RoundedBoxGeometry(0.5, 1.7, 0.5, 10, 0.5),
new THREE.MeshStandardMaterial()
)
cylinder.geometry.translate(0, -0.6, 0);
// @ts-ignore
cylinder.capsuleInfo = {
radius: 0.4,
segment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -1.0, 0.0))
}
cylinder.name = 'es-3d-roaming-cylinder';
cylinder.visible = false;
this.player = cylinder;
this.group.add(cylinder);
this.reloadPerson();
}
/**
* 重置漫游人物模型
*/
async reloadPerson() {
// 加载人物模型glb
const loader = await Loader.createGLTFLoader();
const done = (blob) => {
// 加载人物模型Blob
loader.loadAsync(URL.createObjectURL(blob)).then(result => {
const person = result.scene as THREE.Group;
person.name = "es-3d-roaming-player";
if(this.person){
person.matrix.copy(this.person.matrix);
person.matrixWorld.copy(this.person.matrixWorld);
this.person.removeFromParent();
}
this.person = person;
this.group.add(person);
// 漫游人物动画状态机
if(this.personStatus){
this.personStatus.dispose();
}
this.personStatus = new RoamingStatus(person, result.animations);
});
}
// 从本地DB读取人物模型
const playerConfig= App.config.getKey("roamingCharacter")
App.storage.getModel(`player-${playerConfig}`).then((file: Blob | unknown) => {
if (!file) {
const playerGlbUrl = new URL(`${import.meta.env.BASE_URL}resource/model/${playerConfig}.glb`, import.meta.url).href;
// 加载默认人物模型
fetch(playerGlbUrl).then(res => res.blob()).then(blob => {
App.storage.setModel(`player-${playerConfig}`, blob)
done(blob);
})
} else {
done(file);
}
})
}
// 生成碰撞器环境
generateColliderEnvironment() {
let mergedGeometry:any;
// TODO20251003 - environment组好像没有存在的意义?运行两个月无误后删除
//const environment = new THREE.Group();
//environment.name = "astral-3d-roaming-collider-environment";
const generateBVH = () => {
return new Promise(resolve => {
this.generateMeshBVHWorker.generate(mergedGeometry).then(bvh => {
// @ts-ignore
mergedGeometry.boundsTree = bvh;
this.collider = new THREE.Mesh(mergedGeometry);
// @ts-ignore
this.collider.material.wireframe = false;
this.collider.name = "astral-3d-roaming-collider";
this.collider.visible = false;
// @ts-ignore
this.group.add(this.collider);
resolve("");
this.generateMeshBVHWorker.dispose();
});
//environment.visible = false;
//this.group.add(environment);
this.viewer.scene.add(this.group);
})
}
const generateMergedGeometry = () => {
return new Promise((resolve,reject) => {
const cloneGeom = (me) => {
// 检查对应属性是否存在
if (!me.geometry.attributes || !me.geometry.attributes.position || me.geometry.attributes.position.isInterleavedBufferAttribute) return;
let geom = me.geometry.clone();
geom.applyMatrix4(me.matrixWorld);
// 合并仅保留position即可
geom.attributes = {
position: geom.toNonIndexed().attributes.position, // 取消position索引
}
// 手动纠正有些模型没有顶点索引的问题
if (geom.index) geom.index = null;
this.mergeWorker.postMessage({
type: "push",
// geometry: geom
// 合并在容差范围内的具有相似属性的顶点
geometry: BufferGeometryUtils.mergeVertices(geom)
})
}
this.viewer.scene.traverseByCondition(c => {
// requestIdleCallback(()=>{
// 只合并网格
if (c.geometry) {
// @ts-ignore
if (!c.isInstancedMesh) {
cloneGeom(c);
} else {
const meshes = getMeshByInstancedMesh(c as THREE.InstancedMesh);
meshes.forEach((m: THREE.Mesh) => {
cloneGeom(m);
});
}
}
// })
}, (c) => !c.ignore && !c.isTilesGroup && !c.isTiles && c.visible)
// requestIdleCallback(()=>{
this.mergeWorker.postMessage({
type: "merge"
})
// })
this.mergeWorker.onmessage = (event) => {
if(event.data.type === "error") {
// 有可能是纯3DTiles场景
if(this.viewer.modules.tilesManage.tilesMap.size === 0){
reject(event.data.message);
}else{
resolve("");
}
return;
}
if (!event.data.geometry) return;
mergedGeometry = event.data.geometry;
mergedGeometry.__proto__ = THREE.BufferGeometry.prototype;
mergedGeometry.index && (mergedGeometry.index.__proto__ = THREE.BufferAttribute.prototype);
mergedGeometry.attributes.position.__proto__ = THREE.BufferAttribute.prototype;
mergedGeometry.attributes.normal && (mergedGeometry.attributes.normal.__proto__ = THREE.BufferAttribute.prototype);
// 删除uv属性
if (mergedGeometry.attributes.uv) {
mergedGeometry.deleteAttribute("uv");
}
// const newMesh = new THREE.Mesh(mergedGeometry, new THREE.MeshBasicMaterial());
//const newMesh = new THREE.Mesh(BufferGeometryUtils.mergeVertices(mergedGeometry), new THREE.MeshBasicMaterial());
//newMesh.visible = false;
//environment.add(newMesh);
generateBVH().then(() => {
resolve("");
});
// 关闭 worker
this.mergeWorker.terminate();
}
})
}
return generateMergedGeometry();
}
// 重置人物位置
resetPlayer() {
const player = this.player as THREE.Mesh;
this.playerVelocity.set(0, 0, 0);
player.position.copy(this.playerInitPos);
// 播放模型进入动画
this.personStatus?.init();
const _target = new THREE.Vector3();
this.controls.getTarget(_target);
this.viewer.camera.position.sub(_target);
this.controls.setTarget(player.position.x, player.position.y + 2, player.position.z, false);
this.controls.distance = this.firstPerson ? 0.8 : 6;
this.viewer.camera.position.add(player.position);
this.controls.update(0.016);
}
// 进入漫游
startRoaming() {
if (this.isRoaming) return;
this.group.visible = true;
this.viewer.computedSceneBox3();
this.resetPlayer();
this.isRoaming = true;
}
// 退出漫游
exitRoaming(lastRoadCameraPos = new THREE.Vector3(1, 1, 1), lastRoadCameraTarget = new THREE.Vector3()) {
this.group.visible = false;
lastRoadCameraPos && this.controls.setPosition(lastRoadCameraPos.x, lastRoadCameraPos.y, lastRoadCameraPos.z, true);
lastRoadCameraTarget && this.controls.setTarget(lastRoadCameraTarget.x, lastRoadCameraTarget.y, lastRoadCameraTarget.z, true);
this.controls.maxPolarAngle = Math.PI;
this.controls.minDistance = 0;
this.controls.maxDistance = Infinity;
this.controls.update(0.016);
this.isRoaming = false;
// 停用混合器上所有预定的动作
this.personStatus?.stopAllAction();
useDispatchSignal("sceneGraphChanged");
}
render(delta: number) {
if (!delta) return;
const player = this.player as THREE.Object3D;
// =========================
// 重力与竖直方向
// =========================
if (this.playerIsOnGround) {
this.playerVelocity.y = delta * this.gravity;
} else {
this.playerVelocity.y += delta * this.gravity;
}
player.position.addScaledVector(this.playerVelocity, delta);
// =========================
// 水平方向移动
// =========================
const angle = this.controls.azimuthAngle;
if (this.fwdPressed) {
this.tempVector.set(0, 0, -1).applyAxisAngle(this.upVector, angle);
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
}
if (this.bkdPressed) {
this.tempVector.set(0, 0, 1).applyAxisAngle(this.upVector, angle);
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
}
if (this.lftPressed) {
this.tempVector.set(-1, 0, 0).applyAxisAngle(this.upVector, angle);
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
}
if (this.rgtPressed) {
this.tempVector.set(1, 0, 0).applyAxisAngle(this.upVector, angle);
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
}
player.updateMatrixWorld();
// =========================
// 碰撞检测
// =========================
// @ts-ignore
const capsuleInfo = (player as any).capsuleInfo;
const worldSegStart = capsuleInfo.segment.start.clone().applyMatrix4(player.matrixWorld);
// 收集所有 collider mesh
const colliders: THREE.Mesh[] = [];
if (this.viewer.modules.tilesManage.mergeMesh) {
colliders.push(this.viewer.modules.tilesManage.mergeMesh);
}
if (this.collider) colliders.push(this.collider);
let chosenNewPositionWorld: THREE.Vector3 | null = null;
let maxOffsetLen = -Infinity;
for (const mesh of colliders) {
if (!mesh.geometry?.boundsTree) continue;
// 胶囊段:player.local → world → mesh.local
this.tempMat.copy(mesh.matrixWorld).invert();
this.tempSegment.copy(capsuleInfo.segment);
this.tempSegment.start.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
this.tempSegment.end.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
// AABB for shapecast
this.tempBox.makeEmpty();
this.tempBox.expandByPoint(this.tempSegment.start);
this.tempBox.expandByPoint(this.tempSegment.end);
this.tempBox.min.addScalar(-capsuleInfo.radius);
this.tempBox.max.addScalar(capsuleInfo.radius);
// 执行 shapecast,会修改 this.tempSegment
mesh.geometry.boundsTree.shapecast({
intersectsBounds: box => box.intersectsBox(this.tempBox),
intersectsTriangle: tri => {
// 检查三角形是否与胶囊相交,如果相交则调整胶囊位置。
const triPoint = this.tempVector;
const capsulePoint = this.tempVector2;
const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
if (distance < (this.player as THREE.Object3D).capsuleInfo.radius) {
const depth = (this.player as THREE.Object3D).capsuleInfo.radius - distance;
const direction = capsulePoint.sub(triPoint).normalize();
this.tempSegment.start.addScaledVector(direction, depth);
this.tempSegment.end.addScaledVector(direction, depth);
}
return false;
}
});
// 结果变回 world 空间
const adjustedWorld = this.tempSegment.start.clone().applyMatrix4(mesh.matrixWorld);
const offsetLen = adjustedWorld.distanceTo(worldSegStart);
if (offsetLen > maxOffsetLen) {
maxOffsetLen = offsetLen;
chosenNewPositionWorld = adjustedWorld;
}
}
// 应用最终选择的位移
if (chosenNewPositionWorld) {
const deltaVector = this.tempVector2.subVectors(chosenNewPositionWorld, player.position);
this.playerIsOnGround = deltaVector.y > Math.abs(delta * this.playerVelocity.y * 0.25);
const offset = Math.max(0.0, deltaVector.length() - 1e-5);
deltaVector.normalize().multiplyScalar(offset);
player.position.add(deltaVector);
if (!this.playerIsOnGround) {
deltaVector.normalize();
this.playerVelocity.addScaledVector(deltaVector, -deltaVector.dot(this.playerVelocity));
} else {
this.playerVelocity.set(0, 0, 0);
}
}
// =========================
// 相机调整
// =========================
const v = new THREE.Vector3(player.position.x, player.position.y + 0.2, player.position.z);
const _target = new THREE.Vector3();
this.controls.getTarget(_target);
this.viewer.camera.position.sub(_target);
this.controls.setTarget(v.x, v.y, v.z, false);
this.controls.distance = this.firstPerson ? 0.8 : 6;
this.viewer.camera.position.add(v);
this.controls.polarAngle = Math.PI / 2;
// 人物模型位置跟随
if (this.person) {
this.person.position.set(player.position.x, player.position.y - 1.415, player.position.z);
}
// 跌落检测
if (this.viewer.sceneBox3 && (this.viewer.sceneBox3.min.y - player.position.y > 15)) {
requestAnimationFrame(() => this.resetPlayer());
}
// 动画状态更新
this.personStatus?.update(delta);
}
// render(delta: number) {
// if (!delta) return;
//
// const player = this.player as THREE.Object3D;
//
// if (this.playerIsOnGround) {
// this.playerVelocity.y = delta * this.gravity;
// } else {
// this.playerVelocity.y += delta * this.gravity;
// }
//
// // 人物竖直方向移动(跳跃)
// player.position.addScaledVector(this.playerVelocity, delta);
//
// /* 人物移动 */
// const angle = this.controls.azimuthAngle;
// if (this.fwdPressed) {
// this.tempVector.set(0, 0, -1).applyAxisAngle(this.upVector, angle);
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
// }
//
// if (this.bkdPressed) {
// this.tempVector.set(0, 0, 1).applyAxisAngle(this.upVector, angle);
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
// }
//
// if (this.lftPressed) {
// this.tempVector.set(-1, 0, 0).applyAxisAngle(this.upVector, angle);
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
// }
//
// if (this.rgtPressed) {
// this.tempVector.set(1, 0, 0).applyAxisAngle(this.upVector, angle);
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
// }
//
// player.updateMatrixWorld();
//
// // @ts-ignore 根据碰撞调整位置
// const capsuleInfo = player.capsuleInfo;
// this.tempBox.makeEmpty();
// this.tempMat.copy((this.collider as THREE.Mesh).matrixWorld).invert();
// this.tempSegment.copy(capsuleInfo.segment);
//
// // 获得胶囊在碰撞器的局部空间中的位置
// this.tempSegment.start.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
// this.tempSegment.end.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
//
// // 获取胶囊的轴对齐边界框
// this.tempBox.expandByPoint(this.tempSegment.start);
// this.tempBox.expandByPoint(this.tempSegment.end);
// this.tempBox.min.addScalar(-capsuleInfo.radius);
// this.tempBox.max.addScalar(capsuleInfo.radius);
//
// this.collider?.geometry.boundsTree?.shapecast({
// intersectsBounds: box => box.intersectsBox(this.tempBox),
// intersectsTriangle: tri => {
// // 检查三角形是否与胶囊相交,如果相交则调整胶囊位置。
// const triPoint = this.tempVector;
// const capsulePoint = this.tempVector2;
//
// const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
// if (distance < (this.player as THREE.Object3D).capsuleInfo.radius) {
// const depth = (this.player as THREE.Object3D).capsuleInfo.radius - distance;
// const direction = capsulePoint.sub(triPoint).normalize();
//
// this.tempSegment.start.addScaledVector(direction, depth);
// this.tempSegment.end.addScaledVector(direction, depth);
// }
//
// return false;
// }
// });
//
// if(this.viewer.modules.tilesManage.tilesMap.size > 0){
// this.viewer.modules.tilesManage.mergeMesh?.geometry.boundsTree?.shapecast({
// intersectsBounds: box => box.intersectsBox(this.tempBox),
// intersectsTriangle: tri => {
// // 检查三角形是否与胶囊相交,如果相交则调整胶囊位置。
// const triPoint = this.tempVector;
// const capsulePoint = this.tempVector2;
//
// const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
// if (distance < (this.player as THREE.Object3D).capsuleInfo.radius) {
// const depth = (this.player as THREE.Object3D).capsuleInfo.radius - distance;
// const direction = capsulePoint.sub(triPoint).normalize();
//
// this.tempSegment.start.addScaledVector(direction, depth);
// this.tempSegment.end.addScaledVector(direction, depth);
// }
//
// return false;
// }
// });
// }
//
// // 在检查三角形碰撞并移动后,获得胶囊碰撞器在世界空间中的调整位置。假设capsule.info.segment.start是玩家模型的原点。
// const newPosition = this.tempVector;
// newPosition.copy(this.tempSegment.start).applyMatrix4((this.collider as THREE.Mesh).matrixWorld);
//
// // 检查碰撞器移动了多少
// const deltaVector = this.tempVector2;
// deltaVector.subVectors(newPosition, player.position);
//
// // 如果玩家主要是垂直调整,我们就会认为它是在地面上
// this.playerIsOnGround = deltaVector.y > Math.abs(delta * this.playerVelocity.y * 0.25);
//
// const offset = Math.max(0.0, deltaVector.length() - 1e-5);
// deltaVector.normalize().multiplyScalar(offset);
//
// // 调整玩家模型的位置;
// player.position.add(deltaVector);
// if (!this.playerIsOnGround) {
// deltaVector.normalize();
// this.playerVelocity.addScaledVector(deltaVector, -deltaVector.dot(this.playerVelocity));
// } else {
// this.playerVelocity.set(0, 0, 0);
// }
//
// // 调整相机
// const v = new THREE.Vector3(player.position.x, player.position.y + 0.2, player.position.z);
// const _target = new THREE.Vector3();
// this.controls.getTarget(_target);
// this.viewer.camera.position.sub(_target);
// this.controls.setTarget(v.x, v.y, v.z, false);
// this.controls.distance = this.firstPerson ? 0.8 : 6;
// this.viewer.camera.position.add(v);
// this.controls.polarAngle = Math.PI / 2;
//
// if (this.person) {
// const p = player.position.clone();
// this.person.position.set(p.x, p.y - 1.415, p.z);
// }
//
// //如果玩家跌得太低,将他们的位置重置到起点
// if (this.viewer.sceneBox3 && (this.viewer.sceneBox3.min.y - player.position.y > 15)) {
// this.resetPlayer();
// }
//
// this.personStatus?.update(delta);
// }
dispose() {
window.removeEventListener('keydown', keyDownFn);
window.removeEventListener('keyup', keyUpFn);
App.removeObject(this.group);
this.personStatus?.dispose();
}
}
export {Roaming}