简介
通过three.js我们可以在网站中实现比较不错的3D效果,但three.js仅仅只是一个渲染库,如果想要做碰撞检测的话,通常情况会加上物理系统。
但今天,我想聊的是通过three.js的内置模块Octree八叉树来实现第一人称的碰撞效果。
咱们先来看看实现的效果吧,效果如下图:
还感兴趣的话,可以进入示例Demo里面体验一下,需注意模型资源有点大,加载需要等待一段时间。好了,现在咱们就来聊聊一些实现细节。
Octree-八叉树
八叉树碰撞的检测原理我也不怎么清楚,感兴趣的可以自行百度搜索了解。
我们主要是运用three.js封装好的内置Octree模块,使用该模块只需要通过如下引入方式即可:
import { Octree } from 'three/examples/jsm/math/Octree';
this.octree = new Octree();
引入该模块后,我们可以通过fromGraphNode
为需要的场景构建节点,通过构建好的节点我们就可以实现碰撞等操作,代码很简单,如下:
const model = this.resource.models.find(item => item.userData.name === 'modular_dungeon') as GLTF; // 这是我加载的一个模型,不限于任何方式,只需要获取加载好的模型即可
this.octree.fromGraphNode(model.scene); // 通过Octree对象来构建节点
this.scene.add(model.scene); // 将模型添加到场景中
通过以上的简单几个步骤,我们就为碰撞检测的前提条件做好了准备,现在我们需要一个碰撞对象。
在three.js中,我们可以通过添加一个Capsule
对象来实现碰撞检测。
Capsule-胶囊体
为什么要使用Capsule
对象呢? 因为,在Octree
中提供了Capsule
对象碰撞的方法,让我们可以直接使用来更容易的实现碰撞检测。
import { Capsule } from 'three/examples/jsm/math/Capsule';
通过以上方式我们引入一个Capsule
对象,注意该对象并不是一个几何体。我的意思是,它并不会在场景中存在,它仅仅用于我们碰撞检测的一个数学对象。
如果你想知道这个对象的实际样子的话,可以引入对应的CapsuleGeometry
来看一看,大致如下:
好了,现在一切都准备好了,我们开始进行我们的核心问题之一——碰撞检测。
碰撞检测
在Octree
对象中,我们可以通过capsuleIntersect
方法来捕获Capsule
胶囊体与所构建了八叉树节点的场景是否进行了碰撞,检测方式如下:
const result = world.hall.octree.capsuleIntersect(this.capsule);
没错,非常非常非常的简单,只需要一个方法我们就知道了Capsule
对象是否与场景进行了碰撞,当发生了碰撞后将会把碰撞的信息通过返回值返回给我们,上面我使用了result
属性来存储。
现在,我们来说一说这个result
属性吧,该属性是一个对象,一共包含了两个值,分别是:
- depth: 碰撞的深度,可以理解为物体和场景中相机的比例
- normal:碰撞的法线向量,可以理解为碰撞的方向
或许有一点不大明白,我尝试用一张拙劣的画图来表示一下:
知道了这些信息后,我们就可以很好很简单的处理碰撞的逻辑了,我们只需要如下操作:
当Capsule
对象与场景物体碰撞后,将depth
与normal
法线向量相乘,得到一个新的数值。
这个数值就是我们需要将Capsule
对象偏移的值,偏移该值后Capsule
对象也就不再与场景物体相碰撞了。
const result = world.hall.octree.capsuleIntersect(this.capsule);
if (result) {
const { normal, depth } = result;
this.capsule.translate(normal.multiplyScalar(depth));
}
上面的translate
方法接收一个Vector3
对象,表示想较与当前的位置,进行一个偏移。
同步
注意,我们现在进行的一切操作,在场景中都是得不到体现的!
尽管,我们可能会在不断的移动逻辑中通过translate
修改Capsule
的位置,又通过capsuleIntersect
检测来修复Capsule
的位置,但这一切都只是一个数学上的运算。
所以,我们需要将Capsule
对象的信息同步到场景中的物体上,可以是一个简单的几何体、也可以是一个模型、当然也可以是拍摄的相机。
PointerLockControls控件
例如,这里我介绍PointerLockControls
控件,通过该控件可以比较方便实现第一人称的一个操作方式,使用方式如下:
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls';
document.addEventListener('mouseup', () => {
if (!this.controls.isLocked) {
this.controls.lock(); // 执行该方法后,鼠标将无法控制,转而控制摄像机的镜头,实现第一人称的操作
}
});
通过PointerLockControls
控件再配合Capsule
对象来同步摄像机的位置,我们就可以比较容易的实现一个第一人称的碰撞逻辑了。
结束
篇幅可能有点长了,所以更多一些没有用的细节不会再去介绍了,若有兴趣可以参考源码仓库。
参考:three.js官方fps案例