Toggle navigation
集客麦麦@谢坤
首页
随笔
首页
>>
创作中心
>>
Cannon.js ...
Cannon.js 中碰撞分组(`collisionFilterGroup` 和 `collisionFilterMask`)原理
Cannon.js 中碰撞分组(`collisionFilterGroup` 和 `collisionFilterMask`)的实际应用,通过位掩码实现了不同物体间的碰撞过滤逻辑。以下是核心知识点整理: [TOC] ### **1. 碰撞分组的定义方式** 代码中使用**位左移运算**定义了 4 个碰撞组,每个组对应二进制的一位,确保组之间互不干扰: ```javascript // 每个组的值是 2 的幂(通过位左移 1 << n 实现) const groupGround = 1 << 1; // 地面组(二进制 10,十进制 2) const groupA = 1 << 2; // 刚体组A(二进制 100,十进制 4) const groupB = 1 << 3; // 刚体组B(二进制 1000,十进制 8) const groupC = 1 << 4; // 刚体组C(二进制 10000,十进制 16) ``` - 位左移 `1 << n` 确保每个组的值是唯一的 2 的幂,便于后续通过位运算组合或判断。 ### **2. 碰撞组(collisionFilterGroup)的作用** 标识当前物体**属于哪个组**,用于告知其他物体“我是谁”。代码中不同物体的分组设置: - **地面**:属于 `groupGround` ```javascript groundBody.collisionFilterGroup = groupGround; ``` - **Box 隔板**:分别属于 `groupA`/`groupB`/`groupC` ```javascript boxBody.collisionFilterGroup = group; // group 为传入的 groupA/B/C ``` - **球体**:未显式设置(默认属于 `DEFAULT` 组,但通过掩码控制碰撞对象)。 ### **3. 碰撞掩码(collisionFilterMask)的作用** 定义当前物体**允许与哪些组碰撞**,用于告知其他物体“我可以和谁交互”。代码中球体通过掩码控制碰撞范围: ```javascript // 球体的碰撞掩码设置(允许与指定组碰撞) createSphere({ // ... group: groupGround, // 只与地面碰撞 }); createSphere({ // ... group: groupGround | groupA, // 与地面和 groupA 碰撞(通过 | 组合多个组) }); ``` - 掩码使用 `|` 运算组合多个组,表示“允许与这些组中的任意一个碰撞”。 ### **4. 碰撞检测的逻辑规则** 两个物体 A 和 B 发生碰撞的条件: `(A.group & B.mask) !== 0 && (B.group & A.mask) !== 0` 以代码场景为例: - **红色球体(掩码为 `groupGround`)**: 只能与地面(`groupGround`)碰撞,会穿过所有 Box 隔板(因 Box 属于 `groupA/B/C`,与球体掩码无交集)。 - **绿色球体(掩码为 `groupGround | groupB`)**: 可与地面和 `groupB` 隔板碰撞,会被 `groupB` 隔板阻挡,但穿过 `groupA` 和 `groupC` 隔板。 - **Box 隔板(如 `groupA`)**: 仅会与掩码中包含 `groupA` 的物体碰撞(如第二个红色球体)。 ### **5. 实际效果总结** | 球体 | 碰撞掩码(允许的组) | 碰撞行为 | |---------------------|---------------------------|-------------------------------------------| | 第一个红色球体 | `groupGround` | 仅与地面碰撞,穿过所有 Box 隔板 | | 第二个红色球体 | `groupGround | groupA` | 与地面和 `groupA` 隔板碰撞,被 `groupA` 阻挡 | | 绿色球体 | `groupGround | groupB` | 与地面和 `groupB` 隔板碰撞,被 `groupB` 阻挡 | | 蓝色球体 | `groupGround | groupC` | 与地面和 `groupC` 隔板碰撞,被 `groupC` 阻挡 | ### **6. 核心知识点提炼** - **位掩码基础**:用二进制位表示组,`1 << n` 生成唯一组值,`|` 组合多个组。 - **分组与掩码**:`group` 标识自身所属,`mask` 定义可碰撞的组。 - **碰撞条件**:双向判断(A 允许 B 且 B 允许 A),通过 `&` 运算验证交集。 - **灵活性**:通过修改掩码可快速调整物体间的碰撞关系,无需修改其他逻辑。 这种机制高效且灵活,广泛用于游戏中控制角色、道具、地形等不同类型物体的交互规则。 ## 位掩码(bitmask) 实现碰撞过滤原理 在 Cannon.js 中,`collisionFilterGroup` 和 `collisionFilterMask` 基于**位掩码(bitmask)** 实现碰撞过滤,其核心原理是利用二进制位的状态(0 或 1)表示“集合关系”,通过高效的位运算判断两个物体是否需要碰撞。这种设计既简洁又高效,是游戏和物理引擎中广泛使用的碰撞过滤方案。 ### **一、位掩码的核心原理** 位掩码的本质是用**二进制数的每一位**表示一个“开关状态”(是否包含某个元素),通过位运算快速判断两个集合的关系。 #### 1. **二进制位与“组”的对应** Cannon.js 中,每个碰撞组(如“玩家”“敌人”“地面”)被映射到二进制数的**某一位**,位值为 `1` 表示“属于该组”或“允许与该组碰撞”,`0` 表示“不属于”或“不允许”。 例如,假设我们定义 3 个组: - 地面(Ground)→ 第 0 位(二进制 `001`,十进制 `1`) - 玩家(Player)→ 第 1 位(二进制 `010`,十进制 `2`) - 敌人(Enemy)→ 第 2 位(二进制 `100`,十进制 `4`) 这里每个组的值都是 `2^n`(1、2、4、8...),确保每个组在二进制中占据**唯一的位**,互不干扰。 #### 2. **位运算实现集合操作** 通过简单的位运算,可以快速实现“组的组合”和“组的交集判断”: - **OR 运算(|)**:组合多个组(相当于“包含多个元素的集合”) 例:`Player | Enemy` → `010 | 100 = 110`(十进制 6),表示“同时包含玩家和敌人组”。 - **AND 运算(&)**:判断两个集合是否有交集(是否共享某个组) 例:`(Player & Enemy) = 0`(无交集),`(Player & (Player | Enemy)) = 2`(有交集)。 #### 3. **碰撞判断的底层逻辑** 两个物体 A 和 B 会发生碰撞检测,当且仅当: `(A的组 & B的掩码) ≠ 0` **且** `(B的组 & A的掩码) ≠ 0` 用二进制解释: - A 的组(`A.group`)中至少有一位为 1,且这一位在 B 的掩码(`B.mask`)中也为 1(A 允许与 B 的某个组碰撞)。 - 反之,B 的组中至少有一位为 1,且这一位在 A 的掩码中也为 1(B 允许与 A 的某个组碰撞)。 ### **二、为什么要用位掩码设计?** 这种设计被物理引擎广泛采用,核心原因是**高效性**和**简洁性**: #### 1. **计算速度极快** 位运算(AND、OR 等)是计算机硬件直接支持的底层操作,执行速度远快于数组遍历或哈希表查找。例如: - 检查两个物体是否允许碰撞,只需 2 次 AND 运算和 2 次不等于 0 的判断,耗时可忽略。 - 如果用数组存储“允许碰撞的组”,则需要遍历数组对比,复杂度为 O(n),在物体数量多时性能差距明显。 #### 2. **用一个整数表示多个组** 一个 32 位整数可以表示 32 个不同的组(每个位对应一个组),通过 OR 运算可轻松组合多个组。例如: - 十进制 `6`(二进制 `110`)可同时表示“玩家”和“敌人”两个组,无需额外数据结构。 - 相比之下,用数组 `['Player', 'Enemy']` 表示同样的含义,会占用更多内存且操作更繁琐。 #### 3. **逻辑清晰且灵活** 位掩码的规则是统一的: - `collisionFilterGroup`:当前物体“属于哪些组”(用位表示)。 - `collisionFilterMask`:当前物体“允许与哪些组碰撞”(用位表示)。 无论场景多么复杂(如 10 种不同类型的物体),都可以用同一套逻辑处理碰撞关系,无需为特殊情况单独编写判断代码。 ### **三、举例:直观理解位掩码工作流程** 假设场景中有两个物体: - **玩家(Player)**: - 组(group):`Player`(二进制 `010`,十进制 `2`) - 掩码(mask):`Ground | Enemy`(二进制 `101`,十进制 `5`,表示允许与地面和敌人碰撞)。 - **敌人(Enemy)**: - 组(group):`Enemy`(二进制 `100`,十进制 `4`) - 掩码(mask):`Player`(二进制 `010`,十进制 `2`,表示允许与玩家碰撞)。 #### 碰撞判断: 1. 计算 `Player.group & Enemy.mask` → `010 & 010 = 010 ≠ 0`(条件1满足)。 2. 计算 `Enemy.group & Player.mask` → `100 & 101 = 100 ≠ 0`(条件2满足)。 3. 结论:玩家和敌人会发生碰撞检测。 ### **总结** 位掩码通过二进制位表示“组的归属”和“碰撞许可”,利用高效的位运算实现碰撞过滤。这种设计的优势在于: - **性能**:位运算速度远快于其他集合操作方式。 - **简洁**:用一个整数即可表示多个组的组合关系。 - **灵活**:统一的逻辑适用于任意复杂的碰撞场景。 这也是为什么几乎所有物理引擎(包括 Cannon.js、PhysX、Bullet 等)都采用位掩码实现碰撞过滤的核心原因。 ## 示例 ```javascript import * as THREE from 'three' import { OrbitControls } from 'three/addons/controls/OrbitControls.js' import * as CANNON from 'cannon-es' import CannonDebugger from 'cannon-es-debugger' import { addArrowAxesHelper } from '@/assets/utils/threeUtils' import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' import { GUI } from 'three/addons/libs/lil-gui.module.min.js' const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000, ) camera.position.set(13, 29, 117) camera.lookAt(0, 0, 0) const renderer = new THREE.WebGLRenderer() renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement) // 创建物理世界 const world = new CANNON.World() world.gravity.set(0, -9.82, 0) // 设置重力 const objArr = [] // 存储物理对象 // 添加物理调试器 const cannonDebugger = new CannonDebugger(scene, world, { color: 0x00ff00, // 调试器颜色 scale: 1, // 缩放比例 }) cannonDebugger.update() // 更新调试器 // 创建一个平面作为地面 const groundGeometry = new THREE.PlaneGeometry(100, 100) const groundMaterial = new THREE.MeshBasicMaterial({ color: 0x999999, side: THREE.DoubleSide, }) const ground = new THREE.Mesh(groundGeometry, groundMaterial) ground.rotation.x = -Math.PI / 2 // 旋转平面使其水平 scene.add(ground) // 定义3个group。 const groupGround = 1 << 1 // 地面组 const groupA = 1 << 2 // 刚体组A const groupB = 1 << 3 // 刚体组B const groupC = 1 << 4 // 刚体组C // 创建物理地面 const groundShape = new CANNON.Plane() // 创建平面形状 const groundBodyMaterial = new CANNON.Material({ restitution: 0.8, // 弹性,范围[0, 1],0表示完全不弹性,1表示完全弹性,默认值为0.3 }) const groundBody = new CANNON.Body({ mass: 0, // 静态地面, 质量为0不下坠 position: new CANNON.Vec3(0, 0, 0), // 设置位置 shape: groundShape, // 添加形状到物理体 material: groundBodyMaterial, // 设置物理材质 // 设置分组 collisionFilterGroup: groupGround, // 碰撞组 }) groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // 设置平面旋转 world.addBody(groundBody) // 将物理体添加到物理世界 objArr.push({ mesh: ground, body: groundBody }) // 将物理地面添加到对象列表 // 创建可以与不同刚体组碰撞的球体 const sphereBodyMeterial = new CANNON.Material({ friction: 0.1, // 摩擦系数 restitution: 0.5, // 弹性系数 }) createSphere({ position: new THREE.Vector3(-20, 70, 0), // 设置位置 color: 0xff0000, // 设置颜色 group: groupGround, }) createSphere({ position: new THREE.Vector3(-10, 70, 0), // 设置位置 color: 0xff0000, // 设置颜色 group: groupGround | groupA, // 设置碰撞组 }) createSphere({ position: new THREE.Vector3(0, 70, 0), color: 0x00ff00, group: groupGround | groupB, }) createSphere({ position: new THREE.Vector3(10, 70, 0), color: 0x0000ff, group: groupGround | groupC, }) function createSphere({ position, color, group }) { const sphereGeometry = new THREE.SphereGeometry(5, 32, 16) const sphereMaterial = new THREE.MeshBasicMaterial({ color, side: THREE.DoubleSide, }) const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial) sphereMesh.position.copy(position) // 创建物理体 const sphereBody = new CANNON.Body({ mass: 1, // 设置小球质量 position: new CANNON.Vec3(position.x, position.y, position.z), // 设置位置 shape: new CANNON.Sphere(5), // 创建球形状 material: groundBodyMaterial, // 设置物理材质 // 设置可以与不同刚体组碰撞 collisionFilterMask: group, // 碰撞掩码,允许与其他组碰撞 }) world.addBody(sphereBody) // 将物理体添加到物理世界 objArr.push({ mesh: sphereMesh, body: sphereBody }) // 将物理体和网格添加到对象列表 scene.add(sphereMesh) // 添加网格到场景 } // 创建不同刚性组box隔板 createBox({ position: new THREE.Vector3(0, 10, 0), rotation: new THREE.Vector3(0, Math.PI / 4, 0), color: 0x00ff00, group: groupA, }) createBox({ position: new THREE.Vector3(0, 20, 0), rotation: new THREE.Vector3(0, -Math.PI / 4, 0), color: 0x0000ff, group: groupB, }) createBox({ position: new THREE.Vector3(0, 30, 0), rotation: new THREE.Vector3(0, 0, 0), color: 0xff0000, group: groupC, }) function createBox({ position, rotation, color, group }) { const boxGeometry = new THREE.BoxGeometry(50, 2, 20) const boxMaterial = new THREE.MeshBasicMaterial({ color, side: THREE.DoubleSide, transparent: true, opacity: 0.7, // 半透明 }) const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial) boxMesh.position.copy(position) boxMesh.rotation.setFromVector3(rotation) // 创建物理体 const boxBody = new CANNON.Body({ mass: 0, position: new CANNON.Vec3(position.x, position.y, position.z), shape: new CANNON.Box(new CANNON.Vec3(25, 1, 10)), material: sphereBodyMeterial, collisionFilterGroup: group, }) world.addBody(boxBody) // 将物理体添加到物理世界 objArr.push({ mesh: boxMesh, body: boxBody }) // 将物理体和网格添加到对象列表 scene.add(boxMesh) // 添加网格到场景 } function updatePhysics() { objArr.forEach((obj) => { obj.mesh.position.set(obj.body.position.x, obj.body.position.y, obj.body.position.z) // 更新three.js网格位置 obj.mesh.quaternion.copy(obj.body.quaternion) // 更新three.js网格旋转 }) world.step(1 / 60) // 更新物理世界,60 FPS cannonDebugger.update() // 更新调试器 } // 辅助工具 const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true // 启用阻尼效果 controls.dampingFactor = 0.25 // 阻尼系数 // 向下看 controls.target.set(0, 35, 0) // 设置控制器目标点 controls.update() addArrowAxesHelper(scene, 50) // 添加坐标轴辅助工具 // 窗口大小变化监听 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight // 更新相机宽高比 camera.updateProjectionMatrix() // 更新相机投影矩阵 renderer.setSize(window.innerWidth, window.innerHeight) // 更新渲染器大小 }) animate() function animate() { console.log('相机位置', camera) // 输出圆环位置 requestAnimationFrame(animate) updatePhysics() // 更新物理世界 controls.update() // 更新相机控制器 renderer.render(scene, camera) } ```