我们知道,在Box2d中默认只能创建凸多边形,如果我们定义的顶点不小心形成了一个凹多边形,那么凹面部分的法线会存在问题,并且在物理模拟的时候会有问题(比如检测不到碰撞等等)。
要想直接创建凹多边形肯定是不行了,那么有一种实现思路就是将凹多边形拆分成很多个凸多边形,每个凹多边形作为一个fixture添加到物体上即可。
说到拆分方法,就要提一下b2Separator,b2Separator是一个开源的切分凹多边形的算法,用于在Box2d中创建凹多边形。这里我们首先学习一下如何使用,然后来分析一下其中的算法。b2Separator可以到https://github.com/delorenj/b2Separator-cpp下载,作者AntoanAngelov。下载完成后解压得到三个文件,一个README.md,另外两个是我们要用到的文件,一个是b2Separator.cpp,另一个是b2Separator.h文件。
b2Separator的使用相对简单,这里我们通过一个小例子来说明其使用方法。
首先我们来创建一个cocos2d iOS withBox2d模板的工程(Box2d的版本是2.3.1),接着添加一些成员变量,并且实现touch相关的事件处理函数和注册代理的方法,并在draw中实现绘制多边形路径的代码,这部分内容请参考Box2d中使用开源的PRKit库来制作任意形状的多边形刚体的纹理,这篇教程中介绍了如何绘制任意多边形并为其填充我们制定的纹理,绘制多边形的部分就是本文要用到的。
接着我们在HelloWorldLayer中添加下面的方法:
-(void)createPolygon:(NSMutableArray*) vertexArray {
//创建一个b2Separator的实例
b2Separator* sep = new b2Separator();
//将顶点坐标转换为物体的本地坐标
vector<b2Vec2>* vertexes = [selfconvertToLocalVertexes:vertexArray];
//多边形的第一个顶点
b2Vec2 startVertex = [selftoVec2:[vertexArray[0] CGPointValue]];
//Validate方法用来检测顶点是否合法,返回值为0说明顶点
//符合创建条件,返回1说明边之间有交叉,返回2说明顶点
//不是按照逆时针排序的,返回3说明边有交叉且不是逆时针顺序
if (sep->Validate(*vertexes) == 0) {
//创建刚体
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
//刚体的位置为起始点的位置,其他点的坐标都是相对于这个点的坐标
bodyDef.position = startVertex;
b2Body* body =world->CreateBody(&bodyDef);
b2FixtureDef fixtureDef;
fixtureDef.density = 2.0f;
fixtureDef.friction = 0.2f;
//调用Separate方法,传入刚体,装置指针,顶点数组,米与像素的转换系数即可
sep->Separate(body, &fixtureDef,vertexes, PTM_RATIO);
}
}
该方法中使用到了下面的一些方法,也将他们添加到HelloWorldLayer中:
-(vector<b2Vec2>*)convertToLocalVertexes:(NSMutableArray*) vertexArray {
int vertexCount = [vertexArray count];
CGPoint basePoint = [vertexArray[0]CGPointValue];
std::vector<b2Vec2>* localVertexes =new std::vector<b2Vec2>();
for (int i = 0; i < vertexCount; i++) {
localVertexes->push_back([selftoVec2:ccpSub([vertexArray[i] CGPointValue], basePoint)]);
}
return localVertexes;
}
-(CGPoint)toCGPoint:(b2Vec2) vec {
return ccp(vec.x * (float)PTM_RATIO, vec.y* (float)PTM_RATIO);
}
-(b2Vec2)toVec2:(CGPoint) point {
b2Vec2 vec(point.x / (float)PTM_RATIO,point.y / (float)PTM_RATIO);
return vec;
}
看一下运行效果(注意绘制的时候要满足逆时针绘制,且边之间不要有交叉):
下面我们来分析b2Separator的核心函数calcShapes的算法。
我们使用图片来一步一步地说明其分割步骤。
首先我们假设绘制了如下的多边形(当然实际绘制的过程中,我们程序会使用相同间距进行取样,因此边长应该一致,但是不影响我们的讨论过程):
绘制的起点是v0,绘制到v7结束,共8个顶点。
在继续之前,请参考游戏中两个常用的数学运算推导及算法推论学习一下如何使用向量积来判定三个点的旋转方向。
首先将当前多边形的顶点依次加入到数组中,然后将该数组加入一个空白队列(这个队列用于临时存放未切割的多边形顶点数组,下面的描述中如果没有特殊指明队列用途,都是指这个队列)。接着开始循环:
首先弹出一个队列中的数组,即刚刚添加的数组,
接着从v0开始,顺次取3个顶点,因此第一次取得是v0,v1和v2,由于我们是逆时针绘制的,因此如果向量v0v1与向量v1v2的向量积系数为正,则v0,v1,v2是逆时针形成三角形,因此∠v0v1v2小于180度,继续执行。
(注:给定三个点 A(x1, y1),B(x2, y2),C(x3,y3),向量积系数为k=x1·y2+x2·y3+x3·y1-y1·x2-y2·x3-y3·x1)
这次三个取样点是v1,v2和v3,计算向量v1v2与向量v2v3的向量积系数为负,因此∠v1v2v3大于180度(注意:这里大于180度指的是多边形的内角),此时,做射线v1v2,与多边形的其他边交于p1,p2,p3……pn等若干个点,由于这里我们的多边形很简单,所以这里只有一个焦点,如果多边形比较复杂,则可能会有多个焦点,例如下面的图:
通过计算线段v2pi(i=1,2,……n)的长度,我们可以得到离v2最近的一个交点,即射线与多边形的边的第一个交点。我们回归到前面那个简单的多边形,设交点为p:
交点将多边形切割为两个新的多边形:v0-v1-pv-v7和v2-v3-v4-v5-v6-p,将两个多边形的顶点按照逆时针顺序保存在两个数组中,然后将两个数组添加到队列中。因为已经做过切割了,因此当前这个多边形不是凸多边形,因此抛弃该多边形。
接着,弹出队列中的下一个多边形顶点数组,即v0-v1-pv-v7,从点p开始逆时针遍历(之所以是从点p开始,是因为在切割的时候,新的多边形的两个数组的第一个元素是从切点开始添加的),经过计算,∠pv7v0,∠v7v0v1,∠v0v1p,∠v1pv7这4个内角都小于180度,因此判定多边形v0-v1-pv-v7为凸多边形,将其顶点数组加入到结果队列中保存。
再从队列中弹出多边形v2-v3-v4-v5-v6-p的顶点数组,同样从p开始遍历,∠pv2v3小于180度,继续,∠v2v3v4大于180度,需要切割,做射线v2v3交v5v6于q点:
将新切割出来的两个多边形v2-q-v6-p和v3-v4-v5-q加入到队列中,当前的多边形被切割过了,是凹多边形,丢弃掉。
继续从队列中弹出多边形v2-q-v6-p和v3-v4-v5-q,他们都是凹多边形,因此都加入到结果队列中。
最后我们就得到了结果队列中切割好的三个多边形了。
注:上面的过程中求解射线与边交点的算法请参考判断与求解平面内量线段的交点的算法与实现。
最后贴上注释过的calcShapes实现供大家参考:
voidb2Separator::calcShapes(vector<b2Vec2> &pVerticesVec,vector<vector<b2Vec2> > &result) {
vector<b2Vec2> vec;
//i n j都是循环变量 minLen用来比较得到射线与边的第一个交点
int i, n, j,minLen;
//d t dx dy都是临时变量
float d, t, dx, dy;
//i1 i2 i3为每次计算内角大于还是小于180度的三个点的下标
int i1, i2, i3;
//p1 p2 p3为下标i1 i2 i3对应的顶点
b2Vec2 p1, p2, p3;
//j1 j2为做射线切割时交点所在边的两个端点的下标
int j1, j2;
//v1 v2为下标j1 j2对应的顶点
b2Vec2 v1, v2;
//k h为循环中使用的临时变量
int k=0, h=0;
//vec1和vec2用来保存切割后得到的两个多边形的顶点数组
vector<b2Vec2> *vec1, *vec2;
//hitV为射线与边的交点,pV用来临时记录结果
b2Vec2 *pV, hitV(0,0);
//isConvec用来记录当前的多边形是否是凸多边形,如果是就加入到结果中
bool isConvex;
//figsVec用来保存结果
vector<vector<b2Vec2> >figsVec;
//queue用来保存待切割的顶点数组
queue<vector<b2Vec2> > queue;
queue.push(pVerticesVec);
while (!queue.empty()) {
vec = queue.front();
n = vec.size();
isConvex=true;
for (i=0; i<n; i++) {
i1=i;
i2=(i<n-1)?i+1:i+1-n;
i3=(i<n-2)?i+2:i+2-n;
p1 = vec[i1];
p2 = vec[i2];
p3 = vec[i3];
//计算行列式的值用来判断内角为钝角的情形(此时需要做射线进行分割)
d = det(p1.x, p1.y, p2.x, p2.y,p3.x, p3.y);
if ((d<0)) {
isConvex=false;
minLen = MAX_VALUE;
//当出现需要分割的情况时,遍历所有的边,判断哪一条边与射线p1p2相交
for (j=0; j<n; j++) {
if((j!=i1)&&(j!=i2)) {
j1=j;
j2=(j<n-1)?j+1:0;
v1=vec[j1];
v2=vec[j2];
//下面的hitRay方法判断v1v2是否与射线p1p2相交
pV =hitRay(p1.x,p1.y,p2.x,p2.y,v1.x,v1.y,v2.x,v2.y);
if (pV) {
b2Vec2 v = *pV;
dx=p2.x-v.x;
dy=p2.y-v.y;
t=dx*dx+dy*dy; //p2到交点的距离的平方
//当一个多边形比较复杂,射线p1p2与多条边相交的时候,通过minLen来找到最先相交的边
if ((t<minLen)){
h=j1;
k=j2;
hitV=v;
minLen=t;
}
}
}
}
//如果走下面这条语句,说明多边形没有任何边与射线p1p2相交(理论上不存在这种情况)
if (minLen==MAX_VALUE) {
//TODO: Throw Error !!!
}
//两个顶点数组用来保存分割成的两个多边形
vec1 = newvector<b2Vec2>();
vec2 = newvector<b2Vec2>();
j1=h;
j2=k;
v1=vec[j1];
v2=vec[j2];
//下面判断交点(射线与边的)是否和所在边的顶点是重合的(或者说离得足够近),如果是,就不添加到数组中以避免重复添加
if (!pointsMatch(hitV.x,hitV.y,v2.x,v2.y)) {
vec1->push_back(hitV);
}
if (!pointsMatch(hitV.x,hitV.y,v1.x,v1.y)) {
vec2->push_back(hitV);
}
h=-1;
k=i1;
//循环将下标从i1逆时针到j2的所有顶点(两个被切割的多边形之一)添加到数组中
while (true) {
if ((k!=j2)) {
vec1->push_back(vec[k]);
}
else {
if(((h<0)||h>=n)) {
//TODO: Throw Error!!!
}
if (!isOnSegment(v2.x,v2.y,vec[h].x,vec[h].y,p1.x,p1.y)) {
vec1->push_back(vec[k]);
}
break;
}
h=k;
if (((k-1)<0)) {
k=n-1;
}
else {
k--;
}
}
//将得到的顶点重新排序,按照逆时针方向排列
reverse(vec1->begin(),vec1->end());
//循环将下标从i2到j1得所有点都添加到另一个多边形的顶点数组中
h=-1;
k=i2;
while (true) {
if ((k!=j1)) {
vec2->push_back(vec[k]);
}
else {
if(((h<0)||h>=n)) {
//TODO: Throw Error!!!
}
if (((k==j1)&&!isOnSegment(v1.x,v1.y,vec[h].x,vec[h].y,p2.x,p2.y))) {
vec2->push_back(vec[k]);
}
break;
}
h=k;
if (((k+1)>n-1)) {
k=0;
}
else {
k++;
}
}
//将拆分后的两个多边形结果push到队列中继续拆解
queue.push(*vec1);
queue.push(*vec2);
queue.pop();
break;
}
}
//如果是凸多边形,则放入结果队列中
if (isConvex) {
figsVec.push_back(queue.front());
queue.pop();
}
}
result = figsVec;
}