转自Complex_ok
第04章 碰撞模块(Collision Module)
碰撞模块包含了形状, 和操作形状的函数。该模块还包含了动态树(dynamic tree)和broad-phase, 用于加快大型系统的碰撞处理速度。
形状描述了可相互碰撞的几何对象, 就算不进行物理模拟,也可独立使用。你可以在shape上执行一些操作。
b2Shape是个基类, Box2D的各种形状都实现了这个基类。此基类定义了几个函数:
• 判断一个点与形状是否有重叠
• 在形状上执行光线投射(ray cast)
• 计算形状的AABB
• 计算形状的质量
另外, 每个形状都有两成员变量: 类型(type)和半径(radius)。 对于多边形,半径也是有意义的, 下面会进行讨论。
圆形有位置和半径。
圆形是实心的,你没有办法使圆形变成空心。但是,你可以使用多边形来创建一系列线段,让这些线段首尾相连,串成一串,就可以模拟出空心的圆形。
b2CircleShape circle;
circle.m_p.Set(1.0f, 2.0f, 3.0f);
circle.m_radius = 0.5f;
Box2D的多边形是实心的凸(Convex)多边形。在多边形内部任意选择两点,作一线段,如果所有的线段跟多边形的边都不相交,这个多边形就是凸多边形。多边形是实心的,不可能空心。但是,你可以使用两个点来创建多边形,这样就退化成线段。
创建多边形时,使用的点必须是逆时针排列(CCW)。我们必须很小心,逆时针是相对于右手坐标系统来说的,这坐标系下,Z轴指向平面外面。有可能相对于你的屏幕,就变成顺时针了,这取决于你自己的坐标系统是怎么规定的。
多边形的成员变量具有public访问权限,但是你也应该使用初始化函数来创建多边形。初始化函数会创建法向量(normal vectors)并检查参数的合法性。
创建多边形时,你可以传递一个包含顶点的数组。数组大小最多是b2_maxPolygonVertices,这数值默认是8。这已足够描述大多数的凸多边形了。
// 按逆时针顺序定义一个三角形
b2Vec2 vertices[3];
vertices[0].Set(0.0f, 0.0f);
vertices[1].Set(1.0f, 0.0f);
vertices[2].Set(0.0f, 1.0f);
int32 count = 3;
b2PolygonShape polygon;
polygon.Set(vertices, count);
多边形有一些定义好的初始化函数来创建箱(box)和边(edge,也就是线段)。
void SetAsBox(float32 hx, float32hy);
void SetAsBox(float32 hx, float32hy, const b2Vec2& center, float32 angle);
void SetAsEdge(const b2Vec2& v1,const b2Vec2& v2);
多边形从b2Shape中继承了半径。通过半径,在多边形的周围创建了一个保护层(skin)。堆叠的情况下,此保护层让多边形之间保持稍微分开。这使得可以在核心多边形上执行连续碰撞。
(译注:这一段我不太明白,原文是
Polygons inherit a radius from b2Shape. The radius creates a skin around the polygon. The skin is used in stacking scenarios to keep polygons slightly separated. This allows continuous collision to work against the core polygon.)
你可以测试一个点是否与形状有所重叠。使用这个函数, 需要提供一个形状的变换以及世界坐标上的一个点。
b2Transfrom transform;
transform.SetIdentity();
b2Vec2 point(5.0f, 2.0f);
bool hit =shape->TestPoint(transform, point);
(译注: Box2D中,形状附加在物体之上,它存储的数据是在物体的局部坐标系下定义的,而传进来要测试的点是在世界坐标系下,坐标系不同,就没有办法比较。这个transform用于将形状从局部坐标系转到世界坐标系,之后才可进行比较。而transform的逆转换就是将世界坐标系转到局部坐标系。故要实现这个函数,也可以先求逆转换,将传过来的点转到局部坐标系,这同样可以进行比较,要看哪一个方便。
看Box2D的源码,它实现b2CircleShape::TestPoint时,是将圆心转成世界坐标系,再比较。而b2PolygonShape::TestPoint,是将传进来的点先转成局部坐标再比较。因为转成世界坐标,多边形要同时转换多个点,而圆形就只转换圆心。
使用局部坐标系,不管物体怎么移动,旋转及缩放,改变的只是这个转换矩阵,形状存储的点不用修改,这样就很方便了。下面文档中,你可以看到形状的很多函数,都会传进一个转换,道理是一样的。)
你可以用光线射向形状,得到它们之间的交点和法向量。如果在形状内部开始投射,就当成没有交点,返回false。
b2Transfrom transform;
transform.SetIdentity();
b2RayCastInput input;
input.p1.Set(0.0f, 0.0f);
input.p2.Set(1.0f, 0.0f);
input.maxFraction = 1.0f;
b2RayCastOutput output;
bool hit =shape->RayCast(&output, input, transform);
if (hit)
{
b2Vec2 hitPoint = input.p1 +output.fraction * (input.p2 - input.p1);
}
看看b2RayCastInput的定义,除指定了两个点p1, p2外,还有个maxFraction。这个maxFraction是什么意思呢? 我们知道,两点决定一个直线,在数学上知道了两点,再定义直线上的其它点,常使用参数方程。也就是定义 P(fraction) = p1 + fraction * (p2 - p1)。当fraction为0时,就代表p1, 当fraction=1时,就代表p2。这样的定义下,两点之间的线段就是参数从0到1之间变化。参数小于0,表示反向的点,大于1就表示正向超出线段的点。maxFraction就表示要测试的点对应的参数是在[0, maxFraction]内。b2RayCastOutput也有个fraction,意思是一样的。
数学上很喜欢将一些变量归结成0到1之间变化,这叫做规范化。处理问题的常用手段是用某个变换(这里说的变换是广义的)将变量归结成0到1之间,再在规范化之下计算, 之后再用个反变换得到原问题的答案。上面说的直线参数化,可以看成规范化的一种。那为什么要规范化呢?因为这样计算起来方便。那为什么会方便呢?我就答不出来了。Box2D中凡是涉及到向量的,那个单词fraction应该都是上面说的意思。)
碰撞模块含有对等函数,要传递两个形状,计算出结果。包括:
• 接触manifolds
• 距离
• 撞击时间
(译注: 我不知道Manifold应该翻译成什么,我猜Manifold指有相同属性的一堆东西,可以理解成集合,但翻译成集合又不好。故直接保留英文)
Box2D有些函数用来计算重合形状之间的接触点。考虑一下圆与圆,圆与多边形的碰撞,我们只会得到一个接触点和一个向量。多边形与多边形的碰撞,我们可以得到两个接触点。这些接触点都具有相同的法向量,所以Box2D就将它们归成一组,构成manifold结构。接触求解器进一步处理这结构,以改善物体堆叠在一起时,系统的稳定性。
通常你不需要直接计算接触manifold, 但你可能会希望使用这模拟过程中已处理好的结果。
b2Manifold结构含有一个法向量和最多两个的接触点。向量和接触点都是相对于局部坐标系。为方便接触求解器处理,每个接触点都存储了法向冲量和切向(摩擦)冲量。
b2WorldManifold结构可以用来生成世界坐标下的接触向量和点。你需要提供b2Manifold结构和形状的转换及半径。
b2WorldManifold worldManifold;
worldManifold.Initialize(&manifold,transformA, shapeA.m_radius,
transformB,shapeB.m_radius);
for (int32 i = 0; i <manifold.pointCount; ++i)
{
b2Vec2 point =worldManifold.points[i];
}
模拟过程中,形状会移动而manifold可能会改变。接触点有可能会添加或移除。你可以使用b2GetPointStates来检查状态。
b2PointState state1[2], state2[2];
b2GetPointStates(state1, state2,&manifold1, &manifold2);
if (state1[0] == b2_removeState)
{
// process event
}
b2Distance函数可以用来计算两个形状之间的距离。距离函数需要两个形状,转成b2DistanceProxy。There is also some caching used to warm start the distance function for repeated calls.(看不明白,见谅)。详细见b2Distance.h文件。
如果两个形状快速移动,它们可能会在一个时间步内穿过对方。
b2TimeOfImpact函数用于确定两个形状运动时碰撞的时间。这称为撞击时间(time of impact, TOI)。b2TimeOfImpact的主要目地是防止隧穿效应。特别是,它设计来防止运动的物体隧穿过静态的几何形状而出到外面。
这个函数考虑了形状的旋转和平移,但如果旋转足够大,这函数还是会错过碰撞。函数会报告一个非重叠的时间,并捕捉到所有的平移碰撞。
撞击时间函数最开始时定义了一条的分离轴,并确保形状没有超过这条轴。这可能会在结束位置错过一些碰撞。但这方法很快,在防止隧穿方面也已经足够了。
很难去限定旋转角的范围,有些情况下,就算是很小的旋转角也会导致错过碰撞。通常,就算错过了一些碰撞,也不会影响到游戏的好玩性。
这函数需要两个形状(转成b2DistanceProxy)和两个b2Sweep结构。b2Sweep结构定义了形状的开始和结束时的转换。
你可以在固定旋转角的情况下去执行这个计算撞击时间的函数,这样就不会错过任何碰撞。
Box2D使用b2DynamicTree来高效地组织大量的形状。这个类并不知道形状的存在。取而代之,它通过用户数据指针来操作轴对齐包围框(AABB)。
动态树是分层的AABB树。树的每个内部节点都有两个子节点。左边的子节点是用户的AABB。
这树结构支持了高效的光线投射(ray casts)和区域查询(region queries)。比如,场景中有数百个形状,你想对场景执行光线投射,如果采用蛮力,就需要对每个形状都进行投射。这是很低效的,并没有利用到形状的分布信息。替代方法是,你维护一棵动态树,并对树进行光线投射。在遍历树的时候,可以跳过大量的形状。
区域查询使用树来找到跟需查询的AABB有重叠的所有叶节点。这比蛮力算法高效得多,很多形状会被直接跳过。
通常你并不会直接用到动态树。你会通过b2World类来执行光线投射和区域查询。如果你想创建自己的动态树,你可以去看看Box2D是怎么使用动态树的。
物理步内的碰撞处理可以分成两个阶段: narrow-phase和broad-phase。narrow-phase时,我们去计算两个形状之间的接触点。假设有N个形状,使用蛮力算法的话,就需要执行 N*N/2次narrow-phase。
Tb2BroadPhase类使用了动态树来减少管理数据方面的开销。这可以大幅度减少narrow-phase的调用次数。
通常你不会直接和broad-phase交互。Box2D自己会在内部创建并管理broad-phase。另外要注意,b2BroadPhase是设计用于Box2D中的物理模拟,它可能不适合处理其它情况。