经过前面几篇的学习,我们已经积累了很多关于物理引擎的理论知识,这一篇我们打算把这些知识都学以致用,动手实践一个非常简单的物理引擎,包含了物理引擎的各个阶段,并接入我们的引擎当中,与渲染模块一起工作,实现简单的模拟效果。
一。整体架构
- 在系列的前面部分,我们已经实现了基本的数学功能,和基本的渲染功能。基于这两个基本模块,我们可以在上面构建自己的物理引擎,以及对物理模拟进行可视化。
- BroadPhase、NarrowPhase、ResolvePhase分别是我们前面几篇学习的内容。
- Rigidbody Collider用于描述物体的外形,目前我们只支持长方体。
- GJK/EPA是Narrow Phase阶段非常重要的两个算法。
- Manifold用于描述两个物体之间的接触情况,可以包含多个碰撞点。
- Contact描述一个碰撞点的详细情况。
二。工程细节
代码可以在这里浏览。
上图是从代码文件的角度对各个功能的划分,大致反应了实现的复杂度。由于时间成本和性价比问题,我做了一些实现上的取舍:
- BroadPhase其实属于一种优化阶段,来提高后续碰撞检测的效率。由于我们想快速看到效果,所以也先跳过,直接返回所有物体对。这对于我们的简单测试场景没有任何影响。
- 由于GJK、EPA算法的实现比较繁琐,从头手写的话太多细节了,所以这里选择用Bullet库的实现。由于Bullet有自己的数学库,所以需要把算法里涉及数学运算的部分全部替换成我们自己的数学库的运算,这一步根据编译器的错误信息一点一点改就好了。
- 关于Manifold的简化。物体与物体之间,理论上需要3个接触点才能保持相对稳定的状态。这里其实还涉及到多个碰撞点的维护和更新问题,细节可以看看。目前我先跳过这部分考虑,只维护一个碰撞点。虽然不严谨,但是能让我们快速看到实现效果。
三。效果
首先,我们来看看一个最简单的模拟情景,一个箱子掉落在地板上。效果如下所示。
可以看到,虽然我们是一个非常简单的实现,不包括任何稳定化处理,但是也基本成型了。由于没有稳定化处理,可以看到箱子在落掉到地板后,依然会缓慢地在地板上滑动,时间长了甚至会穿到地底下。这些都需要额外的稳定机制来处理,我们暂时先跳过不管。
如果我们加大摩擦力,可以看到箱子与地面接触后会锁定在落点处。虽然不是非常严谨,但是基本体现了摩擦系数对于碰撞的影响。如下图所示。
如果我们把两者的弹性系数调大,箱子则会发生剧烈的反弹,以至于在空中转圈。如下图所示。
到这里,我们的物理引擎实践就告一段落了。回过头来看物理相关的东西,真的非常感慨。这部分内容需要大量理论知识来支撑,涉及物理、数学、计算机等各个学科的融合。但是这些功能在我们日常的开发过程中又非常常见,以致于我们觉得自己应该懂了。当然我们大部分人在职业生涯中都不太有机会去开发物理引擎,但是如果我们不去细究一下它工作的原理的话,这部分内容对我们来说就会像神奇的黑盒一样,而我们则成为API caller。
令人恐惧的不是我们不懂,而是我们以为自己懂了。