我们知道,在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;

}



Box2d中使用b2Separate开源代码创建凹多边形及其算法分析相关推荐

  1. 根据开源代码创建新的conda虚拟环境pytorch1.8、torchtext0.9

    我之前已经成功装过一次pytorch的最新版本,但在学习新的东西的时候,开源代码的要求和我现在的pytorch.torchtext版本是不适配的,因此需要用Anaconda重新创建一个虚拟环境,在里面 ...

  2. 最强辅助!IDA 辅助工具Karta——二进制文件中搜索开源代码

    介绍 " Karta"是IDA的python插件,其功能是在已经编译过的二进制文件中搜索是否使用了开源的代码.该插件是为了匹配大体积二进制文件中的开放源代码库的开源代码(通常是查找 ...

  3. 开源代码是下一轮攻击潮的重灾区

     聚焦源代码安全,网罗国内外最新资讯! 编译:代码卫士 专栏·供应链安全 数字化时代,软件无处不在.软件如同社会中的"虚拟人",已经成为支撑社会正常运转的最基本元素之一,软件的安全 ...

  4. ztext - 简单几行代码创建酷炫 3D 特效文字的开源 JS 库

    把网页上的文字变成酷炫的 3D 风格,还能制作旋转动效,有了 ztext.js,只需要几行代码. ztext 能做什么 ztext.js 是一个能把常规的平面文字变成 3D 样式的前端开源代码库,让开 ...

  5. eclipse中如何向开源中国(码云)上传代码

    摘要 本文将介绍如何将本地的项目提交到开源中国上去,过程比较详细,实现起来很简单.由于自己也算是一个新手,所以没有做过多的解释,只是单纯的描述了该如何去做. 1.在开源中国上面新建一个空项目 到这里也 ...

  6. 苹果开源代码中惊现“wechat”,老外注释的吐槽亮了!

    点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 每个科技大厂的开源项目,几乎都是各领域开发者最重要的研究学习 ...

  7. 【Android NDK 开发】Kotlin 语言中使用 NDK ( 创建支持 Kotlin 的 NDK 项目 | Kotlin 语言中使用 NDK 要点 | 代码示例 )

    文章目录 一.创建支持 Kotlin 的 NDK 项目 二.Kotlin 语言中使用 NDK 要点 1.加载动态库 2.声明 ndk 方法 3.Project 下的 build.gradle 配置 4 ...

  8. akka actor java_Akka:使用非默认构造函数在Scala中定义一个actor并从Java代码创建它 - java...

    Akka Scala演员必须扩展akka.actor.Actor Akka Java actor必须扩展akka.actor.UntypedActor 因此,在使用非默认构造函数定义Scala act ...

  9. 在Qt中使用C++代码创建界面

    好儿郎~志在四方 Qt视频教程地址:http://space.bilibili.com/84360636/#!/index 目录视图 摘要视图 订阅 图灵赠书--程序员11月书单    [思考]Pyt ...

最新文章

  1. 力扣(LeetCode)刷题,简单题(第9期)
  2. 图解 Attention
  3. /* * 编程第三题(20分) 打印所有的水仙花数。所谓水仙花数是指一个三位数,其各位数字的立方和等于该数本身。(例153=1*1*1+3*3*3+5*5*5) */
  4. 单图像三维重建、2D到3D风格迁移和3D DeepDream
  5. mysql行转列和列转行_mysql 行转列和列转行实例详解
  6. 16.实现多个具有相同方法的接口和父类与实现接口有相同方法
  7. strlen和mb_strlen的区别
  8. 防止多次提交的几个比较
  9. Oracle序列更新为主键最大值
  10. React基础篇(四)之创建组件方式分析
  11. C#中gridView常用属性和技巧介绍
  12. if条件判断C语言,if条件判断语句,谁能帮我分析一下?
  13. 网络安全基础(木马、概述、冰河木马实验)
  14. 华三交换机配置DHCP中继
  15. java冒泡排序(含冒泡排序代码)
  16. 三角形各种心的代数几何性质
  17. 没有大厂经验的前端可以这么写简历
  18. 新一代人工智能产业八大主要应用场景研判
  19. 如何做好数据分析报告(一)
  20. java中compliant是什么意思,compliant是什么意思中文翻译

热门文章

  1. hexo静态博客修改侧边栏
  2. Typecho博客搭建教程
  3. 分布式锁、ZK分布式锁、Redis分布式锁
  4. H5创建一个简单的qq注册页面
  5. 如何在javascript中生成1到10之间的随机数
  6. 用WPSOffice谱写动感乐章(转)
  7. Iconfont(矢量图标)+iconmoon(图标svg互转)配合javascript来打造属于自己的个性化社交分享系统
  8. 少有人走的路 读书笔记二
  9. 《Madame Curie》
  10. Java线程死亡(死掉、退出、挂掉)的几种情况