使用Box2d实现物体在液体中的漂浮效果(二)

news/2024/7/20 20:52:53 标签: IOS, box2d, 浮力, 液体


我们继续来制作物体在液体中的漂浮效果。

我们来考虑物体和液体的三种位置关系:

1.    物体完全离开液体

2.    物体一部分浸入液体

3.    物体完全浸入液体

针对这三种位置关系,我们有下面三种结论:

1.      物体完全离开液体,物体与液体的表面没有交点,且ContactListener不检测物体与液体的接触,因此物体的UserData的isUnderWater属性为false,volumnUnderWater为0

2.      物体一部分浸入液体,物体与液体表面有交点,UserData的isUnderWater为true,volumnUnderWater大于0

3.      物体完全浸入液体,物体与液体表面没有交点,UserData的isUnderWater为true,volumnUnderWater为0

你可能会疑问为什么第三种情况中物体完全浸入液体volumnUnderWater为0,这和我们的算法有关,由于想要精确计算物体浸入液体的体积,我们需要使用射线投射(RayCast)来检测物体与液体表面的交点,如下图:


我们使用穿过液体表面的射线对物体进行两个方向的投射,得到物体与液体的两个交点(如果没有交点,则说明液体完全浸入液体中或者完全离开液体),再利用得到的两个交点与物体在液体下方的顶点组成的多边形求解其浸入液体中的面积,因此如果没有交点,物体浸入液体的面积我们就认为是0。

应用上面的三种情况我们已经能够对物体状态进行区分了,由于物体只有浸入液体的时候才受到浮力,因此首先判断物体是否在液体中,如果在液体中,判断volumnUnderWater是否为0,如果为0,则通过物体的质量和密度的比值求出其整个的体积,不为0的话,volumnUnderWater就是其浸入液体中的体积。最后再根据浮力计算公式来求解其受到的浮力大小。

首先定义类MyRayCastCallback类,继承自b2RayCastCallback:

#import"Box2D.h"

 

classMyRayCastCallback : public b2RayCastCallback {

public:

   NSMutableArray* results;

   NSMutableArray* endResults;

   BOOL resultFlag;

   

   MyRayCastCallback();

   

   float32 ReportFixture(b2Fixture *fixture,const b2Vec2 &point, const b2Vec2 &normal, float32 fraction);

   

   void ClearResults();

   void ResetFlag();

};

实现:

#import"MyRayCastCallback.h"

#import"RayCastResult.h"

 

MyRayCastCallback::MyRayCastCallback(){

   results = [[NSMutableArray alloc] init];

   endResults = [[NSMutableArray alloc] init];

   resultFlag = true;

}

 

float32MyRayCastCallback::ReportFixture(b2Fixture *fixture, const b2Vec2 &point,const b2Vec2 &normal, float32 fraction) {

   if (resultFlag) {

       [results addObject:[[RayCastResultalloc] initWithFixture:fixture point:point normal:normal fraction:fraction]];

   } else {

       [endResults addObject:[[RayCastResultalloc] initWithFixture:fixture point:point normal:normal fraction:fraction]];

   }

   return 1;

}

 

voidMyRayCastCallback::ClearResults() {

   [results removeAllObjects];

   [endResults removeAllObjects];

}

 

voidMyRayCastCallback::ResetFlag() {

   resultFlag = !resultFlag;

}

关于射线投射的原理和使用请参考Box2D中切割刚体效果的实现一览(二),我们上面的这部分实现也是从其中截取出来的。其中RayCastResult类的声明和实现如下:

#import"Box2D.h"

 

@interfaceRayCastResult : NSObject

 

-(id)initWithFixture:(b2Fixture*)fixture

               point:(b2Vec2) point

              normal:(b2Vec2) normal

            fraction:(float32) fraction;

 

@propertyb2Fixture* fixture;

@propertyb2Vec2 point;

@propertyb2Vec2 normal;

@propertyfloat32 fraction;

 

@end

实现:

#import"RayCastResult.h"

 

@implementationRayCastResult

 

@synthesizefixture;

@synthesizepoint;

@synthesizenormal;

@synthesizefraction;

 

-(id)initWithFixture:(b2Fixture*)fixt point:(b2Vec2)p normal:(b2Vec2)n fraction:(float32)f {

   if (self = [super init]) {

       self.fixture = fixt;

       self.point = p;

       self.normal = n;

       self.fraction = f;

   }

   

   return self;

}

 

@end

定义好之后,我们在HelloWorldLayer中添加下面的投射方法:

-(void)doRayCast{

   CGSize size = [[CCDirector sharedDirector]winSize];

   float waterHeight = size.height * 0.4f /PTM_RATIO;

   b2Vec2 waterSurfaceStart(0, waterHeight);

   b2Vec2 waterSurfaceEnd(size.width /PTM_RATIO, waterHeight);

   rayCastCallback.ClearResults();

   world->RayCast(&rayCastCallback,waterSurfaceStart, waterSurfaceEnd);

   rayCastCallback.ResetFlag();

   world->RayCast(&rayCastCallback,waterSurfaceEnd, waterSurfaceStart);

   rayCastCallback.ResetFlag();

}

这个方法在液体表面从左到右和从右到左做两次投射,将结果存储到HelloWorldLayer里我们添加的成员变量中:

MyRayCastCallbackrayCastCallback;

有了投射的方法,我们需要一个方法来根据投射的结果更新物体的状态(isUnderWater和volumnUnderWater),方法如下:

-(void)updateObjectData {

   //遍历两个方向投射得到的交点

   for (RayCastResult* startResult inrayCastCallback.results) {

       for (RayCastResult* endResult inrayCastCallback.endResults) {

           //判断是否是同一个装置(即同一个物体)

           if (startResult.fixture ==endResult.fixture) {

               b2Body* body =startResult.fixture->GetBody();

               //获取物体的UserData,如果UserData不是FloatingObjectData对象,则跳过这个物体

               FloatingObjectData* objectData= (FloatingObjectData*)body->GetUserData();

               if (objectData == nil) {

                   continue;

               }

               

               //得到物体的形状

               b2PolygonShape* shape =(b2PolygonShape*)startResult.fixture->GetShape();

               //物体的顶点数

               int vertexCount =shape->GetVertexCount();

               

               //获取两个投射点的坐标(坐标转换为物体的本地坐标系)

               CGPoint cutPointA = [selftoCGPoint:body->GetLocalPoint(startResult.point)];

               CGPoint cutPointB = [selftoCGPoint:body->GetLocalPoint(endResult.point)];

               

               //判断两个投射点是不是同一个(如果正好和物体相切于1点,那么就只有一个交点)

               if (cutPointA.x == cutPointB.x){

                   //如果只有一个交点,跳过该物体,物体的下一个状态要么是没有交点,要么是有2个交点,到时再判断

                   continue;

               } else {

                   //定义一个数组用来存投射得到的两个点

                   NSMutableArray*underWaterVertexes = [[NSMutableArray alloc] init];

                   

                   //将两个投射点先添加到数组中

                   [underWaterVertexesaddObject:[NSValue valueWithCGPoint:cutPointA]];

                   [underWaterVertexesaddObject:[NSValue valueWithCGPoint:cutPointB]];

                   

                   //遍历物体原来的顶点,将液体中的点添加到数组中

                   for (int i = 0; i <vertexCount; i++) {

                       CGPoint vertex = [selftoCGPoint:shape->GetVertex(i)];

                       

                       //根据行列式的计算结果来确定顶点在液体平面的顺时针一侧还是逆时针一侧(顺时针一侧为液体内部的点)

                       float checkResult =[self calculateDet:cutPointA pointB:cutPointB pointC:vertex];

                       

                       //将符合条件的点添加到数组中

                       if (checkResult < 0){

                           [underWaterVertexesaddObject:[NSValue valueWithCGPoint:vertex]];

                       }

                   }

                   //对顶点进行排序

                   underWaterVertexes = [selfreorderVertexes:underWaterVertexes];

                   //计算液体内部的物体面积

                   objectData.volumnUnderWater= [self calculatePolygonArea:underWaterVertexes];

                   objectData.isUnderWater =true;

               }

           }

       }

   }

}

该方法根据投射的结果更新了所有与液体表面相交的物体的volumnUnderWater属性,方法中添加了详细的注释,在循环的内部有一个calculatePolygonArea方法,用来计算液体内部物体的面积(体积),关于根据凸多边形顶点坐标来计算其面积的算法,请参考根据凸多边形顶点坐标来计算面积算法与实现。下面是涉及到的三个方法:

-(float)calculateTriangleArea:(CGPoint) pointA

                       pointB:(CGPoint) pointB

                       pointC:(CGPoint) pointC{

   float result = [self calculateDet:pointApointB:pointB pointC:pointC] * 0.5f;

   return result > 0 ? result : -result;

}

 

-(float)calculatePolygonArea:(NSMutableArray*) vertexes {

   float result = 0;

   int vertexCount = [vertexes count];

   CGPoint startPoint = [vertexes[0]CGPointValue];

   for (int i = 1; i < vertexCount - 1;i++) {

       result += [selfcalculateTriangleArea:startPoint pointB:[vertexes[i] CGPointValue]pointC:[vertexes[i+1] CGPointValue]];

   }

   

   return result / (PTM_RATIO * PTM_RATIO);

}

 

-(float)calculateDet:(CGPoint) pointA

              pointB:(CGPoint) pointB

              pointC:(CGPoint) pointC {

   return pointA.x * pointB.y + pointB.x *pointC.y + pointC.x * pointA.y

   - pointA.y * pointB.x - pointB.y * pointC.x- pointC.y * pointA.x;

}

顶点的排序方法如下(排序算法请参考Box2D中切割刚体效果的实现一览(完)):

-(NSMutableArray*)reorderVertexes:(NSMutableArray*)vertexes {

   int vertexCount = [vertexes count];

   NSMutableArray* tmpVertexes =[[NSMutableArray alloc] initWithArray:vertexes copyItems:true];

   [vertexes sortUsingComparator:^(id obj1, idobj2) {

       if ([obj1 CGPointValue].x > [obj2CGPointValue].x) {

           return(NSComparisonResult)NSOrderedDescending;

       }

       if ([obj1 CGPointValue].x < [obj2CGPointValue].x) {

           return(NSComparisonResult)NSOrderedAscending;

       }

       return(NSComparisonResult)NSOrderedSame;

   }];

   CGPoint left = [vertexes[0] CGPointValue];

   CGPoint right = [vertexes[vertexCount - 1]CGPointValue];

   int leftPos = 1;

   int rightPos = vertexCount - 1;

   tmpVertexes[0] = vertexes[0];

   for (int i = 1; i < vertexCount - 1;i++) {

       if ([self calculateDet:leftpointB:right pointC:[vertexes[i] CGPointValue]] > 0) {

           tmpVertexes[rightPos--] =vertexes[i];

       } else {

           tmpVertexes[leftPos++] =vertexes[i];

       }

   }

   tmpVertexes[leftPos] = vertexes[vertexCount- 1];

   return tmpVertexes;

}

上面的方法添加完成后,我们在update方法的最后添加上下面的代码:

[selfdoRayCast];

[selfupdateObjectData];

for (b2Body*body = world->GetBodyList(); body; body = body->GetNext()) {

   FloatingObjectData* objectData =(FloatingObjectData*)body->GetUserData();

   if (objectData == nil) {

       continue;

   }

       

   if (objectData.isUnderWater) {

       b2Vec2 bodyVelocity =body->GetLinearVelocity();

       body->SetLinearVelocity(b2Vec2(bodyVelocity.x * 0.999f,bodyVelocity.y * 0.99f));

       body->SetAngularVelocity(body->GetAngularVelocity() * 0.99f);

       float volumn =objectData.volumnUnderWater > 0 ? objectData.volumnUnderWater :body->GetMass() / body->GetFixtureList()->GetDensity();

       float waterForce = 1.0f *fabs(world->GetGravity().y) * volumn;

       body->ApplyForceToCenter(b2Vec2(0,waterForce));

   }

}

这部分代码就比较简单了,首先做射线投射,利用投射结果更新物体状态,然后遍历世界中的所有物体,对于在液体中的物体,根据浮力计算公式来计算它受到的浮力大小,同时,物体在液体由于阻力的存在,它的角速度和线速度也会按照一定的比例变慢,这里我们用了两个系数0.99和0.999来控制,经过调试,这两个系数的模拟效果还比较不错。

好了,制作完成,运行一下,是不是和我们一开始的截图一样了呢?

如果有问题欢迎留言讨论。



http://www.niftyadmin.cn/n/878452.html

相关文章

当前可用的喜马拉雅专辑下载器

之前分享过不少下载喜马拉雅音频或专辑的工具&#xff0c;但是都一一失效了&#xff0c;找和分享都赶不上失效的快&#xff0c;索性就停了一段时间。但是老用车机和手机听歌也不是那么回事&#xff0c;多多少少还是想听点故事评书什么的&#xff0c;所以今天就又花了点时间&…

连接池配置_HikariCP被号称为性能最好的Java数据库连接池,如何配置使用?

公众号关注 “GitHub今日热榜”设为 “星标”&#xff0c;带你挖掘更多开发神器&#xff01;HiKariCP是数据库连接池的一个后起之秀&#xff0c;号称性能最好&#xff0c;可以完美地PK掉其他连接池。为何要使用HiKariCP&#xff1f;这要先从BoneCP说起&#xff1a;什么&#xf…

教你使用Box2d制作用蜡笔手绘物体的效果(一)

&#xfeff;&#xfeff;首先推荐一款好玩儿的物理益智游戏“CrayonPhysics”&#xff0c;中文名叫“蜡笔物理学”&#xff08;当然不是做广告哈哈&#xff09;&#xff0c;游戏中我们通过手工绘制各种各样的“物体”来让一个红色的小球吃掉星星。整个游戏都是蜡笔画的风格&am…

免费45天WPS稻壳会员领取

昨天在微信公众号“十点神器”分享了一个WPS专业版的永久密钥和使用方法&#xff0c;想到大家可能有需求使用稻壳会员&#xff0c;今天就找了一下一些免费的稻壳会员&#xff0c;还真的找到了一个45天的免费稻壳会员领取链接&#xff0c;好东西当然要分享。 稻壳会员能干啥&am…

栅格图像 转_这项AI图像描摹,90%的人都不知道!学会就是出图大佬

我们在寻找场地信息时&#xff0c;经常会找到一些专项地图&#xff0c;但是其非矢量的性质导致我们无法对其编辑&#xff1b;有时候从地图网站中下载地图&#xff0c;下载的图标和图像均无法更改&#xff0c;今天就告诉大家一个非矢量图像转矢量的方法。可能有的同学已经知道图…

教你使用Box2d制作用蜡笔手绘物体的效果(二)

&#xfeff;&#xfeff;我们继续来制作蜡笔手绘物体的效果。上一篇中我们完成了刚体绘制&#xff0c;这一部分我们来完成蜡笔纹理。 先来看一下最终的效果&#xff1a; &#xff08;如果觉得蜡笔的纹理不够好的话&#xff0c;可以精加工一个自己的蜡笔纹理&#xff09; 下面…

8 无法识别raid盘_车辆识别系统!开源可自取

今天&#xff0c;介绍一款车牌识别开源系统- yx-image-recognition&#xff0c;大伙需要的可以收藏哈&#xff01;&#xff01;&#xff01;gitee开源地址&#xff1a;https://gitee.com/admin_yu/yx-image-recognitionspring boot maven实现的车牌识别系统基于Opencv实现、在…

使用b2MouseJoint实现鼠标拖拽刚体的效果

&#xfeff;&#xfeff;要在Box2d中实现鼠标拖拽效果&#xff08;当然在移动设备上就不是鼠标拖拽而是手指拖拽了&#xff09;&#xff0c;可以使用Box2d中定义的b2MouseJoint来制作。大体思路是&#xff0c;首先在ccTouchBegan事件中获取鼠标&#xff08;以下不再区分鼠标或…