我們繼續來制作物體在液體中的漂浮效果。
我們來考慮物體和液體的三種位置關系:
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來控制,經過調試,這兩個系數的模拟效果還比較不錯。
好了,制作完成,運作一下,是不是和我們一開始的截圖一樣了呢?
如果有問題歡迎留言讨論。