- 物理引擎
【免费下载链接】cannon.js
A lightweight 3D physics engine written in JavaScript.
cannon.js 是一个用 JavaScript 编写的轻量级 3D 物理引擎,专为 Web 场景设计,提供刚体动力学、离散碰撞检测、接触/摩擦/恢复系数以及多种关节约束能力。本文以仓库根目录的 README.markdown 为核心,结合 src 目录下的源码实现与 test 目录中的测试用例,系统讲解从浏览器/Node.js 安装、物理世界搭建、固定时间步模拟循环,到形状碰撞矩阵、约束类型与渲染集成的完整实战方案,读者学完后可以独立搭建一个可运行的 3D 物理仿真场景。
一、项目定位与设计动机
cannon.js 的诞生源于 Web 平台长期缺乏可用的物理引擎这一现实。其设计同时受到 three.js(渲染)与 ammo.js(物理)的启发,但选择了纯 JavaScript、无原生依赖的实现路线,目标是让开发者能在浏览器中直接引入即可使用的轻量级刚体物理引擎。
从 package.json 可以看到,当前仓库版本为0.6.2,主入口为./src/Cannon.js,包名为cannon,采用 MIT 许可。而 src/Cannon.js 作为统一的导出入口,将引擎的全部公开类(World、Body、Vec3、Quaternion、各类Shape、Constraint、Solver等)以 CommonJS 模块形式对外暴露,这也是浏览器构建产物中CANNON全局命名空间的来源。
引擎的核心能力在 README 中概括为:简单的碰撞检测、多种刚体形状、接触(contact)、摩擦(friction)与约束(constraint)。接下来我们按"安装 → 运行 → 深入原理"的顺序展开。
二、安装方式:浏览器与 Node.js 双通道
README 提供了三种安装途径,适用场景各不相同。
2.1 浏览器直接引入
如果只是想在 HTML 页面中快速使用,直接把构建产物(cannon.js或压缩版cannon.min.js)引入即可:
<script src="cannon.min.js"></script>引入后引擎的全部 API 会挂在全局CANNON命名空间下(这一点由构建工具 browserify 的standalone: "CANNON"配置保证,见 Gruntfile.js),随后即可直接使用CANNON.World、CANNON.Body等类。
2.2 Node.js / npm 安装
在服务端、构建工具链或单元测试环境中,通过 npm 安装:
npm install --save cannonpackage.json中"main": "./src/Cannon.js"指明了包入口,因此在 Node 环境中require('cannon')或require('cannon/src/Cannon.js')即可获得完整 API。仓库自带的测试也遵循这一方式——例如 test/World.js 直接require("../src/world/World")等模块进行验证。
2.3 直接从 GitHub 仓库安装
若希望使用尚未发布到 npm 的最新开发版代码,可以指向仓库安装:
npm install --save schteppe/cannon.js这种方式会拉取仓库主干的最新源码,适合需要即时跟进上游改动、或希望自行基于源码构建的场景。
三、快速上手:第一个物理场景
README 给出了一个完整可运行的入门示例:创建一个落在无限平面上的球体,推进模拟,并在控制台输出球体的 z 坐标。下面我们逐段拆解,并结合源码说明每个步骤背后的实现。
3.1 创建物理世界并设置重力
// Setup our world var world = new CANNON.World(); world.gravity.set(0, 0, -9.82); // m/s²CANNON.World是物理世界的容器。从 src/world/World.js 可以看到,World构造时会初始化一系列核心设施:
gravity:一个Vec3,默认零向量,示例中设置为(0, 0, -9.82),即 z 轴方向的重力加速度(m/s²);broadphase:默认使用 NaiveBroadphase(朴素宽相位,O(N²) 复杂度);solver:默认使用 GSSolver(Gauss-Seidel 约束求解器,默认 10 次迭代、容差 1e-7);narrowphase:负责生成接触点对的窄相位;- 默认材质
defaultMaterial与默认接触材质defaultContactMaterial(摩擦系数 0.3、恢复系数 0.0,见 src/world/World.js)。
3.2 创建刚体与形状
// Create a sphere var radius = 1; // m var sphereBody = new CANNON.Body({ mass: 5, // kg position: new CANNON.Vec3(0, 0, 10), // m shape: new CANNON.Sphere(radius) }); world.addBody(sphereBody); // Create a plane var groundBody = new CANNON.Body({ mass: 0 // mass == 0 makes the body static }); var groundShape = new CANNON.Plane(); groundBody.addShape(groundShape); world.addBody(groundBody);这段代码演示了 cannon.js 两个关键概念:
Body(刚体):CANNON.Body是物理对象的基本单位。从 src/objects/Body.js 的构造函数可以看到,一个 Body 拥有position(位置)、velocity(线速度)、quaternion(姿态四元数)、angularVelocity(角速度)、mass(质量)、force/torque(力与力矩)等属性。其中:
- 质量与类型的关系:
mass <= 0时,Body 自动被归类为Body.STATIC(静态体);mass > 0时为Body.DYNAMIC(动态体)。这正是注释mass == 0 makes the body static的源码依据(见 src/objects/Body.js)。静态体表现为"无限质量"——速度恒为零、不响应外力,适合做地面、墙壁等固定障碍。 - 除了
STATIC(值 2)与DYNAMIC(值 1),引擎还定义了KINEMATIC(值 4,按速度运动但不响应力的运动学物体),三者常量定义见 src/objects/Body.js。
Shape(形状):CANNON.Sphere(radius)创建半径为 1 米的球体形状,CANNON.Plane()创建无限平面。Body 与 Shape 的装配有两种方式——通过构造选项shape直接绑定,或通过body.addShape(shape)添加。后一种方式支持一个 Body 挂载多个形状并各自带局部偏移与朝向(shapeOffsets、shapeOrientations),这是复合刚体(compound body)的基础;每次addShape都会触发updateMassProperties()与updateBoundingRadius()重新计算质量属性与包围球半径(见 src/objects/Body.js)。
SI 单位制:README 特别强调 cannon.js 使用国际单位制(米、千克、秒),示例注释中m、kg、m/s²均按此标注。这也意味着渲染层若使用不同单位(如 three.js 的任意缩放),需要自行做尺度换算,或者将物理单位直接映射到场景单位。
3.3 推进模拟:固定时间步与插值
var fixedTimeStep = 1.0 / 60.0; // seconds var maxSubSteps = 3; // Start the simulation loop var lastTime; (function simloop(time){ requestAnimationFrame(simloop); if(lastTime !== undefined){ var dt = (time - lastTime) / 1000; world.step(fixedTimeStep, dt, maxSubSteps); } console.log("Sphere z position: " + sphereBody.position.z); lastTime = time; })();这是 cannon.js 最核心的用法:用requestAnimationFrame驱动渲染循环,把真实流逝的时间交给world.step(),由引擎内部完成固定步长模拟。world.step(fixedTimeStep, dt, maxSubSteps)的三个参数含义为:
fixedTimeStep:固定的物理步长(示例为 1/60 秒),物理积分始终按此步长推进,保证模拟稳定性与确定性;dt:自上次调用以来的真实流逝时间(秒);maxSubSteps:单次调用内最多执行的固定子步数(示例为 3),用于限制追赶积分的上限。
从 src/world/World.js 的step实现可以看出它支持两种模式:
固定步长模式(timeSinceLastCalled为 0 或未提供):直接执行一次internalStep(dt),时间累加dt。对应 README 中提到的"不带插值的固定时间步"。
带插值的追赶模式:将流逝时间累加到accumulator,然后循环执行internalStep(dt)直到accumulator < dt或达到maxSubSteps上限;随后用剩余时间的比例因子t对每个刚体的previousPosition/position与previousQuaternion/quaternion做线性插值(slerp),得到interpolatedPosition/interpolatedQuaternion。这为渲染层提供平滑的视觉插值,思路源自 Gaffer On Games 的 "Fix Your Timestep" 方法(World 源码注释中有引用)。
3.4 每步模拟内部发生了什么
internalStep(见 src/world/World.js)是每帧模拟的主干流程,依次执行:
- 施加重力:对所有
DYNAMIC刚体,将mass × gravity累加到force上; - 更新子系统:调用各
subsystems的update()(如 SPH 流体系统); - 宽相位碰撞检测:
broadphase.collisionPairs()找出可能碰撞的物体对,并剔除collideConnected === false的约束连接对; - 碰撞矩阵轮换:
collisionMatrixTick()交换当前/上一帧碰撞矩阵; - 窄相位接触生成:
narrowphase.getContacts()生成接触方程与摩擦方程(方程对象会从对象池复用,减少 GC); - 求解约束系统:将摩擦方程、接触方程以及用户约束的方程全部加入
solver,调用solver.solve(dt, world),随后清空方程; - 阻尼与积分:对动态体应用
linearDamping/angularDamping(默认 0.01),再执行 Leap-frog 式积分vnew = v + h·f/m; xnew = x + h·vnew,并按quatNormalizeSkip/quatNormalizeFast配置规范化四元数; - 清力与收尾:
clearForces()清零所有力与力矩,触发preStep/postStep事件,若开启allowSleep则执行sleepTick睡眠状态更新。
四、特性总览:cannon.js 的能力清单
README 的特性列表是对引擎能力最权威的概括,结合源码逐一对应如下:
| 特性 | 说明 | 源码位置 |
|---|---|---|
| 刚体动力学(Rigid body dynamics) | 完整的位置/速度/角速度/力矩积分 | src/objects/Body.js |
| 离散碰撞检测(Discrete collision detection) | 宽相位(Broadphase)+ 窄相位(Narrowphase)两级检测 | src/collision、src/world/Narrowphase.js |
| 接触、摩擦与恢复(Contacts, friction and restitution) | 由ContactMaterial定义两材质间的摩擦与恢复系数 | src/material/ContactMaterial.js |
| 约束(Constraints) | 五类关节约束,见下文 | src/constraints |
| Gauss-Seidel 求解器与 island 拆分算法 | 默认GSSolver+ 可选的SplitSolver | src/solver |
| 碰撞过滤器(Collision filters) | 基于collisionFilterGroup/collisionFilterMask的位掩码过滤 | src/collision/Broadphase.js |
| 刚体睡眠(Body sleeping) | 速度低于阈值一段时间后自动休眠,节省计算 | src/objects/Body.js |
| 实验性 SPH 流体支持 | Smoothed-particle hydrodynamics 流体模拟 | src/objects/SPHSystem.js |
| 多种形状与碰撞算法 | 见下一节的形状-形状碰撞矩阵 | src/shapes |
4.1 求解器:Gauss-Seidel 与 island 拆分
README 特别点名了两项求解器特性。默认的 GSSolver 是约束方程 Gauss-Seidel 迭代求解器,暴露两个关键调参项:
iterations(默认 10):迭代次数越多,约束求解越精确,但计算量越大;重力较大或约束较硬的世界需要更多迭代;tolerance(默认 1e-7):当单轮迭代误差总和低于该容差平方时提前收敛跳出循环。
Gauss-Seidel 求解的每一次迭代会遍历全部方程,计算deltalambda = invC × (B - GWlambda - eps·λ),并用方程自身的minForce/maxForce做投影钳制,最后把累积的 λ 结果写回刚体速度(见 src/solver/GSSolver.js)。
SplitSolver 则提供了 island 拆分优化:它将所有方程按刚体连接关系构建图结构,用 BFS 把世界拆分成彼此独立的子系统(island),再交给内部的subsolver逐个求解。由于互不相连的刚体组互不影响,拆开后每个子系统的方程数大幅减少,从而提升整体求解性能(见 src/solver/SplitSolver.js)。
4.2 碰撞过滤、睡眠与事件系统
- 碰撞过滤:每个 Body 和 Shape 都有
collisionFilterGroup(所属组,默认 1)与collisionFilterMask(可碰撞掩码,默认 -1 即全部)。宽相位在needBroadphaseCollision中用按位与判断两个物体是否允许碰撞:(groupA & maskB) !== 0 && (groupB & maskA) !== 0,同时会跳过"双方均为静态或均处于睡眠"的组合(见 src/collision/Broadphase.js)。 - 睡眠机制:
World.allowSleep开启后,每个刚体通过sleepTick检查速度模长平方是否低于sleepSpeedLimit²(默认 0.1),持续低于sleepTimeLimit(默认 1 秒)则进入SLEEPING状态并清空速度;睡眠中的刚体在求解时会被当作无限质量(invMassSolve = 0),从而大幅减少计算量(见 src/objects/Body.js)。 - 事件系统:
World与Body都继承自EventTarget,支持addBody、removeBody、preStep、postStep、collide、beginContact/endContact等事件,其中collide事件在每次接触建立的瞬间派发到碰撞双方刚体上(见 src/world/World.js),beginContact/endContact由OverlapKeeper对比前后帧的接触集合得出(见 src/world/World.js)。
五、形状系统与形状-形状碰撞矩阵
cannon.js 内置 8 种形状,全部继承自Shape基类(src/shapes/Shape.js),并用位标志常量区分类型(见 src/shapes/Shape.js):
- Sphere(球体,
SPHERE=1) - Plane(无限平面,
PLANE=2) - Box(长方体,
BOX=4) - ConvexPolyhedron(凸多面体,
CONVEXPOLYHEDRON=16) - Particle(质点,
PARTICLE=64) - Heightfield(高度场,
HEIGHTFIELD=32) - Trimesh(三角网格,
TRIMESH=256) - 另外
Cylinder(圆柱)内部是ConvexPolyhedron的特殊构造,COMPOUND=8表示复合形状组合。
README 提供了权威的形状间碰撞支持矩阵,指明哪两种形状之间可以产生碰撞接触(横轴为第一种形状,纵轴为第二种形状,"Yes" 表示支持,"-" 表示不适用或未实现,"todo" 表示规划中):
| Sphere | Plane | Box | Convex | Particle | Heightfield | Trimesh | |
|---|---|---|---|---|---|---|---|
| Sphere | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Plane | - | - | Yes | Yes | Yes | - | Yes |
| Box | - | - | Yes | Yes | Yes | Yes | (todo) |
| Cylinder | - | - | Yes | Yes | Yes | Yes | (todo) |
| Convex | - | - | - | Yes | Yes | Yes | (todo) |
| Particle | - | - | - | - | - | (todo) | (todo) |
| Heightfield | - | - | - | - | - | - | (todo) |
| Trimesh | - | - | - | - | - | - | - |
实际碰撞检测在窄相位(src/world/Narrowphase.js)中完成,它会根据形状类型分派到对应的接触生成算法。以最简单的 Sphere 为例:其惯性矩为I = 2·m·r²/5(球体绕任意直径的转动惯量公式),体积V = 4·π·r/3,包围球半径即自身半径,AABB 则是以位置为中心、半径为 r 的立方体范围(见 src/shapes/Sphere.js)。
六、约束系统:五类关节详解
README 明确列出引擎支持的 5 类约束,它们统一继承自 Constraint 基类(提供equations列表、enable()/disable()、collideConnected选项——即被约束连接的两个物体是否仍参与相互碰撞)。每个约束在每次step时通过update()刷新方程数据,随后加入求解器统一求解。
| 约束类型 | 中文俗称 | 典型用途 | 核心实现 |
|---|---|---|---|
PointToPointConstraint | 球窝关节(ball/socket joint) | 把两个刚体的一点钉在一起,允许绕该点自由旋转,如布娃娃的肩/髋关节 | src/constraints/PointToPointConstraint.js |
DistanceConstraint | 距离约束 | 保持两刚体间距离恒定(如绳子、弹簧的刚性版本) | src/constraints/DistanceConstraint.js |
HingeConstraint | 铰链/合页关节(可选电机) | 只允许绕一个轴旋转,如门铰链、车轮轴;可通过enableMotor()/setMotorSpeed()激活电机驱动旋转 | src/constraints/HingeConstraint.js |
LockConstraint | 锁定关节 | 将两刚体的相对位置与姿态完全锁死,如同焊接在一起 | src/constraints/LockConstraint.js |
ConeTwistConstraint | 圆锥扭转关节 | 限制绕一轴的摆动角与扭转角范围,适合模拟肩部等锥形摆动约束 | src/constraints/ConeTwistConstraint.js |
以 HingeConstraint 为例,其内部由一组 PointToPoint 约束(钉住枢轴点)+ 两条RotationalEquation(限制绕轴的旋转自由度)+ 一条默认禁用的RotationalMotorEquation(电机方程)组成。构造时可指定pivotA/pivotB(两刚体局部坐标系中的枢轴点)与axisA/axisB(局部旋转轴),未指定时轴默认取 x 轴(1,0,0)并自动归一化。
七、与渲染引擎集成:让网格跟随刚体
物理引擎只负责计算,不负责绘制。README 指向的 Examples 专门讲解了如何把 cannon.js 与 Three.js、SceneJS、KickJS 等渲染库对接,核心思路是同步 position 与 orientation。
以 Three.js 为例,最便捷的方式是启用网格的四元数模式,然后逐帧把刚体的位姿复制给网格:
mesh.useQuaternion = true; mesh.position.x = body.position.x; mesh.position.y = body.position.y; mesh.position.z = body.position.z; mesh.quaternion.x = body.quaternion.x; mesh.quaternion.y = body.quaternion.y; mesh.quaternion.z = body.quaternion.z; mesh.quaternion.w = body.quaternion.w;SceneJS 则需要在场景中为网格建立translate与quaternion节点,再按轴/角表示更新;KickJS 直接复制位置与四元数向量即可,并可在组件update中调用world.step(0.001 * deltaTime)推进模拟。仓库 examples 目录提供了threejs.html、scenejs.html、threejs_cloth.html、threejs_fps.html、threejs_mousepick.html、threejs_voxel_fps.html、worker.html等完整示例,demos 目录则包含 30 余个交互演示场景(由 src/demo/Demo.js 的CANNON.Demo类驱动),可直接在浏览器中体验碰撞、约束、弹簧、车辆、流体等各项能力。
八、质量属性、力与冲量 API 速查
围绕Body,引擎还提供了一系列高频 API,用于操控刚体行为(均见 src/objects/Body.js):
body.applyForce(force, relativePoint)/applyLocalForce(...):在世界/局部坐标系施加力,会同时累加线性力到force、由力臂叉积产生力矩到torque(L715-L754);body.applyImpulse(impulse, relativePoint)/applyLocalImpulse(...):施加冲量,直接改变velocity与angularVelocity(L762-L817);body.updateMassProperties():改变形状或质量后调用,基于 AABB 近似重新计算转动惯量(L825-L839);body.wakeUp()/body.sleep():手动唤醒/强制休眠;body.pointToLocalFrame()/pointToWorldFrame()等:刚体局部与世界坐标系间的点/向量变换;linearFactor/angularFactor:按世界轴限制平动或转动自由度,(0,0,0)表示禁止某轴运动;fixedRotation:设为 true 则禁止刚体旋转(需重新调用updateMassProperties())。
九、调参与调试建议
结合源码中的默认值与注释,给出几条实用调参指引:
- 求解器迭代数(
world.solver.iterations,默认 10):若世界重力很大或约束偏"软"导致穿透、抖动,可适当增大;反之追求性能可减小; - 接触材质参数(
ContactMaterial的friction、restitution、刚度/松弛时间):ContactMaterial默认friction: 0.3、restitution: 0.3,而World的defaultContactMaterial为friction: 0.3、restitution: 0.0(见 src/material/ContactMaterial.js 与 src/world/World.js)。World.internalStep中还会检查两个刚体各自Material上的friction/restitution是否同时非负,若是则取两者乘积覆盖接触材质的值(L714-L726); - 四元数归一化:
world.quatNormalizeSkip控制每隔多少步归一化一次(0 表示每步都归一化),quatNormalizeFast开启快速近似归一化;若刚体出现"爆炸"式发散,应把 skip 调小、关闭 fast(见 src/world/World.js); - 宽相位选择:默认的 NaiveBroadphase 是 O(N²) 的全对检测,物体数量较多时可换用 SAPBroadphase(扫描线)或 GridBroadphase(空间网格)以提升性能;
broadphase.useBoundingBoxes = true可把包围球相交测试切换为 AABB 相交测试。
十、测试与构建:验证引擎行为
仓库用 nodeunit 编写了覆盖各模块的单元测试(test 目录,共 20 个测试文件),并在 Gruntfile.js 中集成了完整工具链:
grunt test:运行 nodeunit 测试(test/**/*.js),例如 test/World.js 验证了clearForces、射线检测(rayTest/raycastClosest/raycastAll)、以及用两帧步进world.step(0.1)校验碰撞矩阵状态的正确性(test/World.js#L317-L328);- 默认任务链:
test → concat → browserify → uglify → 追加 LICENSE 与日期 → RequireJS 兼容修复,产出build/cannon.js与build/cannon.min.js; grunt-contrib-yuidoc从 src 源码的 JSDoc 注释生成 API 文档(输出到 docs 目录,含每个类的完整属性/方法说明)。
十一、结语
cannon.js 以极小的接入成本提供了刚体物理所需的核心能力:世界与刚体的概念清晰(World/Body/Shape三分天下),安装只需一行<script>或一条 npm 命令,模拟循环通过world.step一以贯之,形状碰撞矩阵与五类约束则覆盖了绝大多数 3D 交互场景的物理需求。对于希望在 Web 中快速获得"球落地、物体堆叠、关节驱动、车辆行驶"等物理效果的开发者而言,这是一份开箱即用的轻量级方案;若要进一步探索,可阅读 docs 的完整 API 文档、demos 的交互示例以及 src 中每一模块的源码注释,README 中标注的@todo注释与 Issue 跟踪也揭示了引擎仍在演进的方向。
- 物理引擎
【免费下载链接】cannon.js
A lightweight 3D physics engine written in JavaScript.
相关推荐
ChatDev物理引擎:刚体动力学与碰撞检测
ChatDev物理引擎:刚体动力学与碰撞检测 🎯 痛点场景:为什么游戏开发需要物理引擎? 你是否曾经尝试用ChatDev开发游戏时遇到这样的困境:角色移动生硬
人工智能AI Agent多智能体Agent 工作流后端前端低代码Phaser 4 Matter.js 物理系统实战指南:刚体、约束、复合体、传感器与碰撞过滤完整解析
Phaser 4 Matter.js 物理系统实战指南:刚体、约束、复合体、传感器与碰撞过滤完整解析 本文基于 Phaser 仓库中的官方技能文档( skill
游戏开发图形学前端Godot 2D 刚体 RigidBody2D 完全指南:物理模拟、力与冲量、碰撞响应实战
Godot 2D 刚体 RigidBody2D 完全指南:物理模拟、力与冲量、碰撞响应实战 RigidBody2D 是 Godot 引擎中实现完整 2D 物理模
文档教程游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考