使用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。
應用上面的三種情況我們已經能夠對物體狀態進行區分了,由於物體只有浸入液體的時候才受到浮力,因此首先判斷物體是否在液體中,如果在液體中,判斷
首先定義類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;
}
-(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來控制,經過除錯,這兩個係數的模擬效果還比較不錯。
好了,製作完成,執行一下,是不是和我們一開始的截圖一樣了呢?
如果有問題歡迎留言討論。