当前位置: 首页 > 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.xdnf.cn/news/1096255.html

相关文章:

  • 论文笔记(LLM distillation):Distilling Step-by-Step!
  • MiniGPT4源码拆解——models
  • 原生微信小程序研发,如何对图片进行统一管理?
  • 微信小程序101~110
  • UnrealEngine5游戏引擎实践(C++)
  • Android Coil 3 data加载图的Bitmap或ByteArray数据类型,Kotlin
  • Android 如何阻止应用自升级
  • C语言 | 函数核心机制深度解构:从底层架构到工程化实践
  • Matplotlib 全面使用指南 -- 自动缩放坐标轴 Autoscaling Axis
  • 【Linux】Linux 操作系统 - 27 , 进程间通信(三) --System V 共享内存
  • 编写bat文件自动打开chrome浏览器,并通过selenium抓取浏览器操作chrome
  • 抽象类基础知识
  • 如何选择合适的ai降重工具?七个实用的ai查重网站
  • 【会员专享数据】2013-2024年我国省市县三级逐日SO₂数值数据(Shp/Excel格式)
  • 告别繁琐:API全生命周期管理的新范式——apiSQL
  • 调用京东API接口时,如果超过了调用频率限制,应该如何处理?【项目经验分享】
  • Django+DRF 实战:自定义异常处理流程
  • FeatherScan v4.0 – 适用于Linux的全自动内网信息收集工具
  • 快速搭建服务器,fetch请求从服务器获取数据
  • linux网络编程之读缓冲区设计
  • 系统性部署系统母盘【rhel7和rhel9】
  • 腾讯云分为几个区域
  • 2025社交电商新风口:推客小程序的商业逻辑与技术实现
  • 以太网基础⑤UDP 协议原理与 FPGA 实现
  • 《信号与系统》学习笔记——第八章(补充部分)
  • 分库分表之实战-sharding-JDBC分库分表执行流程原理剖析
  • 【算法笔记】6.LeetCode-Hot100-链表专项
  • VTK 9.0中的屏幕空间环境光遮挡
  • 【Android】在平板上实现Rs485的数据通讯
  • 【Docker基础】Docker容器与网络关联命令使用指南:深入理解容器网络连接