当前位置: 首页 > news >正文

【threejs】第一人称视角之八叉树碰撞检测

目录

  • 引言
  • 基本概念和原理
  • 实现过程
  • 总结
  • 参考

引言

在游戏开发、3D 仿真和物理引擎中,碰撞检测(Collision Detection)是一个核心问题。当场景中有成千上万的物体时,如何高效判断“谁撞上了谁”?如果简单粗暴地遍历所有物体两两检测,计算复杂度会高达 O(n²),性能直接爆炸!💥

这时,八叉树(Octree) 闪亮登场✨——它通过 空间分割 技术,将 3D 世界递归划分成小块,只检测 可能发生碰撞的物体,让计算复杂度骤降至 O(n log n),甚至更低!

本文将带你深入八叉树的原理,手把手实现一个高效的碰撞检测系统!

基本概念和原理

  1. 相机控制系统

(1)相机类型选择:
PerspectiveCamera(透视相机)适合第一/第三人称视角,参数:fov, aspect, near, far

  // 相机(透视)const camera = new THREE.PerspectiveCamera(75, //视角window.innerWidth / window.innerHeight, //aspect视锥长宽比0.1, //near10000 //far);camera.rotation.order = "YXZ"; //默认旋转顺序是 'XYZ',设置相机旋转的顺序的属性。这个属性指定了欧拉角(Euler angles)的旋转顺序camera.lookAt(0, 0, 0);camera.position.set(0, 1, 5);scene.add(camera);

(2)相机控制器:
PointerLockControls(指针锁定控制器) 精准的鼠标输入(无加速/边界限制),完全的移动逻辑控制权(可插入碰撞检测),更低的性能开销(无内置惯性计算),若项目需要真实的物理碰撞或竞技级FPS体验,自定义 PointerLockControls 是唯一选择

为什么选择 PointerLockControls实现第一人称视角碰撞检测而不是直接使用FirstPersonControls?
①PointerLockControls直接捕获鼠标输入,消除光标移动范围限制,实现无间断的视角旋转(适合FPS游戏),而FirstPersonControls依赖鼠标相对移动事件,无法完全隐藏系统光标,降低沉浸感。
②PointerLockControls与物理引擎/碰撞检测无缝集成,可自由扩展 update 逻辑,在每一帧计算移动前先检测碰撞(如射线检测或物理引擎查询)。FirstPersonControls 的封闭性,移动逻辑内置且不可干预,无法关闭自动的水平矫正(不适合需要自由旋转的场景)强制覆盖其 update 方法,可能破坏内部状态机。

需求PointerLockControlsFirstPersonControlsOrbitControls
核心用途FPS游戏/仿真简易第一人称浏览3D模型观察/场景调试
鼠标控制✅ 锁定指针,无光标干扰⚠️ 受系统光标限制,无法实现无光标✅ 自由旋转/缩放(光标可见)
键盘控制需手动添加键盘移动和鼠标旋转默认支持WASD移动和鼠标旋转键盘只能控制左右俯仰,鼠标左点击旋转,右点击拖拽,围绕目标物体,target只能在一个小区域
视角限制✅ 可限制俯仰角(如±90°)⚠️ 固定限制✅ 可限制旋转范围/缩放距离
物理引擎集成✅ 直接同步物理体位置❌ 难以与物理体同步❌ 完全独立,无物理交互
自定义碰撞响应✅ 自由扩展检测逻辑❌ 移动逻辑不可干预❌ 固定交互逻辑
移动平滑性⚠️ 需手动实现阻尼✅ 内置惯性/阻尼✅ 内置平滑旋转/缩放
UI兼容性❌ 需额外处理UI交互(指针锁定)⚠️ 需隐藏光标✅ 完美兼容UI(光标自由移动)
典型场景FPS射击游戏,VR行走模拟3D博物馆浏览,简单场景漫游模型展示,开发者调试场景
  1. 射线(Raycaster)

Raycaster 是用于 射线检测(Raycasting) 的核心类,其本质是从 3D 空间中的一个点向特定方向发射一条无限延伸的虚拟射线,检测该射线与场景中物体的交点。六个核心使用场景第一人称视角的碰撞检测、鼠标拾取(3D物体选择)、地面高度检测(角色站立/楼梯攀爬)、武器子弹命中检测、视线检测(AI敌人感知)、动态遮挡剔除(性能优化)

	//射线由 起点(origin) 和 方向(direction) 定义const raycaster = new THREE.Raycaster(origin, direction);const intersects = raycaster.intersectObjects(this.scene.children); //返回交叉部分数组[ { distance, point, face, faceIndex, object }, ... ]/*distance —— 射线投射原点和相交部分之间的距离。point —— 相交部分的点(世界坐标)face —— 相交的面faceIndex —— 相交的面的索引object —— 相交的物体uv —— 相交部分的点的UV坐标。uv1 —— 相交部分的点的第二组UV坐标normal - 交点处的内插法向量instanceId – 与InstancedMesh物体相交时的instance索引*/
  1. 八叉树(Octree)

是一种 空间分割数据结构,用于高效管理 3D 空间中的物体。它通过递归地将立方体空间划分为 8 个子立方体(称为“节点”或“象限”),每个子立方体可继续分割,直到满足终止条件(如深度限制或物体数量阈值)。

  • 分层结构:树状组织,根节点代表整个空间,叶节点存储实际物体。
  • 动态适应:根据物体分布自动调整分割粒度。
  • 快速查询:利用空间位置跳过无关区域,优化碰撞检测、射线检测等操作。

为什么用八叉树?
在 3D 场景中,直接遍历所有物体进行碰撞检测的复杂度为 O(n²),而八叉树可将其降至 O(n log n) 或更低。典型应用场景包括:
①碰撞检测:快速筛选可能相交的物体对。
②射线检测:仅检测射线途径的节点内的物体。
③视锥剔除:只渲染相机可见区域的物体。
④动态场景管理:如游戏中的粒子系统、物理引擎。

(2)胶囊体(Capsule)
本质是碰撞几何体,由两个半球和一个圆柱组成的数学模型,用于简化角色或物体的碰撞形状。用于替代复杂网格碰撞体,提供更高效且自然的碰撞检测(尤其适合角色控制器)

  1. 平滑移动余阻尼

使用线性插值(LERP)实现平滑过渡

const currentPosition = new THREE.Vector3().copy(startPosition);
const lerpFactor = 0.1; // 插值系数 (0~1,值越大过渡越快)
currentPosition.lerp(targetPosition, lerpFactor);

应用缓动函数(easing functions)改善手感

let damping = Math.exp(-4 * deltaTime) - 1; //阻尼,随着deltaTime指数增加damping越小(减去 1。这可能是为了调整阻尼值的范围)
if (!this.onFloor) {this.playerVelocity.y -= this.gravity * deltaTime;damping *= 0.1;
}
this.playerVelocity.addScaledVector(this.playerVelocity, damping);
const deltaPosition = this.playerVelocity.clone().multiplyScalar(deltaTime);
this.capsule.translate(deltaPosition);

实现过程

  1. 加载模型和胶囊把场景分解成一些节点 this.octree.fromGraphNode(this.modelObj)
    // 加载模型,并渲染到画布上loadGLTF(this.modelUrl).then((object: any) => {this.modelObj = object.scene;console.log(this.modelObj); // 返回组对象Groupthis.scene.add(this.modelObj);// 遍历场景中的所有几何体数据this.modelObj.traverse((child: any) => {if (child.isMesh) {child.castShadow = true;child.receiveShadow = true;}});//八叉树this.octree = new Octree();this.octree.fromGraphNode(this.modelObj); // 通过Octree对象来构建节点// OctreeHelper// const helper = new OctreeHelper(this.octree);// helper.visible = true;// this.scene.add(helper);});
  1. 把胶囊体的位置传给网格对象,进行运动交互
  //player类中的部分方法init() {//胶囊体,用于碰撞检测,Capsule不是一个几何体//this.capsule位置方向大小设置很重要,this.height要将其底部与场景中其他几何体的基准线对齐this.capsule = new Capsule(new THREE.Vector3(0, this.radius, 0), //第一个端点new THREE.Vector3(0, this.height + this.radius, 0), //第二个端点this.radius //半径);this.mesh = new THREE.Mesh(new THREE.CapsuleGeometry(this.radius, this.height),new THREE.MeshNormalMaterial());this.mesh.rotation.order = "YXZ";this.scene.add(this.mesh);this.sync();this.addkeyBoard();}sync() {const end = this.capsule.end.clone();end.y -= this.radius;this.mesh.position.copy(end);}
  1. 进行碰撞检测,模拟物理效果

在Octree对象中,我们可以通过capsuleIntersect方法来捕获Capsule胶囊体与所构建了八叉树节点的场景是否进行了碰撞,检测方式如下:const result = this.octree.capsuleIntersect(this.capsule);

  • depth: 碰撞的深度,可以理解为物体和场景中相机的比例
  • normal:碰撞的法线向量,可以理解为碰撞的方向
  handleCollider() {//检查场景空间和胶囊的碰撞const result = this.octree.capsuleIntersect(this.capsule);this.onFloor = false;if (result) {const { normal, depth } = result;this.onFloor = normal.y > 0;if (!this.onFloor) {this.speedVel.addScaledVector(result.normal, -result.normal.dot(this.speedVel));} else {this.time = 0;this.speedVel.y = 0;}this.capsule.translate(normal.multiplyScalar(depth));//实现不同平面的行走,镜头可以向下或向上移动一定距离}}
  1. 移动镜头,通过键盘和鼠标操控镜头移动旋转实现浏览场景的基本操作

(1)PointerLockControls指针控制器+鼠标控制旋转视角

// 添加相机控件-指针
this.controls = new PointerLockControls(this.camera, this.canvas);
this.controls.lock();  // 锁定鼠标到画布,隐藏光标, 注:Tween操作需要在this.controls.lock()之前
this.controls.unlock();  //释放鼠标,恢复光标//鼠标控制
addMouseEvent() {let mouseX;let mouseY;document.onmousedown = (event) => {event.preventDefault();mouseX = event.pageX;mouseY = event.pageY;};document.onmousemove = (event) => {libraryState.isDraging = true;event.preventDefault();if (mouseX && mouseY) {var deltaX = event.pageX - mouseX;var deltaY = event.pageY - mouseY;mouseX = event.pageX;mouseY = event.pageY;// 根据触摸事件的移动量调整相机的角度this.camera.rotation.y -= deltaX * 0.003; //左右旋转this.camera.rotation.x -= deltaY * 0.003; //俯仰旋转}};document.onmouseup = (event) => {event.preventDefault();if (libraryState.viewing) return;mouseX = null;mouseY = null;};}

(2)键盘事件移动方向

在requestAnimationFrame方法种执行keyControls和updatePlayer

监听键盘事件修改方向向量playerVelocity → 根据碰撞检测handleCollider计算出胶囊体capsule最新位置 → 同步更新胶囊和相机位置

  keyControls(deltaTime) {const speedDelta = deltaTime * (this.onFloor ? 25 : 8);if (this.keyStates["KeyW"]) {this.playerVelocity.add(this.getForwardVector().multiplyScalar(speedDelta));}if (this.keyStates["KeyS"]) {this.playerVelocity.add(this.getForwardVector().multiplyScalar(-speedDelta));}if (this.keyStates["KeyA"]) {this.playerVelocity.add(this.getSideVector().multiplyScalar(-speedDelta));}if (this.keyStates["KeyD"]) {this.playerVelocity.add(this.getSideVector().multiplyScalar(speedDelta));}if (this.onFloor) {if (this.keyStates["Space"]) {this.playerVelocity.y = 5;}}}async updatePlayer(deltaTime: number) {let damping = Math.exp(-4 * deltaTime) - 1;//随着deltaTime指数增加damping越小(减去 1。这可能是为了调整阻尼值的范围)if (!this.onFloor) {this.playerVelocity.y -= this.gravity * deltaTime;damping *= 0.1;}this.playerVelocity.addScaledVector(this.playerVelocity, damping);const deltaPosition = this.playerVelocity.clone().multiplyScalar(deltaTime);this.capsule.translate(deltaPosition);this.handleCollider(); //碰撞检测this.sync(); //同步mesh胶囊this.check(); //回归中心点// 同步到缩略图上this.handleMiniMapMove();this.handleMiniMapRoate();}sync() {// 同步胶囊和相机位置const end = this.capsule.end.clone();// end.y -= this.radiusthis.mesh.position.copy(end);this.camera.position.copy(end);}

效果图如下:
请添加图片描述

总结

八叉树是一种高效空间索引工具,减少需处理的物体数量,适合动态场景的碰撞检测。胶囊体比复杂网格的碰撞计算快10-100倍,胶囊体+射线组 平衡精度与性能,是角色控制的黄金组合。简单场景用纯八叉树,复杂交互需集成 Cannon.js。

参考

  1. 基于three.js实现第一人称的碰撞检测
  2. threejs官方fps示例
http://www.dtcms.com/a/272691.html

相关文章:

  • 蜻蜓I即时通讯系统重构宣言:破茧重生的技术革命-长痛不如短痛卓伊凡|麻子|果果
  • 大健康IP如何借“合规创新”抢占行业新风口|创客匠人
  • 解读 Go 中的 constraints包
  • 【TCP/IP】7. IP 路由
  • xml 知识总结: xsd,xsi:schemaLocation,xmlns,xmlns:xsi
  • SpringBoot系列—MyBatis(xml使用)
  • codeforeces Round1032 - Round 1036
  • 【node后端】搭建项目(Express+Ts+Typeorm+Mysql一步到位)
  • 深入浅出 Python Asynchronous I/O:从 asyncio 入门到实战
  • Arc Institute提出首个AIVC虚拟细胞模型STATE
  • 上海交大医学院张维拓老师赴同济医院做R语言训练营培训
  • 从Debug中学习MiniGPT4
  • 在Vue中如何对组件进行销毁在进行挂载
  • 模型训练之数据标注-Labelme的使用教程
  • 5款工具高效制作插图,PPT设计新选择!
  • 货车车架和悬架设计cad【7张】+设计说明书
  • leetcode 3440. 重新安排会议得到最多空余时间 II 中等
  • 《PyQt6-3D:开启Python 3D编程新世界 2》
  • 【TCP/IP】8. 传输层协议
  • hive小文件问题
  • 二层环路避免-STP技术
  • Linux【大数据运维】下制作Redis绿色免安装包(一)
  • 企业网络安全的“金字塔”策略:构建全方位防护体系的核心思路
  • upload-labs靶场通关详解:第20关 /.绕过
  • 以下哪种类型在Golang中不是内置类型?
  • zookeeper etcd区别
  • Keepalived+LVS实现LNMP网站的高可用部署
  • 登录为图片验证时,selenium通过token直接进入页面操作
  • Java 导出word 实现饼状图导出--可编辑数据
  • CIEDE2000 色差公式C++及MATLAB实现