Godot 物理源码分析
前言
看引擎源码应该是引擎岗的基操了,且了解我的可能知道我主物理模拟。之前在项目组实习时改过Unity的源码,返校后就只能接触些开源引擎。UE的Chaos有些过重了:Init时Scene依次创建Scene、Solver、Evolution,Object依次RegisteEvent,CreateState,InitBody;Simu时先将游戏线程的AOS数据Push到SOA的物理线程中,经过一系列复杂的计算(毕竟个人比较在意移动端的表现)后将改动的脏数据Pull回去。而Godot的源码轻量,结构清晰,干脆就着拆解下3D物理部分。
看Godot最新版本4.5的文档其实还挺有趣的,如Godot的内存管理:对小对象采取预留空闲内存的堆分配,对大对象采取动态内存池 (用PooledList记录list和freelist)。与操作系统的分页选择截然不同,算是特化与通用的差异?以及Godot在21年发布的解释Godot为什么不基于ECS的架构和抽象层次的选择 (提供了一种新的视角,但个人其实不大认同,不过既然开源了有需要自己搭框架就好)。还有Godot的移动端渲染管线无法读取相邻像素 (不是哥们你这就BAN了我的片元偏导,计算着色器也基本不支持,印度东南亚手机都没这么不抗造吧?)。
架构概览
整个Godot的3D物理部分都在modules/godot_physics_3d中,十分甚至九分的方便查找
- 物理服务器(godot_physics_server_3d)创建和管理空间、形状、区域和碰撞对象。
- 物理空间(godot_space_3d)包含物理世界中的所有物体,负责物理模拟。
- 物理步进器(godot_step_3d)被空间用来逐步执行物理模拟。
- 碰撞检测(godot_broad_phase_3d)快速找出可能发生碰撞的物体对
- 碰撞求解(godot_collision_solver_3d)处理具体的形状之间的碰撞检测。
- 形状(godot_shape_3d)定义了各种几何形状,用于碰撞检测。
- 碰撞对象(godot_collision_object_3d)是区域、刚体和软体的基类,都可以有形状。
- 区域(godot_area_3d)用于检测物体进入和退出,并可以修改重力等属性。
- 刚体(godot_body_3d)硬质物理物体,受力和碰撞的影响。
- 软体(godot_soft_body_3d)可变形物体,使用粒子系统模拟。
- 约束和关节(godot_constraint_3d和godot_joint_3d)用于限制物体之间的运动。
然后模拟流程大致为:

参数准备
首先还是从准备开始讲起,虽说是要处理各种属性设置,主要的处理还是落在更新物体的质量属性。以最复杂的刚体为例,其先计算总面积(累加每个形状的面积),再计算质心(按面积分配质量),最后计算惯性张量(平行轴定理)。
1 | void GodotBody3D::update_mass_properties() { |
稍微补充下惯性张量的计算: $I_{total} = \displaystyle\sum_{i=1}^{n}{(R I_{local}R^T + w(r \cdot r \cdot I_{identity} - r \otimes r))} $。以加号为界,左侧为当前形状转动惯量,右侧为加权合成。光是这样可能十分抽象,那就以点云的惯性张量举例吧。
假设我们有一个由 n 个点组成的滔博陀螺,这些点的坐标是 pi ,其中点 i 的质量为 mi,我们希望计算其初始的旋转惯量。 则有公式:$I_{ref}=\displaystyle\sum_{i=1}^{n}(m_i(r_i\cdot r_i\cdot I_{identity}− r_i\otimes r_i))$,其中偏移向量$r_i=p_i-p_{center}$。这就是引擎所用公式的右侧部分,内积描述质量分布,外积描述惯性贡献。之后陀螺开始自转,旋转矩阵为 $R$ ,则有$ I=RI_{ref}R^T $。这就是公式的左侧部分,合并下就是引擎所使用的方案(这里引擎的方法考虑了形状在整体惯性张量中的位置偏移,个人认为没这必要)。
积分外力
在更新完物理属性后就是刚体的积分外力,软体的形状预测和BVH碰撞检测。
刚体的积分外力
首先是刚体的积分外力,跳过前面那堆遍历区域确认阻尼模式的代码,直接进入最核心的部分。对于运动学物体,其由外部驱动,再根据位置和旋转变化更新线速度和角速度。而对于刚体,其先计算合力和合力矩,再应用阻尼更新速度。
1 | void GodotBody3D::integrate_forces(real_t p_step) { |
软体的形状预测
然后是软体的形状预测,同样跳过前面那堆遍历区域确认重力的代码,在计算完风力后积分部分采用的显式欧拉加位移钳制,最后更新边界和加速结构(AABB包围盒树)。(有些想吐槽软体连个半隐欧拉都不上,但其实后续软体的迭代修正应该也足够了)
1 | void GodotSoftBody3D::predict_motion(real_t p_delta) { |
BVH碰撞检测
最后是BVH碰撞检测,先更新BVH树,再检测碰撞。其中更新BVH树有增量优化和动态扩张。而碰撞检测阶段则是先扩展AABB,再填充裁剪参数,找出不再碰撞的项,查询重叠的项,处理新的碰撞项。
1 | void GodotBroadPhase3DBVH::update() { |
先从Godot基于模板库的动态BVH树实现开始,节点和叶子定义如下。
1 | struct TNode { |
在增量优化中则先就着原来结构自从底而上的更新AABB包围盒,再分帧抽出一个节点更新后重新插入,由此可解决AABB膨胀和次优位置。
1 | void incremental_optimize() { |
约束分组
构造约束
在积分完外力之后,就是构造约束关系供后续并行化了。Godot分了区域,刚体和软体三处不同逻辑,简单来说区域部分就是逮着一个就扔进去(毕竟不进行计算),刚体是检测与其关联的刚体软体,软体只检测与其关联的刚体(软体之间应该是暂不支持)。三处构造约束的逻辑大同小异,这里放个刚体的做简单说明。
1 | void _populate_island(GodotBody3D *p_body, ...) { |
求解约束
构造完约束的话下一步自然是求解:先并行设置所有约束,再单线程预求解(过滤无效约束),最后并行求解(使用优先级迭代)。具体约束由area,body,joint这些继承类实现,这里选取较为通用的body分析(area中没有求解步骤,joint变体过多)。
1 | bool GodotBodyPair3D::setup(real_t p_step) { |
比较值得一提的是其3D碰撞检测是Bullet物理引擎中GJK-EPA算法的移植,算是游戏引擎的标配了。不过挺好奇为什么没有引擎上SAT,这种小常数的O(n)大概在100以内应该是优于大常数的O(log n)的,处理OBB那种简单形状应该较优。原因可能是Godot需要EPA求深度用于穿透冲量,但个人也不喜欢穿透冲量这一并不物理的概念,只能说有空自己写个物理引擎去玩玩。
积分速度
积分速度
在解约束后,就是积分速度。对于刚体而言本质上就是各种注册回调,处理物体属性,锁定轴向的位置更新。
1 | void GodotBody3D::integrate_velocities(real_t p_step) { |
约束求解
如果是软体的话最终输出阶段还是要稍微复杂些,涉及到迭代求解约束(本质上还是基于 PBD 的距离约束,但引入了类 XPBD的刚度缩放与质量权重)。
1 | void GodotSoftBody3D::solve_constraints(real_t p_delta) { |
还是稍微展开下约束求解吧,它先计算了柔度$c_0=(invMass0+invMass1)/stiffness$ ,之后以 $k=(L²−|x|²)/(c0∗(L²+|x|²)) $求解。前一步结合了质量和刚度,但未除以 $Δt²$ 解耦材料属性与模拟时间步长;后一步在分母中使用 $(L²+|x|²)$ 稳定数值,但也没有存储和累计拉格朗日乘子 λ ,所以只能算是魔改的PBD。
但话又说回来,游戏中步长和迭代次数恒定,只要刚度能调就行。先不提XPBD所带来的5%-10%性能消耗,XPBD还被 $Δt²$ 的大常数拖累导致参数调整不直观,反馈到美术那边就是开到极大极小值才有些微表现差异。这种情况下XPBD反而是技术陷阱(毕竟个人程策美三开)。
后记
Godot的3D物理引擎是一个轻量级、模块化的实时物理模拟系统。通过约束划分和并行求解,也能在多核CPU上高效运行。虽然看源码本来就是一件麻烦事,但确实远比不上UE的心智负担,还是希望这个分析能给读者一些帮助吧。







