three.js Capsule 胶囊碰撞体积类:从 API 到 Octree 碰撞检测的完整解析
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
Capsule是 three.js 提供的胶囊形碰撞体积(bounding volume)类,由一段线段(start到end)加上半径radius完全定义,等价于一个沿线段扫掠的球体。它常被用作角色控制器、物理近似碰撞等场景中的检测体,与 AABB(轴对齐包围盒)和包围球并列为最常用的三种包围体积之一。读完本文,你将掌握 Capsule 类 的完整 API 与默认值、intersectsBox等方法的底层实现原理,并能参照官方 FPS 示例把它接入基于 Octree 的场景碰撞检测。
需要特别说明:本文讨论的是examples/jsm/math/Capsule.js中的数学类Capsule,它不是用于渲染的几何体。three.js 中另有一个 CapsuleGeometry,用于生成可见的胶囊网格(参数为radius、height、capSegments、radialSegments等),二者命名相近但用途完全不同:前者是碰撞/数学对象,后者是渲染对象。
导入方式
Capsule属于 Addons(three/addons命名空间下的扩展模块),不在three核心包内,必须显式导入:
import { Capsule } from 'three/addons/math/Capsule.js';该文件在源码中以具名导出方式提供(见 examples/jsm/math/Capsule.js 的export { Capsule }),并被 Addons.js 统一聚合导出,因此也可以从three/addons总入口按需引入。
构造函数与属性
new Capsule( start, end, radius )
三个参数在源码中都有默认值(见 构造函数定义):
| 参数 | 类型 | 含义 | 默认值 |
|---|---|---|---|
start | Vector3 | 胶囊线段起点 | new Vector3(0, 0, 0) |
end | Vector3 | 胶囊线段终点 | new Vector3(0, 1, 0) |
radius | number | 胶囊半径 | 1 |
即不带参数直接new Capsule()时,会得到一个起点在原点、终点在(0, 1, 0)、半径为 1 的竖直胶囊。
属性
三个属性均为公共成员,可直接读写:
.start : Vector3— 起点向量;.end : Vector3— 终点向量;.radius : number— 半径,默认1。
注意start和end存储的是传入的Vector3对象引用本身(构造器直接this.start = start赋值,见 源码)。如果多个 Capsule 实例共用同一个向量对象,修改其中一个会互相影响;需要独立副本时应先clone()或使用set()/copy()。
方法详解
.set( start, end, radius )
将胶囊各分量设置为给定值。源码实现(set 方法):
set( start, end, radius ) { this.start.copy( start ); this.end.copy( end ); this.radius = radius; return this; }这里的关键点是:它只复制传入向量上的数值,不会替换this.start/this.end的引用(官方文档原文也提示 "this method only copies the values from the given objects")。因此set()可以安全地用任意临时向量更新一个已有实例,而不会改变实例内部持有的向量对象。返回值是this,支持链式调用。
.copy( capsule ) / .clone()
copy把一个 Capsule 的start、end、radius全部拷入当前实例并返回this;clone则通过new this.constructor().copy( this )创建新实例(源码)。这两个方法同样采用"拷贝数值、保留自身引用"的语义。
.getCenter( target )
返回胶囊中心点,即起点与终点的中点,结果写入传入的target向量:
getCenter( target ) { return target.copy( this.end ).add( this.start ).multiplyScalar( 0.5 ); }该写法完全复用target,不产生中间临时对象,适合放在每帧执行的循环里调用。
.translate( v )
把偏移向量v同时加到start和end上,相当于在 3D 空间中平移整个胶囊(源码)。这是碰撞响应中推动胶囊的主要手段,官方 FPS 示例中每帧用它应用位移、并沿法线推出穿透深度(见下文实战部分)。
.intersectsBox( box )
判断给定Box3包围盒是否与胶囊相交,返回布尔值。这是整个类中最有实现含量的方法,也是它接入 Octree 加速结构的基础。
intersectsBox 的底层实现:三平面投影的保守相交测试
从源码结构看,intersectsBox并不是精确的胶囊—包围盒几何求交,而是一个保守(conservative)快速测试:
intersectsBox( box ) { return ( checkAABBAxis( this.start.x, this.start.y, this.end.x, this.end.y, box.min.x, box.max.x, box.min.y, box.max.y, this.radius ) && checkAABBAxis( this.start.x, this.start.z, this.end.x, this.end.z, box.min.x, box.max.x, box.min.z, box.max.z, this.radius ) && checkAABBAxis( this.start.y, this.start.z, this.end.y, this.end.z, box.min.y, box.max.y, box.min.z, box.max.z, this.radius ) ); }核心辅助函数 checkAABBAxis:
function checkAABBAxis( p1x, p1y, p2x, p2y, minx, maxx, miny, maxy, radius ) { return ( ( minx - p1x < radius || minx - p2x < radius ) && ( p1x - maxx < radius || p2x - maxx < radius ) && ( miny - p1y < radius || miny - p2y < radius ) && ( p1y - maxy < radius || p2y - maxy < radius ) ); }其原理是:把胶囊的轴线段投影到XY、XZ、YZ 三个坐标平面,在每个 2D 平面上判断"线段两端点之一是否落在包围盒对应边界的radius距离之内"。只要有一个投影平面上线段完全位于盒子某条边的外侧且超出半径,即可断言不相交(分离轴思想);三个平面都不分离时返回true。
这种设计带来两个实践含义:
- 只会"多报"不会"漏报":它用于剔除确定不相交的分支是安全的,但返回
true只表示"可能相交"。因此它天然适合作为包围体粗筛(broad phase),后续需要精确结果时应再与三角形等细节几何做细筛; - 实现非常廉价:整个测试只有加减法与比较,没有任何开方、叉乘或内存分配,这正是它能在 Octree 递归遍历中被高频调用的原因。
与 Octree 的协作:场景碰撞检测的完整调用链
three.js 的 Octree 扩展 是Capsule最主要的下游消费者,其碰撞流程清晰展示了上述方法如何组合使用:
- 粗筛收集:
getCapsuleTriangles递归遍历八叉树,每访问一个子树前先用capsule.intersectsBox( subTree.box )剔除不相交的子树——这正是上一节保守测试的用途; - 精确求交:对筛出的每个三角形执行
triangleCapsuleIntersect,它先做三角形平面的快速排斥(两端点到平面距离与半径比较),再分别检测轴线与三角形面的包含关系、轴线与三条边的最近点对距离,返回{ normal, point, depth }形式的碰撞信息; - 迭代推出:
capsuleIntersect用_capsule.copy( capsule )在副本上循环调用求交,每命中一次就_capsule.translate( result.normal.multiplyScalar( result.depth ) )沿法线推出穿透深度,直到不再相交,最后返回整体法线与总深度。
也就是说,Capsule提供"定义 + 平移 + 包围盒粗筛",Octree 负责空间索引与精确三角形碰撞,两者配合就构成了一套完整的胶囊—三角网格碰撞方案。
实战:官方 FPS 示例中的玩家碰撞体
examples/games_fps.html 提供了一个完整的可运行参考。其玩家碰撞体定义如下(第 121 行):
const playerCollider = new Capsule( new THREE.Vector3( 0, 0.35, 0 ), new THREE.Vector3( 0, 1, 0 ), 0.35 );即从脚底上方 0.35 到头部 1.0 的竖直线段、半径 0.35 的胶囊——注意线段端点距离(0.65)小于两半球的直径,因此两端的半球会有重叠,胶囊总高度为0.65 + 2 × 0.35 = 1.35,恰好覆盖角色全身。每帧的更新与碰撞逻辑(playerCollisions / updatePlayer):
function playerCollisions() { const result = worldOctree.capsuleIntersect( playerCollider ); playerOnFloor = false; if ( result ) { // 法线接近竖直(>= 0.15)视为可站立的地面 playerOnFloor = result.normal.y >= 0.15; if ( ! playerOnFloor ) { // 墙/障碍:去掉沿法线的速度分量(滑动而非停下) playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity ) ); } if ( result.depth >= 1e-10 ) { // 沿法线推出穿透深度 playerCollider.translate( result.normal.multiplyScalar( result.depth ) ); } } } function updatePlayer( deltaTime ) { // 重力、阻尼…… const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime ); playerCollider.translate( deltaPosition ); // 应用本帧位移 playerCollisions(); camera.position.copy( playerCollider.end ); // 相机跟随胶囊顶端(头部) }这段代码集中体现了Capsule三类 API 的典型用法:translate驱动运动与碰撞响应、end作为相机锚点、intersectsBox在 Octree 内部完成子树剔除。此外示例还演示了角色与球形道具的碰撞近似——把胶囊拆成start、end和中点三个球体分别做球—球测试(playerSphereCollision),说明在胶囊类之外,start/end/radius三个数据也可以被当作自定义碰撞算法的输入。
小结与适用前提
Capsule是纯数学类,不依赖渲染器,适用于角色控制器、物理引擎桥接、拾取/阻挡等逻辑层;intersectsBox是保守快速测试,只宜用于剔除与粗筛;精确碰撞应交给 Octree 的三角形求交或外部物理引擎;start/end保存的是传入向量的引用,共享向量需谨慎;更新数值请使用set/copy,移动位置请使用translate;- 需要可视化胶囊外观时,请使用独立的 CapsuleGeometry 构建网格,其构造参数(
radius、height、capSegments、radialSegments、heightSegments)与Capsule类无关。
相关文档与源码位置:docs/pages/Capsule.html.md(官方 API 文档)、examples/jsm/math/Capsule.js(类实现)、examples/jsm/math/Octree.js(碰撞调用方)、examples/games_fps.html(完整实战示例)。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考