本文最后更新于:2 years ago

0. Pre-Everything

0.0 Reference

Blog:拉小登,上面有Box2D部分概念和应用

Erin Catto’s GDC Tutorials

Collision Detection in Interactive 3D Environments, Gino van den Bergen, 2004
Real-Time Collision Detection, Christer Ericson, 2005

1. Collision模块。

1.0 Shape

用于碰撞检测,在b2Fixture创建时会由b2World自动创建。

  • 第一种:通过fixture与body绑定,随body进行rigid moves。
  • 第二种:通过顶点坐标的形式存在于world中,只能通过手动设置坐标进行移动。

(1)分类:

enum Type { 
  e_circle = 0, e_edge = 1, 
  e_polygon = 2, e_chain = 3, 
  e_typeCount = 4 
}

(2)子类:b2ChainShape, b2CircleShape, …Edge…, …Polygon…。

(3)数据成员:m_type, m_radius。

(4)public成员函数:

image-20200709114719125

1.1 Circle

b2CircleShape circle;  //实心
circle.m_p.Set(2.0f, 3.0f); //圆心位置
circle.m_radius = 0.5f; //半径必须大于零

1.2 Polygon

Def: 实心凸(连接内部两点的线段不与边交叉)多边形,>=3边。

通过CCW(逆时针winding)填充:

多边形填充有两种模式:alternate和winding。

alternate:

显示器的每个扫描行都是从左到右扫描,系统只填充每个扫描行遇到的多边形的奇数边和偶数边之间的部分,不填充偶数边到奇数边之间的部分。

winding:

用一笔画一个多边形,分clockwise或counterclockwise。

画一条直线M,对于与它相交的线段:从直线M的左边到右边为clockwise,count+1;从直线M的右边到左边为counterclockwise,count-1。
判断一个区域E是否要被填充:从该区域画一线段M到整个多边型区域外,按照上面的方法对与该线相交的多边形的边进行count的计数,如果count非零则区域E要被填充,否则不填充。

从shape继承了radius,用于在多边形周围创造skin,防止tunneling。

这会导致形状之间有小gap,可以将visual表示做得大一点。

成员都是public的, 初始化可传入一个vertex数组:

///至多b2_maxPolygonVertices个顶点

class b2PolygonShape : public b2Shape
{
public:
	b2PolygonShape();
    
	/// Implement b2Shape.
	b2Shape* Clone(b2BlockAllocator* allocator) const;

	/// @see b2Shape::GetChildCount
	int32 GetChildCount() const;

	/// 用顶点数组创造多边形。
	/// 边数count属于[3, b2_maxPolygonVertices].
	/// 即时传入的点能构成多边形,点也可能被重排。
	/// 共线的点被处理但不被移除。
	void Set(const b2Vec2* points, int32 count);

	/// 以原点为中心,平行于坐标轴。
	/// hx the half-width,hy the half-height.
	void SetAsBox(float32 hx, float32 hy);

	/// hx the half-width,hy the half-height.
	/// center:中心坐标
	/// angle:旋转角度
	void SetAsBox(float32 hx, float32 hy, const b2Vec2& center, float32 angle);

	/// @see b2Shape::TestPoint
	bool TestPoint(const b2Transform& transform, const b2Vec2& p) const;

	// @see b2Shape::ComputeDistance
	void ComputeDistance(const b2Transform& xf, const b2Vec2& p, float32* distance, b2Vec2* normal, int32 childIndex) const;

	/// Implement b2Shape.
	bool RayCast(b2RayCastOutput* output, const b2RayCastInput& input,
					const b2Transform& transform, int32 childIndex) const;

	/// @see b2Shape::ComputeAABB
	void ComputeAABB(b2AABB* aabb, const b2Transform& transform, int32 childIndex) const;

	/// @see b2Shape::ComputeMass
	void ComputeMass(b2MassData* massData, float32 density) const;

	/// Get the vertex count.
	int32 GetVertexCount() const { return m_count; }

	/// Get a vertex by index.
	const b2Vec2& GetVertex(int32 index) const;

	/// 验证凸性,耗时很长。
	/// @returns true if valid
	bool Validate() const;

	b2Vec2 m_centroid; //初始为0
	b2Vec2 m_vertices[b2_maxPolygonVertices]; 
	b2Vec2 m_normals[b2_maxPolygonVertices];
	int32 m_count; //b2_polygonRadius
    //m_type = e_polygon;
	//m_radius = b2_polygonRadius;
};

1.3 Edge

线段,用于做静态的环境。能与circle,polygon碰撞,但不能与线段碰撞。

(这是由于碰撞系统要求两个物体至少一个有体积,线段无体积。)

b2Vec2 v1(0.0f, 0.0f), v2(1.0f, 0.0f);
b2EdgeShape edge;
edge.Set(v1, v2);  //初始化

通常游戏世界会涉及多条线段连接而成的线。当物体在其上滑动时,可能会与线段顶点发声ghost collision,造成internal collision normal。

于是有如下机制:使用命令

b2Vec2 v0-3(x0-3,y0-3);
b2EdgeShape edge;
edge.Set(v1, v2);

   edge.m_hasVertex0 = true;
   edge.m_hasVertex3 = true;
   edge.m_vertex0 = v0;
   edge.m_vertex3 = v3;

存入临近的ghost vertice,但这有点麻烦。

![image-20200708114649294](C:\Users\ybr19\Documents\100%_MOREHAB\Files\BLOGGER\source\img\blog\ghost collision.png)

于是又有如下机制:将边缝合为chain shapes。这可以防止ghost collision,并提供双边碰撞(two-sided collision, 我猜测指的是在两端的碰撞)。

Chain shape

// This a chain shape with isolated vertices
  b2Vec2 vs[4]; //Set them
  b2ChainShape chain;
  chain.CreateChain(vs, 4);

对于滚动的游戏世界,可以使用ghost vertices将chain连接在一起。

//Install ghost vertices
chain.SetPrevVertex(b2Vec2(3.0f, 1.0f));
chain.SetNextVertex(b2Vec2(-2.0f, 0.0f));

还可以创造loop

// first and last vertices are connected
b2ChainShape chain;
chain.CreateLoop(vs, 4);

代码不支持chain shape之间出现交叉,且vertices之间过近也可能导致问题,故最好保证边长于b2_linearSlop = 5mm。

/// A small length used as a collision and constraint tolerance. Usually it is
/// chosen to be numerically significant, but visually insignificant.
#define b2_linearSlop 0.005f

在chain中的边都被视作child shape,可以通过指标访问。当chain shape与body连接时,每个边在broad-phase碰撞树中都会得到自己的bounding box。

// Visit each child edge.
for (int32 i = 0; i < chain.GetChildCount(); ++i)
{
   b2EdgeShape edge;
   chain.GetChildEdge(&edge, i);
   …
}

1.5 Transform

transform类用于表示刚体的位置,在Math.h中定义。

数据成员:

b2Vec2 p; //translation
b2Rot q; //rotation
//Rot类含两个数据成员:旋转角的s和c。

1.4 几何查询

1.4.1 查询点与形状的重合

bool hit = shape->TestPoint(transform, point);

edge和chain(包括loop)总会返回false。

1.4.2 进行ray cast

ray cast(光线投射):

用于3D数据场的可视化,是体绘制中的一种处理方式:

向三维数据场投射出光线,然后沿着光线方向积分,数值化方法为由前往后或由后向前合成。

(不是很懂)

在这里指用一束光线打到shape上,得到第一个交点和对应的向量。若光线从形状内部触发,则标记为未击中。

image-20200709140659310

1.4.3 两个图形的重合

bool b2TestOverlap(const b2Shape *sA, int32 indexA, const b2Shape *sB, int32 indexB, const b2Transform &xfa, const b2Transform &xfb);
//index指对于chain shapes的child index。

1.4.4 Contact Manifold(接触流形)

LiquidFun有用于计算重合shape的接触点的函数。

normal vector: 法向量。

圆对圆/圆对多边形:一个点+一个法向量。

多边形对多边形:两个点+一个法向量,被统一在一个流形结构里。

b2Manifold类包含1*法向量 + <=2个接触点,均以local坐标系保存。每个点都存下了法向和切向(摩擦)的力。

b2Manifold是用于内部使用的类。用户最好使用b2WorldManifold来生成normal和points在world中的坐标,需要提供b2Manifold, shape transform 和 radius。

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检测。

image-20200709153724244

image-20200709153811976

1.4.5 距离

b2Distance函数用于计算两个形状之间的距离,该函数需要将shape转化为shape proxy,还会进行一定的缓存以准备重复调用。

/// Compute the closest points between two shapes. Supports any combination of:
/// b2CircleShape, b2PolygonShape, b2EdgeShape. The simplex cache is input/output.
/// On the first call set b2SimplexCache.count to zero.
void b2Distance(b2DistanceOutput* output,
				b2SimplexCache* cache, 
				const b2DistanceInput* input);

其中,input的具体信息如下:

/// Input for b2Distance.
/// You have to option to use the shape radii
/// in the computation. 
struct b2DistanceInput
{
	b2DistanceProxy proxyA;
	b2DistanceProxy proxyB;
	b2Transform transformA;
	b2Transform transformB;
	bool useRadii;
};

output的具体信息如下:

/// Output for b2Distance.
struct b2DistanceOutput
{
	b2Vec2 pointA;		///< closest point on shapeA
	b2Vec2 pointB;		///< closest point on shapeB
	float32 distance;
	int32 iterations;	///< number of GJK iterations used
};

SimplexCache的信息如下(不是很明白具体要传啥):

/// Used to warm start b2Distance.
/// Set count to zero on first call.
struct b2SimplexCache
{
	float32 metric;		///< length or area
	uint16 count;
	uint8 indexA[3];	///< vertices on shape A
	uint8 indexB[3];	///< vertices on shape B
};

1.4.6 Time of Impact

用于决定两个moving shapes的碰撞时间,防止tunneling。

该函数会考虑旋转和平移,但如果旋转角度过大,则函数可能遗漏碰撞。但函数仍然会报告不重合的时间,且会考虑所有的平移碰撞。

后面还有一大段讨论这个函数的限制等,没太明白,暂且跳过。

1.5 动态树

用于有效地管理大量shapes(但实际上它并不掌握shape的信息,而是在AABB上操作)。

该树就是AABB的层级树,每个内部节点有两个子节点,叶子是单用户的AABB。该树会使用旋转保持平衡,即便输入是退化的。

树的结构使得它能有效地执行ray cast和region query。一般我们不会直接使用该树。

同样,使用动态树进行pair management,可以对碰撞进行broad-phase检测。broad-phase一般也不会直接使用。

2. Dynamics模块

包含fixture, rigid body, contact, joint, world, listener等多个类,之间依赖复杂。

2.1 Bodies

  • Bodies有位置和速度;

  • 可以在其上施加力-foce、力矩-torque、冲量-impulse;

  • 可以是static, kinematic, dynamic。

Dynamic: 具有全套属性(有限质量、阻力),受重力和作用力的影响,可以和其他每个刚体碰撞,性能成本高、最具互动性。

Kinematic: 仅在明确的用户控制下移动(手动移动/速度),只能与Dynamic碰撞,不受重力和作用力的影响,成本低。碰撞下同样不动(无限质量:mass = inverse mass = 0)。

Static: 不动(具有无限质量)(除非用户手动移动),只能与Dynamic碰撞。

  • Bodies carry fixtures. 在LiquidFun中一定是刚体(一个Body上的两个fixture不会发生相对运动,不会碰撞)。
  • body从fixture处获得质量属性(可以override)。
  • 一般会对创建的body保存其指针,以便于查询和析构。

2.1.1 定义

通过 b2BodyDef(可循环使用) 定义一个body。

body有两个点比较重要:

  • 原点:fixture和joint都是以原点为参照附着的。
  • 质心:很多内部计算都要使用质心。

frame of reference: 信仰和准则,参照系。

Shapes are added to a body after construction.

    struct b2BodyDef
{

	///enum b2BodyType {
	///b2_staticBody = 0, b2_kinematicBody, b2_dynamicBody, b2_bulletBody,};
    /// 最好在创造时就确定,因为之后改变type花费巨大
	/// Note: if a dynamic body would have zero mass, the mass is set to one.
	b2BodyType type;

	/// The world position of body's 原点. 
    ///不要把body放在world的原点然后再移动!这会导致时间成本提高
	b2Vec2 position;

	/// The world angle of the body in radians.
	float32 angle;

	/// The 线速度 of the body's origin in world co-ordinates.
	b2Vec2 linearVelocity;

	/// The 角速度 of the body.
	float32 angularVelocity;

Damping – 阻尼

  • Damping与friction不同,friction仅在有接触时出现
  • damping系数应当>0=no damping && <infinity=full damping
  • 通常damping系数 0-0.1
  • linear damping会让body看着像飘起来
    ///线性damping是用于降低线速度,阻尼系数可以>1,但系数较大时效果对time step较为敏感
    float32 linearDamping;
    
    /// 角度damping是用于降低角速度,阻尼系数可以>1,但系数较大时效果对time step较为敏感
    float32 angularDamping;

Sleep:

将body送入sleep状态意味着停止模拟它,降低成本。

当:

  • 一个醒来的body和睡着的body碰撞时
  • body上附着的joint/contact被消灭时
  • 人工唤醒时

睡着的body会醒来。

/// 允许睡觉吗?
bool allowSleep;

/// 最初睡着/醒着?
bool awake;

Fixed Rotation

对于人物角色,你可能希望通过设置使其不能旋转。

若将fixedRotation设为true,则转动惯量和其逆都将被设为0。

/// 能转喵?
bool fixedRotation;

Bullets

游戏中常常需要生成一系列以一定帧率播放的图片,这也就是所谓的离散模拟。这种模拟中,刚体可能在一个time step里进行了大量移动,这可能会导致tunneling。

LiquidFun使用CCD来防止tunneling,也即:

  • 将物体从旧位置移动至新位置,寻找过程中的碰撞;
  • 计算碰撞的TOI;
  • 将物体移动到第一次TOI,然后剩余时间悬停;

一般来说在dynamic bodies之间不会使用CCD(降低成本),但你可能需要这样的操作(子弹穿过dynamic的砖)。

在LiquidFun中,你可以将高速运动的物体设置为bullets,这样他们就会与static, dynamic的物体都运行CCD。

注:static/kinematic bodies不会发生tunneling。

/// 高速且dynamic且要防止tunneling吗?
/// 少更改(很耗时)。
bool bullet;

Activation

创造一个物体,但它不参与碰撞/运动。

与sleeping类似,但不会被碰撞唤醒,它的fixture不会参与broad-phase检测。

不参与碰撞、ray cast等。

joint可以附着在未被激活的物体上,这些joint不会参与模拟。

激活一个joint没有变形的物体要小心(?)。

/// 激活了吗?
bool active;

用户数据

一个空指针,用于链接用户自己的特殊设置。

应当对所有用户数据使用同样的一类对象

/// Use this to store application specific body data.
void* userData;

Gravity Scale

用于单独调整某个物体受到的重力。增加重力可能影响稳定性。

    /// Scale the gravity applied to this body.
    float32 gravityScale;
};

2.1.2 Body Factory

我们使用world类提供的body factory生成和析构一个物体。

b2Body* dynamicBody = myWorld->CreateBody(&bodyDef);
/// DO SOMETHING
myWorld->DestroyBody(dynamicBody);
dynamicBody = NULL;

2.1.3 一些其他的注意事项

(1)手动移动static bodies:

  • 要注意不要压扁两个/多个static bodies之间的dynamic bodies。

  • Friction不能正常工作;

  • 将多个shape绑定到一个静态物体上要比创造多个静态物体,每个绑定一个shape要快。

  • 静态物体的质量和其逆都是0。

(2)删除Bodies:

  • LiquidFun allows you to avoid destroying bodies by deleting your b2World object, which does all the cleanup work for you. However, you should be mindful to nullify body pointers that you keep in your game engine. (?)
  • 删除Body时其上附着的fixture和joint也会被自动删除。

2.1.4 使用bodies

(一)质量数据

物体都具有:

  • 质量(标量)
  • 质心(2维向量)
  • 转动惯量(标量)

image-20200710112021317

静态物体的质量=转动惯量=0;当一个物体被设置为不能转动时,转动惯量也为0。

一般来说,物体的质量性质在其上添加fixture时自动生成,当然也可以在运行时使用以下命令改变。

void SetMassData(const b2MassData* data);

如此暴力改变后,你可以通过如下命令将其恢复为由fixtures决定的自然质量:

void ResetMassData();

可以通过一系列GetXxxx() const的函数获得质量数据。

(二)状态数据

void SetType(b2BodyType type);
b2BodyType GetType();

void SetBullet(bool flag);
bool IsBullet() const;

void SetSleepingAllowed(bool flag);
bool IsSleepingAllowed() const;

void SetAwake(bool flag);
bool IsAwake() const;

void SetActive(bool flag);
bool IsActive() const;

void SetFixedRotation(bool flag);
bool IsFixedRotation() const;

(三)位置和速度

bool SetTransform(const b2Vec2& position, float32 angle);
const b2Transform& GetTransform() const;

const b2Vec2& GetPosition() const;
float32 GetAngle() const;
//质心位置
const b2Vec2& GetWorldCenter() const; //in world coordinates
const b2Vec2& GetLocalCenter() const; //in local coordinates

获取到的线速度是针对于质心的。

2.2 Fixtures

形状是可以独立于物体存在的。b2Fixture类就专门负责将形状连接到物体上。

一个物体可以有0/多个fixtures,有多个fixtures的物体被称为复合物体。

fixtures包含以下组件:

  • 1*shape
  • broad-phase检测的代理
  • 密度,friction(摩擦),restitution(恢复系数)
  • collision filtering flags(?)
  • 指向parent body的指针
  • 用户数据
  • sensor flag

image-20200710130809558

2.2.1 创建fixture

使用如下命令创建fixture:

b2Fixture* myFixture = myBody->CreateFixture(&fixtureDef);

不需要保存fixture的指针,因为parent body被析构时会将其上附着的fixture顺带删除。

可以通过parent body删除其上的fixture(由此可以为能打碎的物体建模):

myBody->DestroyFixture(myFixture);

2.2.2 密度

fixture的密度是用于计算parent body的质量数据的。密度可以为0/正数。

最好对所有的fixtures都使用相似的密度,能提高效率。

改变密度时,body的质量并不会自动变化,必须调用下列命令

body->ResetMassData();

2.2.3 摩擦

  • LiquidFun支持静摩擦和动摩擦,但两者系数相同。

  • LiquidFun使用库仑摩擦力(即摩擦力与法向力成正比)。

  • 摩擦系数通常介于0-1之间,但可以是任何非负值。

  • 两个物体摩擦时,采用的摩擦系数为:sqrtf( a->friction * b->friction )

  • 上述默认的混合摩擦系数算法可以通过b2Contact::SetFriction改变,这是在b2ContactListener callback里完成的。

2.2.4 恢复系数

  • 用于让物体弹起来!
  • 通常设置在0-1之间:0-不弹,1-刚好弹回到原位;
  • 采用b2Max( a->restitution, b->restitution )进行混合(这样弹力球就可以在完全不弹的桌面上弹来弹去了)
  • 可以使用b2Contact::SetRestitution改变,是在…里完成的。
  • 当一个shape有多个contact时,回弹是近似模拟的。
  • 碰撞速度很小时,LiquidFun会使用非弹性碰撞防止jitter。

2.2.5 Filtering

image-20200710131000291

  • Filtering用于阻止特定fixture之间发生碰撞;
  • LiquidFun支持16个碰撞类(categories),对每个fixture你可以规定它属于哪个类,可以与哪些类碰撞。这是通过位屏蔽操作的

&1 不变; &0 变0。 |1 变1; |0 不变;

置位:变为1 复位:变为0

位屏蔽

屏蔽字中与要检查的位对应的位全部为1,而被屏蔽的位全部为0。

为了检查一个字节中的某些位,可以让这个字节和屏蔽字(bit mask)进行按位与操作(C的按位与运算符为&)。

为了置位所需的位,可以让数据和屏蔽字进行按位或操作(C的按位或运算符为|)。

为了清除所需的位,可以让数据和对屏蔽字按位取反所得的值进行按位与操作。

碰撞出现的法则如下:

//如果A被B屏蔽后还有其他位 且 B被A屏蔽后还有其他位
if ((catA & maskB) != 0 && (catB & maskA) != 0)
{
    // fixtures can collide
}
  • LiquidFun还支持碰撞组(group):正数index的同组物品总会碰撞,复数index的同组物品总不会碰撞。不同组之间也是通过category和mask bits来判断是否碰撞的(换而言之,group比category优先级更高)。

  • 还有一些其他的filtering会出现:

    • 静态/kinematic物体上的fixture只能与dynamic物体碰撞;
    • 同一物体上的fixture不碰撞;
    • 你可以启用/禁用通过joint连接的物体之上fixture的碰撞。
  • 若要在fixture创造后改变collision filtering , 你可以通过b2Fixture::GetFilterDatab2Fixture::SetFilterData来获取和设置b2Filter的结构。这直到下一个time step之前都不会增/减contacts。

2.2.6 Sensors

什么时候两个fixtures重叠但不发生碰撞呢?这是由sensors决定的。

sensors是一种专门用于检测碰撞、但不触发碰撞反应的fixture。

你可以把任一个fixture设置为sensor,可以是static/kinematic/dynamic。

记住,一个body可以有多个fixture,这可以任何solid fixture和sensor的混合。

sensor仅当至少一个body是动态的时候才会形成contact。

sensor不会生成接触点。

可以通过以下两种方式得知sensor的状态:

b2Contact::IsTouching

b2ContactListener::BeginContact and EndContact

2.3. Joints

Joints是用来约束bodies在世界中的状态/约束bodies之间的关系的,在游戏世界中比较典型的有Ragdolls(布娃娃)、teeter(跷跷板)、pulley(滑轮)。Joints可以以很多方式连接,形成有趣的动画。

有些Joints提供limits,使得运动受限。

有些Joints提供motors,可以控制joint运动的速度,直到施加的力/力矩超过预先设置的限度;可以控制位置(通过调整速度使其运动);可以模拟joint friction:将joint的速度设为0,提供一个较小、但能起决定作用的力/力矩,则motor会试图阻止joint运动,直到负载过重。

2.3.1 Joint的定义

image-20200710154221125

  • Joint是通过b2JointDef这个类及其衍生(见上)定义的;
  • joint连接了两个不同的物体,其中一个可以是静态的。在static, kinematic物体之间的joint是合法的,但没有效果且会占用运行时间;
  • 你可以为任一类关节指明用户数据,也可以提供flag允许相连的物体之间碰撞(默认情况下阻止)。
  • 大多jointdef都需要提供几何数据。通常joint是由anchor point(锚点:在相连物体中固定的点)的局部坐标定义的(因此,即使现在的物体变化违背了joint的限制,也同样可以定义/指明joint,这在游戏reload的时候是常见的)。
  • 有些jointdef还需要知道两个物体之间的默认相对夹角。
  • 大多数joint都有使用当前物体变化定义joint的初始化函数(因为提供几何数据非常麻烦),但这些函数只应被用于制作原型。

2.3.2 Joint Factory

使用world类中提供的factory来生产关节:

//Example: RevoluteJoint
b2RevoluteJointDef jointDef;
jointDef.bodyA/B = myBodyA/B;
jointDef.anchorPoint = myBodyA->GetCenterPosition();

b2RevoluteJoint* joint = (b2RevoluteJoint*)myWorld->CreateJoint(&jointDef);

//DO SOMETHING

myWorld->DestroyJoint(joint);
joint = NULL;  //防止太nasty的崩溃

重要:但凡关节连接的一个物体被摧毁,joint也会被摧毁!

2.3.3. 使用Joints

许多模拟创造了关节后,在摧毁前就不会再调用它们。当然,在关节中有许多有用的数据可以帮你让模拟变得更加生动。

首先,你可以获取物体、锚点、用户数据:

b2Body* GetBodyA();
b2Body* GetBodyB();

b2Vec2 GetAnchorA();
b2Vec2 GetAnchorB();

void* GetUserData();

所有关节都有reaction力/力矩,也即在锚点处被施加向body 2的力(见Distance Joint中的图)。可以使用反应力破坏关节,或触发其它游戏事件。

但这两个函数挺慢的,不要随便调用:

b2Vec2 GetReactionForce();
float32 GetReactionTorque();

接下来介绍各种奇形怪状的joint。

2.3.3.1 Distance Joint

最简单的joint,保持两个物体上各一点之间的距离恒定(应该在两个物体已经就位的情况下定义,指定两个锚点的世界坐标)。

image-20200710160614198

b2DistanceJointDef jointDef;
jointDef.Initialize(BodyA, BodyB, worldAnchorA, worldAnchorB);
jointDef.collideConnected = true;

Distance Joint也可以被做成柔性连接,就像弹簧阻尼那样。方法是调整定义中的两个常量:frequency和damping ratio。

frequency就像吉他弦的振动频率一样,以赫兹为单位,一般来说它是time step的频率的一半。(与Nyquist frequency有关,不查了/kkl)。

damping ratio是无量纲的,通常介于0-1之间,但可以更大。在此例中,damping是关键的(所有震动都应该消失)。

2.3.3.2. Revolute Joint(旋转关节)

旋转关节强迫两个物体共享一个锚点,通常被叫做hinge point(铰点)。旋转关节只有一个自由度:两个物体之间的相对旋转角度,也即joint angle。物体B绕锚点逆时针绕时角度为正,用弧度计算,初始化时角度默认为0。

image-20200710162449344

关节的初始化函数假设两个物体已经就位,如下例:

b2RevoluteJointDef jointDef;
jointDef.Initialize(myBodyA, myBodyB, myBodyA->GetWorldCenter());

你可以通过limit和motor来控制关节。

limit可以使得旋转角度落在一定区间内。一般来说,这个区间应当包括0,否则在模拟开始时关节将暴走。

motor可以允许你指定joint的角速度(角度对事件求导),可以为正可以为负。motor可以有无穷的力,但这不是我们所希望的,想想这个问题:

如果无穷的力遇上了不能动的物体怎么办?(超棒的矛和超棒的盾)

所以最好给motor提供一个力矩的上限。motor会保持以设定速度运行,除非维持这一状态所需的力矩超出了所设定的最大值,此时关节可能变慢/倒过来转。

你可以使用motor来模拟joint friction。把joint的速度设为0,把力矩上线设的小但别太小。负载超过这个值时关节就会开始运动。(改的例子见Joints这一节)。

通过一系列Get和Set函数获取/改变(每个time step都可以改)关节的角度、速度、力矩。

joint motor有非常有趣的特性,你可以每个time step都改变joint的速度,从而让它像钟摆一样动:

myJoint->SetMotorSpeed(cosf(0.5f * time));

也可以使用joint motor来追踪想要的角度:

float32 angleError = myJoint->GetJointAngle() - angleTarget;
float32 gain = 0.1f;
myJoint->SetMotorSpeed(-gain * angleError);
//gain别设太大
2.3.3.3 Prismatic Joint(平移关节)

平移关节允许两个物体沿着某个轴发生相对滑动,不允许发生相对转动。因此它也只有一个自由度。

image-20200710165307318

平移关节的定义和旋转关节很相似,只需要把角度换成平移、力矩换成力即可。由此可以得到平移关节带limit, motor的一个定义:

b2PrismaticJointDef jointDef;
b2Vec2 worldAxis(1.0f, 0.0f);
jointDef.Initialize(BodyA, BodyB, BodyA->GetWorldCenter(), worldAxis);

jointDef.lower/upperTranslation = -5.0f/2.5f;
jointDef.enableLimit = true;

jointDef.maxMotorForce = 1.0f;
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;

旋转关节有一个未被指明的轴朝向屏幕外。平移关节则需要指明一个在屏幕内的轴,轴在两个物体间被固定,且跟随他们运动。

默认初始化时,平移为0,所以平移range应当包含0。

一样,有一系列Get和Set。

2.3.3.4 Pulley Joint(滑轮关节)

滑轮关节可以创造一个理想化的滑轮,将两个物体与地面连接/互相连接。一个物体上升时另一个下降,滑轮绳的长度在一开始就配置好了。

image-20200710173334581

length1 + length2 = constant

也可以通过加一个比率来模拟滑轮组(高中物理,出现力),这会导致一边的绳比另一边延展的快,约束力在一遍比另一边小,可以用来创造杠杆

length1 + ratio * length2 = constant

例:ratio = 2时,length1的变化速度是length2的两倍,绳1的拉力是绳2的1/2。

如果一边绳子拉没了,滑轮可能会出问题。最好防止一下。

它的定义如下:

b2Vec2 anchor1/2 = myBody1/2->GetWorldCentor();
b2Vec2 groundAnchor1/2(x1/x2, y1/y2);
float32 ratio = 1.0f;

b2PulleyJointDef jointDef;
jointDef.Initialize(myBody1, myBody2, groundAnchor1, groundAnchor2, anchor1, anchor2, ratio);

可以用GetLengthA/B() const获取当前的长度。

2.3.3.5 Gear Joint(齿轮关节)

机械游戏当然需要齿轮啦!在LiquidFun里你可以使用复合形状来制作齿轮齿的模型,这有点慢且有点麻烦。你在排齿轮齿的时候也得小心,才能使得齿轮平滑地转动。

用齿轮关节制作齿轮就比较简单了。

image-20200710174517774

齿轮关节只能连接旋转 and/or 平移关节。

你可以定义一个齿轮ratio,可以是负数。还要注意:当一个是旋转一个是平移关节的时候,需要提供一个角度到长度(或者反过来)的单位转化。

coordinate1 + ratio * coordinate2 == constant

下面是一锅厘子:(bodyA/B是两个关节提供的任一个物体,不相同即可):

b2GearJointDef jointDef;
jointDef.bodyA/B = myBodyA/B;
jointDef.joint1/2 = myRevolute/PrismaticJoint;
jointDef.ratio = 2.0f * b2_pi / myLength;

注意到齿轮是依赖于之前那两个关节的,因此:

总是先删除齿轮,再删除其依赖的关节/物体。否则出事。

2.3.3.6 Mouse Joint(鼠标关节)

鼠标关节在testbed中被用于鼠标操控物体,它试图驱动物体上一点向光标的当前位置移动,并未对旋转做出限制。

mouse joint的定义中有一个目标点、力的上限、频率、阻尼ratio。

目标点最初与物体的锚点重合。

最大力是用于防止动态物体碰撞时产生触发过于激烈的反应,设置成多大都没关系。

频率、阻尼ratio是用于创造弹簧/阻尼的效果的,与distance joint中的情况一样。

用户常常希望将鼠标关节改造后用在游戏里,做到精准的位置放置和立刻的反应,但鼠标关节在这方面性能并不好。你可能需要考虑使用kinematic bodies

2.3.3.7 Wheel Joint(轮子关节)

轮子关节使得bodyB上的一点与bodyA上的一条线联系起来,并且提供了一个悬挂弹簧绳。

image-20200711110933763

2.3.3.8 Weld Joint(焊接关节)

weld joint限制了两个物体之间的所有相对运动。可以看testbed里面的Cantilever.h(悬臂)了解相关信息。

你也许会想用weld joint来定义能打破的结构,但在LiquidFun的实现下关节比较柔软,所以通过weld joints连接的chain of bodies可能会屈伸(flex)。

事实上,创造能打破的物体时,不如从一个物体开始,往上附着多个fixtures。在物体破坏时,你可以把fixture摧毁然后在另一个物体上重建这个fixture。具体可以看testbed里面的breakable example。

2.3.3.9 Rope Joint(绳子关节)

Rope Joint限制了两个点之间的最大距离,这可以有效地防止chain of bodies在过高的负载下延伸,详见b2RopeJoint.h + RopeJoint.h。

2.3.3.10 Friction Joint(摩擦关节)

Friction Joint是用于自顶向下的摩擦的,提供了2维的平移摩擦和角摩擦,详见b2FrictionJoint.h + ApplyForce.h。

2.4 Contacts

Contacts是用于管理两个fixtures之间的碰撞的。如果fixture有子对象(例如chain shape),则对每个相关的孩子都会存在一个contact。

contact有多种种类,都是b2Contact类的衍生类,用于管理不同种类fixtures之间的contact,比如有应付多边形-多边形碰撞的,也有应付圆-圆碰撞的。

先来看几个术语。

2.4.1. Terminology

Contact Point: 两个shapes接触的点。LiquidFun通过几个点来模拟contact。

Contact Normal:一个单位向量,常规情况下从fixtureA指向fixtureB。

Contact Separation:这是penetration的反面。当形状重合时separation是negative的,有可能未来版本会出现带positive separation的接触点,所以出现接触点的时候最好检查下符号。(啥?

Contact Manifold(流形,出现力)在两个凸多边形之间接触时可能会有两个接触点,它们共用同一个法向量,所以被组合在一个流形(=一个近似接触区域)里。

Normal Impulse:normal force(我猜是法向力)是在接触点处防止形状互相穿透的力。为了便利,LiquidFun使用冲量。法向冲量 = 法向力 * time_step

Tangent Impulse:切向力在接触点产生的(与摩擦有关)冲量。

Contact Ids

LiquidFun试图循环使用一个time step产生的接触力结果,作为下一个time step的初始猜测值。LiquidFun使用contact id在time step之间匹配接触点。这些id包含了几何特征的指标,以区分各个接触点。

contact在两个fixture的AABB重合的时候创建。有些时候collision filtering会阻止contact的创造。AABB停止重合的时候contact会被摧毁。

所以你可能会注意到,有些时候两个fixture不接触的时候contact也会被创造。这就是“鸡生蛋”的问题。如果contact对象不创建,我们就无法通过contact来分析碰撞,从而真正知道contact对象到底是不是需要被创建的。如果形状没接触,我们可以立刻把contact删掉,但也可以干脆等到AABB不重合了。LiquidFun决定采用后一种方式,效率更高。

2.4.2 Contact类

之前已经说过,contact类是由LiquidFun创造和消灭的,Contact对象并不由用户创造。当然,你可以获取其中信息并与其互动。

你可以访问raw contact manifold:

b2Manifold* GetManifold();
const b2Manifold* GetManifold() const;

理论上你可以修改manifold,但不资瓷,除非需要进阶用法。

还有一个helper function来访问b2WorldManifold:

void GetWorldManifold(b2WorldManifold* worldManifold) const;

它使用了物体的当前位置来计算接触点的世界坐标。

Sensors(不碰撞的fixture)不会创造manifold,所以你可以这样使用:(对于non-sensor也可以啦)

bool touching = sensorContact->IsTouching();

可以从contact那里获知fixture,进一步获知body的信息:

b2Fixture* fixtureA = myContact->GetFixtureA();
b2Body* bodyA = fixture->GetBody();
MyActor* actorA = (MyActor*)bodyA->GetUserData();

你也可以禁用contact,这只在b2ContactListener::PreSolve里面有用,见下文。

2.4.3 获取Contact

可以用几种方式获取contact。

(1)可以通过world/body的结构直接访问;

  • 可以遍历world里面所有的contact:

    for (b2Contact* c = myWorld->GetContactList(); c; c = c->GetNext())
    {
        // process
    }
  • 可以遍历一个body上产生的所有contact,他们都通过边连接存在一个图里:

    for (b2ContactEdge* ce = myBody->GetContactList(); ce; ce = ce->next)
    {
        b2Contact* c = ce->contact;
        //process
    }

(2)可以实现一个contact监视器类:见下文

注:使用world/body遍历可能会漏掉一些在time step中间短暂出现的contact,使用监视器能得到最为准确的结果。

2.4.4 Contact监视器

监视器支持几种不同的事件:开始,结束,pre-solve, post-solve。

class MyContactListener : public b2ContactListener
{
 public:
   void BeginContact(b2Contact* contact) {}
   void EndContact(b2Contact* contact) {}
   void PreSolve(b2Contact* contact, const b2Manifold* oldManifold) {}
   void PostSolve(b2Contact* contact, const b2Manifold* oldManifold) {}
};

注:不要保留送到b2ContactListener的指针的引用。你应该使用深拷贝将接触点的数据拷贝到你自己的buffer里,例子见下文。

在运行的时候,可以创造listener的一个对象,并且使用b2World::SetContactListener函数登记。确保在world对象的生命期内监视器都是可以访问的。

(1)开始:当两个fixture重合的时候调用,sensor和non-sensor都会调用。这类事件只会在time step内部出现。

(2)结束:当两个fixture停止重合的时候调用,sensor和non-sensor都会。如果一个body被摧毁的时候也会调用,所以可能在time step之外出现此类事件。

(3)Pre-Solve:在碰撞检测结束之后、碰撞解决之前调用。这给了用户根据当前配置禁用contact的机会。例如:你可以使用这个callback实现一个一边封闭的平台(如:只出不进),并且调用b2Contact::SetEnabled(false)。每次在处理碰撞的时候contact都会被重新启用,所以每个time-step都要禁用contact。由于碰撞检测是连续的,pre-solve事件可能会在每个time step每个contact被触发多次。

//一段不太明白的代码
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold)
{
    b2WorldManifold worldManifold;
    contact->GetWorldManifold(&worldManifold);
    
    //如果平台过于平了
    if (worldManifold.normal.y < -0.5f)
    {
        //就禁止接触?
        contact->SetEnabled(false);
    }
}

pre-solve事件也可以用来决定point state还有碰撞的来袭速度:

void PreSolve(b2Contact* contact, const b2Manifold* oldManifold)
{
    b2WorldManifold worldManifold;
    contact->GetWorldManifold(&worldManifold);
    b2PointState state1[2], state2[2];
    b2GetPointStates(state1, state2, oldManifold, contact->GetManifold);
    if (state2[0] == b2_addState)
    {
        const b2Body* bodyA/B = contact->GetFixtureA/B()->GetBody();
        b2Vec2 point = worldManifold.points[0];
        b2Vec2 vA/B = bodyA/B->GetLinearVelocityFromWorldPoint(point);
        float32 approachVelocity = b2Dot(vB - vA, worldManifold.normal);
        if (approachVelocity > 1.0f)
        {
            MyPlayCollisionSound();
        }
    }
}

(4)Post-Solve

​ post-solve事件中可以收集碰撞的冲量数据。

​ 你可能很想在游戏中基于contact的callback函数改变物理世界情况。例如,你可能想使得某个碰撞触发角色或刚体的受损。但LiquidFun不允许你在callback函数里更改物理世界的情况,因为你摧毁的物体可能正在被LiquidFun处理,这可能导致孤儿指针。

​ 建议的处理方法是:把所有的接触数据存下来,然后在time step之后再立刻处理。这时候你可以更改物理世界,但仍然要小心不要让接触数据缓冲区里出现孤儿指针。testbed里提供了不产生孤儿指针的安全的contact point处理的例子。

​ 下面这段代码展示了处理contact buffer的时候应当怎样处理orphaned bodies。这是一段摘要。所有的接触点都在b2ContactPoint数组m_points里存储。

//接下来通过接触点数据删除一些物体
//一定要缓存删除的物体的数据,因为可能会在多个接触点中存在,不保存会删除空指针
const int32 k_maxNuke = 6;
b2Body* nuke[k_maxNuke];
int32 nukeCount = 0;

//遍历contact缓冲,将接触物体比自己重的的物体删除
for (int32 i = 0; i < m_pointCount; ++i)
{
    ContactPoint* point = m_points + i;
    b2Body* bodyA/B = point->fixtureA/B->GetBody();
    float32 massA/B = bodyA/B->GetMass();
    if (massA > 0.0f && massB > 0.0f)
    {
        if (massB > massA)
        {
            nuke[nukeCount++] = bodyA;
        }
        else
        {
            nuke[nukeCount++] = bodyB;
        }
        if (nukeCount == k_maxNuke)
        {
            break;
        }
    }
    //排序,去重
    std::sort(nuke, nuke + nukeCount);
    int32 i = 0;
    while (i < nukeCount)
    {
        b2Body* b = nuke[i++];
        while (i < nukeCount && nuke[i] == b)
        {
            ++i;
        }
        m_world->DestroyBody(b);
    }
}

2.4.5 Contact Filtering

这是用来阻止部分物体之间的互动的。(例如,创造一个门,只有特定的角色能穿过)

LiquidFun允许你通过实现b2ContactFilter类来自定义contact filtering。这个类要求你实现一个ShouldCollide函数,接收两个b2Shape指针,返回是否该碰撞。

默认实现使用了之前定义过的b2FilterData。

bool b2ContactFilter::ShouldCollide(b2Fixture* fixtureA, b2Fixture* fixtureB)
{
    const b2Filter& filterA/B = fixtureA/B->GetFilterData();
    if (filterA.groupIndex == filterB.groupIndex && filterA.groupIndex != 0)
    {
        return filterA.groupIndex > 0;
    }
    bool collide = (filterA.maskbits & filterB.categoryBits) != 0 && (filterA.categoryBits & filterB.maskbits) != 0;
    return collide;
}

你可以在运行的时候创造一个你自己的contact filter,然后使用b2World::SetContactFilter来注册。请确保在world生命期内它始终可以访问。

world->SetContactFilter(&filter);

2.5 World类

b2World类包含了物体、关节,负责了模拟的所有部分,允许非共时的请求(AABB/ray cast/etc.)。你绝大部分的互动都是与b2World对象进行的。

2.5.1 创造/消灭一个世界

//doSleep = 物体能睡觉吗?
b2World* myWorld = new b2World(gravity, doSleep);
// do something
delete myWorld;

2.5.2 使用世界

世界类包含了创造、摧毁物体、关节的工厂。

2.5.3 模拟

指定time step,指定速度、位置的迭代数目。

float32 timeStep = 1.0f / 60.f;
int32 velocityIterations = 10;
int32 positionIterations = 8;
myWorld->Step(timeStep, velocityIterations, positionIterations);

在每个time step之后你都可以检查你的物体、关节,获取信息。一般你都会从物体处获得位置信息,以渲染他们。你可以在你的游戏循环的任意位置启动一个time step,但要注意做事情的顺序。

例如,你应该time step之前创造物体,这样才能获取他们的碰撞数据。

最好使用固定的time step。time step越大,在低帧率的场景里游戏的性能表现就越好。但一般来说,time step不应当超过1/30秒。1/60秒的time step可以使得模拟质量较高。

迭代次数控制了constraint solver将遍历contact和joint多少次。迭代越好,模拟效果肯定更好。但不要将time step变大以增加迭代次数,60Hz + 10次迭代要比 30Hz + 20次迭代效果好得多。

在stepping之后,应该把施加在物体上的力清零。这是通过b2World::ClearForces这个命令完成的。这可以让你在几个sub-step内都使用相同的力场。

2.5.4 探索世界吧!

世界是由物体、接触、关节组成的。你可以从world这里获取它们的列表并遍历。例如:

//叫醒所有物体
for (b2Body* b = myWorld->GetBodyList(); b; b = b->GetNext())
{
    b->SetAwake(true);
}

2.5.5. AABB查询

有些时候你想要查询一个区域内的所有图形。b2World类提供了一个log(N)的使用broad-phase数据结构的函数。你可以提供一个世界坐标下的AABB,并且实现b2QueryCallback。世界会对于每个与你提供的AABB重合的AABB的fixture调用你提供的类。返回true的时候查询继续,返回false的时候查询中止。

例如:如下代码找到所有可能分割了你提供的AABB的fixture,然后把它们相关的物体都唤醒:

class MyQueryCallback : public b2QueryCallback
{
  public:
    bool ReportFixture(b2Fixture* fixture)
    {
        b2Body* body = fixture->GetBody();
        body->SetAwake(true);
        return true;
    }
}
...
MyQueryCallback callback;
b2AABB aabb;
aabb.lowerBound.Set(-1.f, -1.f);
aabb.upperBound.Set(1.f, 1.f);
myWorld->Query(&callback, aabb);

不要随意猜测、假设callback的顺序。

2.5.6 Ray Cast

你可以用ray cast进行视线检查、开枪等操作。通过实现一个callback类 + 提供开始和结束点,就可以进行一次ray cast操作。

world类会对每个被光线束击中的fixture调用你所提供的类。你的callback函数能调用的数据是:fixture,分割点,单位法向量,分割下来的沿光线束距离(小数)。不要随意猜测、假设callback的顺序。

你将返回一个小数,来控制ray cast是否继续。返回0意味着ray cast中止,返回1意味着ray cast将假装没有击中的fixture,返回之前传入的小数意味着光线将被截断在当前的分割点。这样,你就可以ray cast一个图形/所有图形/最接近的图形(通过调整返回的分数)。

甚至还可以返回-1,使得光线忽略这一个fixture,假装被击中的这个fixture不存在。

例子如下:

//获取最近的形状
class MyRayCastCallback : public b2RayCastCallback
{
  public:
    MyRayCastCallback()
    {
        m_fixture = NULL;
    }
    float32 ReportFixture(b2Fixture* fixture, const b2Vec& point, const b2Vec2& normal, float32 fraction)
    {
        m_fixture = fixture;
        m_point = point;
        m_normal = normal;
        m_fraction = fraction;
        return fraction;
    }
    b2Fixture* m_fixture;
    b2Vec2 m_point;
    b2Vec2 m_normal;
    float32 m_fraction;
}
MyRayCastCallback callback;
b2Vec2 point1(-1.0f, 0.0f);
b2Vec2 point2(3.0f, 1.0f);
myWorld->RayCast(&callback, point1, point2);

由于近似的原因,ray cast可能会在你的静态环境中从多边形之间的小缝隙之间穿过去。如果这不行,请把你的多边形适当调大。

2.5.7 力和冲量

你可以在物体上应用力、力矩、冲量。当你应用力/冲量的时候,要提供负载应用的地点的世界坐标。这通常会导致一个质心上的力矩。

void ApplyForce(const b2Vec2& force, const b2Vec2& point);
void ApplyTorque(float32 torque);
void ApplyLinearImpulse(const b2Vec2& impulse, const b2Vec2& point);
void ApplyAngularImpulse(float32 impulse);

应用力/力矩/冲量都会唤醒物体,有些时候你不想这样(例如,提供一个恒定力,但希望物体睡着提供效率)。这个时候可以这样做:

if (myBody->IsAwake() == true)
{
    myBody->ApplyForce(myForce, myPoint);
}

你可以给粒子/粒子组提供力/冲量,但你并不能在任一个点提供。事实上,力/冲量将被应用在每个粒子的中心上。

myParticleGroup->ApplyLinearImpulse(impulse);

2.5.8 坐标转换

body类为你提供了点、向量在局部、世界坐标之间的转换。不明白这两个概念的话(?是我)请读*”Essential Mathematics for Games and Interactive Applications* by Jim Van Verth & Lars Bishop。这些函数在内联下很快:

b2Vec2 GetWorldPoint(const b2Vec2& localPoint);
b2Vec2 GetWorldVector(const b2Vec2& localVector);
b2Vec2 GetLocalPoint(const b2Vec2& worldPoint);
b2Vec2 GetLocalVector(const b2Vec2& worldVector);

2.5.9 列表

你可以遍历一个body上的fixture。这主要是在你需要访问fixture的用户数据的时候管用:

for (b2Fixture* f = body->GetFixtureList(); f; f = f->GetNext())
{
    MyFixtureData* data = (MyFixtureData*)f->GetUserData();
    //DO SOMETHING
}

遍历一个物体的关节list的方式也类似。

body也提供了一个相关contact的列表。但要小心,因为列表里不一定包含了所有在上一个time step存在过的contact。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

Comment: Notes on the ALBUMs Previous
Diary: 2020/07 Next