设计一个物理引擎

2020-08-01 13:09:18

巧合的是,就在切尔诺宣布他的游戏引擎系列的时候,我刚刚开始使用我自己的引擎。我迫不及待地想最终就如何制作一部电影有一个专业的意见。对于自学成才的编程,很难不不断地怀疑自己,怀疑自己是否做得对,或者只是认为自己做得对。

最近,他一直在发布关于他的引擎的巨大方面的视频,比如物理和实体系统,这是我真的想通过自己制作来了解的,但他最终使用了库,而不是查看内部!我不反对使用图书馆,而是把它们用来做有趣的事情?我觉得它违背了定制发动机系列的意义。

有关于节省时间的争论,但这是我做的第一个C++项目,从一开始的目标就是通过引擎的所有主要支柱:输入、图形、物理、实体和音频。我想了解这些东西是如何与C++和代码设计一起工作的。

我打赌其他一些人会对这些系统如何工作的细节感兴趣,我想学习如何更好地解释代码,所以我将尝试制作一些视频来介绍这些系统的内部结构。它们最终比乍一看要简单得多。

物理引擎负责计算出场景中每个对象随时间推移的位置。对象可以相互碰撞,然后选择以多种方式响应。这是一个一般性问题,用户可以在几个不同的级别进行配置。他们想要对撞机吗?他们想对碰撞做出反应吗?他们想要模拟动态吗?他们可能想要动力,但不想要重力。这是一个需要良好的规划和稳健的设计的问题。

我观察了Bullet和Box2d是如何对引擎进行分类的,并得出结论,Bullet在其中运行的方式是坚固的。我把它归结为需要的东西,并以此为我的设计基础。已经有一些很棒的文章复习了其中涉及的数学难题,所以我将把重点放在设计方面,因为我还没有看到任何人这样做,这也是一个真正令人头疼的问题。

目前,这个物理引擎还没有完整的功能,但是在以后的文章中,我计划进一步构建它。本文将不介绍旋转、多接触点碰撞或受约束的模拟。我认为这会是最好的结果,因为它很容易让人不知所措,我想轻松地进入这些话题。说到这里,让我们深入了解物理引擎的不同部分。

问题可以分为2个或3个部分,即动力学、碰撞检测和碰撞响应。我将从动力学开始,因为它是目前为止最简单的。

动力学就是根据对象的速度和加速度计算对象的新位置。在高中,你会学到四个运动学方程,以及描述物体运动的牛顿三定律。我们将只使用第一个和第三个运动学方程,其他方程在分析情况时更有用,而不是模拟。这就给我们留下了:

V=v_0+at\Delta x=v_0t+\frac{1}{2}在^2我们可以通过使用牛顿第二定律,减去加速度给我们:

V=v_0+\frac{F}{m}tx=x_0+vt每个物体都需要存储这三个属性:速度、质量和净力。在这里,我们找到了我们可以对设计做出的第一个决定,净力可以是列表,也可以是单个矢量。在学校里,你制作力图并总结力,这意味着我们应该存储一个列表。这将使它成为您可以设置的力,但是您需要稍后删除它,这可能会让用户感到厌烦。如果我们进一步考虑,净力实际上是在单个帧中施加的总力,所以我们可以使用一个向量,并在每次更新结束时清除它。这允许用户通过添加力来施加力,但删除力是自动的。这缩短了我们的代码,并带来了性能提升,因为没有力的总和,而是一个运行的总和。

Struct对象{Vector 3的位置;a//s具有3个浮点数的struct对象;对于xx,yy,zz或{i}+{j}+{k}的向量3的速度;向量3的力;质量的浮点;};,{##**$$},{{##**$$}},{##**$$};

我们需要一种方法来跟踪要更新的对象。一种经典的方法是让物理世界具有对象列表和在每个对象上循环的步长函数。让我们看看它可能是什么样子;为简洁起见,我将省略头文件/cpp文件。

类PhysicsWorld{private:std::Vector<;Object*>;*m_Objects;Vector 3:m_gravitor3=1 Vector 3(0,0-9.81f,0);Public:void/AddObject*(Object*Object){0/*...*/1}void/*RemoveObject(Object*Object)*{0//}void in Step(Float Under Dt){Float(Object*Object):Float_Objects){obj->;Force+=OBJ->;Bulk*Obj->;Bulk*OBJ_GRANCE;*//应用有效的OBJ-&D力OBJ-&O(对象*OBJ_OBJECTS)和{OBJ->;MASS*OBJ-&GROUCTION;Force/OBJ->;MASS*OBDT;OBJ-&gT;Position C+=OBJ->;Velocity*Obdt;obj->;Force=eVector3(0,000,000);//s在后端重置净FORCE};

注意指针的使用,这迫使其他系统负责对象的实际存储,让物理引擎担心物理问题,而不是内存分配问题。

有了它,你可以模拟各种各样的东西,从天空中飞过的物体到太阳系。

你可以用这个做很多事情,但老实说,这是很容易的部分,这不是你来找…的目的。

碰撞检测比较复杂,但我们可以通过一些巧妙的技巧来减轻负载。让我们先想想需要找到什么。如果我们观察一些对象碰撞的示例,我们会注意到在大多数情况下,每个形状上都有一个点,该点位于另一个形状内部最远的位置。

球体A,B球体,B球体,这就是我们对碰撞做出反应所需要的全部。从这两个点我们可以找到法线,以及物体彼此之间的深度。这是巨大的,因为这意味着我们可以将不同形状的概念抽象出来,而只需担心响应中的点。

让我们跳到代码中,我们需要一些帮助器结构,我将首先注意到这些结构。

Struct-CollisionPoints{Vector 3-A;//最远的数据点从A转换为A的向量3和B;//将B的最远的数据点转换为A的向量3:正常;//*B-*A的归一化浮点数深度;**//A的最大长度**//A bool-HasCollision;};struct-变换对象{0//*描述的是对象的结构(C/A)};结构//最远的数据点转换为A的向量3//A的最远的数据点转换为A的向量3:正常;//*B的最远的数据点转换为A的向量3:正常化的浮点数。

每个形状都将有不同类型的碰撞器来保存其属性,并有一个底座来存储这些属性。任何类型的对撞器都应该能够测试与任何其他类型的冲突,所以我们将在库中为每种类型添加函数。这些函数将接受变换,因此碰撞器可以使用相对坐标。我将只演示球体和平面,但是代码对于任何数量的对撞机都是可重复的。

Struct to Collider{virtual CollisionPoints to TestCollision(const Transform*Transform,const Collider*Collider,const Transform*ColliderTransform);virtual of CollisionPoints for TestCollision(const>Transform*t Transform,const>SphereCollider*Consphere,const>Transform*SphereTransform)*const=0.10;virtual of CollisionPoints(const>Transform*TestCollider*TestCollision)<const>Transform**>Const>Transform*>>Const>Const>Transform*>。

让我们同时制作这两种类型的对撞机,看看它们是如何相互作用的。球体定义为点和半径,平面定义为矢量和距离。我们将覆盖来自Collider的函数,但目前不会担心这项工作。

我们可以通过填写或不填写这些函数来选择每个对撞机将检测到哪些其他对撞机。在本例中,我们不希望平面v平面碰撞,因此返回一个空的CollisionPoints。

Struct SphereCollider:Const Collider{Vector 3 Center;Float of Radius;CollisionPoints for TestCollision(const>Transform*t Transform,const>Collider*Collider,const>Transform*ColliderTransform)>Const>Override{Return t Collider->;TestCollision(ColliderTransform,Tthis,Transform);}CollisionPoints for TestCollision(const>Transform*)。}CollisionPoints for TestCollision(const>Transform*>Transform,const>Plane Collider*>Plane,const>Transform**planeTransform)>Const>Override{return_algo::FindSpherePlane CollisionPoints(this,Transform,Plane,PlaneTransform);}};

我们可以添加一个用于测试库的函数,并使用一种称为双重调度的技术。这利用类型系统来为我们确定两种类型的对撞器,方法是交换参数,通过两次TestCollision调用确定第一种类型,然后确定第二种类型。这就省去了我们需要知道我们正在检查哪种类型的碰撞机,这意味着我们已经完全抽象出碰撞检测之外的不同形状的概念。

Struct Plane Collider:Const Collider{Vector tor3平面;浮动距离;CollisionPoints for TestCollision(const变换*t变换,const t Collider*t Collider,const变换*t colliderTransform)常量覆盖{return to collider->;TestCollision(colliderTransform,on this,Transform);}CollisionPoints to TestCollision(const t Transform*T Transform,}CollisionPoints for TestCollision(const t Transform*T Transform,);}CollisionPoints for TestCollision(const t Transform*T Transform,}CollisionPoints for TestCollision(const变换*平面,const Plane Collider*平面,const变换*平面转换)const覆盖{return{};n//n无任何平面vv平面};

在这样的情况下,有许多具有相似函数网络的类,可能会混淆实际代码所在的位置。Sphere v Sphere显然会在Sphere.cpp文件中,但Sphere v Plane可能在Sphere.cpp或Plane.cpp中,没有搜索就无法知道,当有很多文件时,这会很烦人。

为了解决这个问题,让我们创建一个ALGO命名空间,并将实际工作放入其中。我们需要为我们想要检查的每一对对撞机提供一个函数。我做了一个球体v球体,球体v平面,但不是平面v平面,因为它不是很有用。我不打算在这里介绍这些功能,因为它们本身不是设计的一部分,但是如果您感兴趣,可以查看源代码。

命名空间冲突点{CollisionPoints和FindSphereSphereCollisionPoints(const_SphereCollider*_a,const_ash_Transform*_ta,const_SphereCollider*_b,t_const_cTransform*btb);CollisionPoints_FindSpherePlane CollisionPoints(const_SphereCollider*_a,const_Transform*tta,const_Plane CollisionPoints);CollisionPoints for FindSpherePlane CollisionPoints(const_SphereCollider*_a,Const_Transform*Tta,const_Plane Collilita*ctb)。

您可以单独使用这些碰撞器,但最有可能的是将其附加到对象。我们将用对象中的变换替换位置。我们仍然只在动力学中使用位置,但是可以在碰撞检测中使用缩放和旋转。这里需要做出一个棘手的决定。现在我将使用转换指针,但是我们将在最后回到这一点,看看为什么这可能不是最好的选择。

一个好的设计实践是将复杂功能的所有不同方面分开,比如单步执行它们自己的功能。这使得代码更具可读性,因此让我们在物理世界中添加另一个名为ResolveCollisions的函数。

再说一次,我们有物理世界,我会压缩我们已经看过的部分,但有上下文是很好的。

Class PhysicsWorld{private:std::Vector<;Object*>;*m_Objects;Vector 3*m_grasion=1 Vector 3(0,0-9.81f,90);public:void和AddObject(Object*Object){/*......*/*}void RemoveObject(Object*Object)*{0/*Float...*/*}void Step(Float Tdt)。对于冲突(object*obj_Objects){/*...*/}}void ResolveCollisions(Float Dt){std::Vector<;Collision>;t;冲突;对于冲突(object*a):{m_Objects){对于对象(object*):{m_Objects){if(A)=1b)中断;if(A)=1b!A&>;碰撞机||!B-&>;Collider){Continue;}CollisionPoints TestCollision(a-&>;Transform,b-&>;Collider,b-&>;Transform);If(Points)(a-&>;Collider;Collider,b-&>;Transform);IF(Points)(a-&>;Transform,b-&>;Collider,b-&>;Transform);If(Points。HasCollision){碰撞。Emplace_back(a,bb,点数);}//解决冲突}};

这看起来不错,因为有了双重分派,除了对TestCollision的单个调用外,不需要任何其他操作。在for循环中使用Break可以为我们提供唯一的对,因此我们永远不会两次检查相同的对象。

只有一个恼人的警告,那就是因为对象的顺序是未知的,所以有时您会得到球体v平面检查,但其他时候会得到平面v球体检查。如果我们只是调用球体v平面的algo函数,我们会得到相反的答案,所以我们需要在平面对撞器中添加一些代码来交换碰撞点的顺序。

CollisionPoints与PlaneCollider::TestCollision(const变换*变换,const与SphereCollider*变换,const变换*SphereTransform);const{//n重用球体代码冲突点:Points=spsphere->;TestCollision(spherTransform,对此,变换);Vector 3:T=3个Points。A;b;//a您不可能用一架AALGO的飞机VV球体来做更多的交换点。A=1分。B;积分。B=T;分。正常分数=0分。正常;返回积分;}。

既然我们已经检测到冲突,我们需要一些方法来对其作出反应。

因为我们已经将不同形状的概念抽象为点,所以碰撞响应几乎是纯数学的。与我们刚刚经历的设计相比,设计相对简单;我们将从求解器的概念开始。解算器是用来解决有关物理世界的事情的。这可能是来自碰撞或原始位置修正的冲动,实际上是您选择实现的任何东西。

我们需要物理世界中的另一个列表来存储这些内容,以及添加和删除它们的函数。在我们生成碰撞列表之后,我们可以将其提供给每个求解器。

类PhysicsWorld{PRIVATE:STD::Vector<;Object*>;*m_Objects;std::Vector<;Solver*>;*m_solvers;Vector 3:m_gravitor3(0,0-9.81f,0);Public:void>AddObject解算器>(Object*Solver)>{/*Solver.../*/}void>RemoveObject(Object*Solver)>{/*>*/}void>AddSolver>(Solver*Solver)>{/**Solver.../*/}void>RemoveSolver(Solver*Solver)>{0/*Float*/}void Step(Float*DT)。{/*VolveCollisions(Float Tdt){std::Vector<;}void of ResolveCollisions(Float Tdt){std::Vector<;冲突>;解决冲突;对于解算器(Object*Collisions:Mm_Objects)和{/*解算器...*/}用于解算器(解算器*解算器ID:3m_solvers)和{解算器->;解算器(Collisions,DT);};

在上一节中,重点在于设计,这一节更倾向于您实现的解算器类型。我自己做了一个似乎适用于大多数情况的冲动和位置解算器。为简短起见,我不会在这里介绍数学知识,但是如果您感兴趣,可以在这里查看脉冲解算器的源代码,在这里查看位置解算器。

物理引擎的真正威力来自于您给用户的选项。在本例中,可以更改的选项不是太多,但是我们可以开始考虑要添加的不同选项。在大多数游戏中,您需要混合使用对象,其中一些模拟动态对象,另一些是静态障碍物。还需要触发器,这些对象不会经过碰撞响应,但会触发外部系统可以做出反应的事件,比如关卡结束标志。让我们进行一些较小的编辑,以便轻松配置这些设置。

我们能做的最大改变是区分模拟动力学的对象和不模拟动力学的对象。由于动态对象需要更多设置,所以让我们将这些设置与碰撞检测所需的设置分开。我们可以将对象分为CollisionObject和Rigidbody结构。我们将使Rigidbody继承自CollisionObject以重用碰撞器属性,并允许我们轻松地存储这两种类型。

我们只剩下这两个结构。Dynamic_cast可以用来判断CollisionObject是否真的是刚体,但会使代码稍微长一些,所以我喜欢添加布尔标志,即使它不被认为是最佳实践。我们还可以为对象添加一个用作触发器的标志和一个回调函数。既然这样,让我们通过保护原始价值来加强安全。

Struct for CollisionObject{Protected:Transform*Transform;Collider*Om_Collider;bool:m_isTrigger;bool for m_isDynamic;std::function<;void(Collision&;,t Float)>;;&onCollision;public://dgetters创建&;设置程序,而不是为isDynamic}设置程序。

我们可以为刚体添加更多设置。如果每个对象都有自己的重力、摩擦力和弹力性,则该选项非常有用。这为各种基于物理的效果打开了大门。在游戏中,你可以有一种能力,可以在一段时间内改变一个区域的重力。你可以让一些物体有弹性,也可以有一些像重力球这样的东西。地板可以是冰做的,对于更困难的挑战来说会很滑。

Struct to Rigidbody:*CollisionObject{private:Vector 3.m_grasion;*//*重力加速度向量3*m_force;**//*净力向量3*m_ocity;浮动:m_quality;bool:m_takesGravity;*//f如果我们的刚体不会从世界各地获取重力信息。Float:m_staticFriction;//n静态摩擦系数Float:m_DynamicFriction;n//s动态摩擦系数Float:m_reuittion;://s碰撞弹性系数Float Float(弹力)public://dgetters s&;{setters};

让我们将PhysicsWorld拆分为CollisionWorld和DynamicsWorld。我们可以将步长函数移动到DynamicsWorld中,并将ResolveCollisions移动到CollisionWorld中。这使那些不想要动态的人不必筛选对他们没有用处的函数。

我们可以对ResolveCollisions函数进行一些编辑,以赋予触发器正确的功能。让我们将函数拆分成几个部分,以保持其可读性。如果您想要程序范围内的事件,向世界添加回调也很有用。

类CollisionWorld{Protected:std::Vector<;CollisionObject*>;*m_Objects;std::Vector<;Solver*>;m_solvers;std::function<;void(Collision&;,浮点)>;m_onCollision;Public:void/AddCollisionObject解算器解算器(CollisionObject*Solver){/*Solver...*/}void RemoveCollisionObject(CollisionObject*Object){/*OVID RemoveCollisionObject(CollisionObject*Object){/*OVID RemoveCollisionObject(解算器*解算器)(解算器*解算器){/*解算器...*/}void RemoveSolver(解算器*解算器)*{/*...*/}void RemoveSolver(解算器*解算器)\{/*OVID。,(Float)>;&;(回调){0/*SolveCollisions(std::Vector<;Collision>;&;t;Collisions,Float){对于解算器(Solver*Solver):{m_solvers)和{solver->;solve(Collisions,Tdt);}}void和SendCollisionCallback(STD::Vector<;SendCollisionCallback){solver->;solve(Collisions,Tdt);}}void和SendCollisionCallback(std::Vector<;碰撞时间:碰撞){m_onCollision(Collision,.dt);auto&;a=a碰撞。对象->;OnCollision。

.