我们继续来制作物体在液体中的漂浮效果。
我们来考虑物体和液体的三种位置关系:
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来控制,经过调试,这两个系数的模拟效果还比较不错。
好了,制作完成,运行一下,是不是和我们一开始的截图一样了呢?
如果有问题欢迎留言讨论。