Cocos2d-x教程(35)-三維拾取Ray-AABB碰撞檢測演算法,cocos2d-xray-aabb
歡迎加入Cocos2d-x 交流群:193411763
轉載時請註明原文出處 :http://blog.csdn.net/u012945598/article/details/39927911
-----------------------------------------------------------------------------------------------------------------------------------------------------------
1.三維拾取技術
在3D遊戲中通常會有這樣的需求,使用者可以選取3D世界中的某些物體進行如拖拽等操作,這時便需要程式通過將二維螢幕上的點座標轉換為三維世界中的座標,並進行比對,這個過程就需要用到三維拾取。
三維拾取的基本原理並不複雜,我們仍然以Cocos2d-x 3.3beta0版本來分析。拾取思想可以簡單的理解為:首先得到在螢幕上的觸摸點的座標,然後根據攝像機投影矩陣與螢幕上的觸摸點計算出一條射線ray,注意,正常情況下之後應該去找與射線相交並且交點距離射線起點最近的點所在的包圍盒,這個包圍盒才是應該被觸摸到的包圍盒,但是實際上Cocos2d-x 3.3beta0中並沒有做此操作,這個問題在後文討論。
2.原理圖
三維拾取原理圖1-1所示:
圖1-1
如的這種情況,射線實際上會與物體A和物體B都相交,但是實際上物體A才應該是被觸摸到的物體。但是Cocos2d-x 3.3beta0中目前還沒有做此處理,僅判斷出了射線是否與某一當前存在的包圍盒存在交點。下面看一下Cocos2d-x 3.3beta0中OBB包圍盒Demo中的一段的碼:
void Sprite3DWithOBBPerfromanceTest::onTouchesBegan(const std::vector<Touch*>& touches, Event* event){ for (auto touch: touches) { auto location = touch->getLocationInView(); //擷取在螢幕座標系中觸摸點的座標 if(_obb.size() > 0) //判斷螢幕上是否存在OBB包圍盒 { _intersetList.clear(); Ray ray; //射線 //根據螢幕座標系觸摸點座標計算射線在全局座標系中的起始點和方向向量 calculateRayByLocationInView(&ray,location); for(int i = 0; i < _obb.size(); i++) { if(ray.intersects(_obb[i])) //判斷射線與包圍盒是否相交 { _intersetList.insert(i); return; } } } }}
這個演算法在對包圍盒進行遍曆時,一旦得出的射線和某一個包圍盒碰撞了,迴圈便終止了,然後取到了這個物體的包圍盒。但是如果兩個包圍盒重疊在一起的時候,應該判斷是哪個包圍盒距離射線起點的距離更近,更近的才是應該被摸到的盒子。而此種做法相當於,兩個重疊的盒子哪個排在容器前面先被遍曆到了就相當於摸到了哪個。
下面拋開上述問題,回到圖1-1。按照圖1-1所示,最終需要做的就是,根據螢幕上的觸摸點求出射線與近平面和遠平面的交點,這樣便能得到我們所需要的射線了。在Cocos2d-x 3.3beta0中,Ray表示的便是射線類,裡麵包含了射線的起點以及方向向量,同時提供了與AABB包圍盒、OBB包圍盒碰撞檢測的演算法。同時在上述代碼中,調用了一個方法:calculateRayByLocationInView(Ray* ray, const Vec2& location)。這個方法便是根據螢幕座標繫上一點座標求射線的方法,下面來看一下實現:
//將螢幕上一點座標轉化為全局座標系中的座標void Sprite3DWithOBBPerfromanceTest::unproject(const Mat4& viewProjection, const Size* viewport, Vec3* src, Vec3* dst){ assert(dst); assert(viewport->width != 0.0f && viewport->height != 0.0f); //計算點在攝像機座標系中的座標,利用觸摸點的座標與攝像機近平面座標的線性相關性 Vec4 screen(src->x / viewport->width, ((viewport->height - src->y)) / viewport->height, src->z, 1.0f); screen.x = screen.x * 2.0f - 1.0f; screen.y = screen.y * 2.0f - 1.0f; screen.z = screen.z * 2.0f - 1.0f; //將得到的攝像機座標系中的座標經攝像機矩陣的逆矩陣變換得到其全局座標 viewProjection.getInversed().transformVector(screen, &screen); //齊次座標正常化 if (screen.w != 0.0f) { screen.x /= screen.w; screen.y /= screen.w; screen.z /= screen.w; } //儲存該點的全局座標 dst->set(screen.x, screen.y, screen.z);}//計算射線void Sprite3DWithOBBPerfromanceTest::calculateRayByLocationInView(Ray* ray, const Vec2& location){ auto dir = Director::getInstance(); auto view = dir->getWinSize(); //擷取視窗大小 用於計算觸摸點在攝像機座標系中位置 Mat4 mat = dir->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW); //擷取投影矩陣棧棧頂元素(即原棧頂元素的拷貝,攜帶父節點的變換資訊) mat = dir->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION); Vec3 src = Vec3(location.x, location.y, -1); Vec3 nearPoint; //近平面點 unproject(mat, &view, &src, &nearPoint);//計算近平面點在全局座標系中的座標 src = Vec3(location.x, location.y, 1); Vec3 farPoint; //遠平面點 unproject(mat, &view, &src, &farPoint);//計算遠平面點在全局座標系中的座標 Vec3 direction; //方向向量 Vec3::subtract(farPoint, nearPoint, &direction); //遠平面點減去近平面點求方向向量 direction.normalize(); //歸一化 ray->_origin = nearPoint; //射線起點位置 ray->_direction = direction; //射線方向向量}
3.Ray-AABB碰撞檢測 進行求出射線後,需要做的便是與包圍盒的碰撞檢測了,如之前的代碼所示,在做碰撞檢測時,Cocos2d-x 3.3beta0中的Ray類裡面為我們提供了intersects()方法,該方法的參數有OBB對象和AABB對象兩種,實際上最終都是轉換成了對AABB的檢測,最後來看一下碰撞檢測相關代碼:
bool Ray::intersects(const AABB& aabb) const{ Vec3 ptOnPlane; //射線與包圍盒某面的交點 Vec3 min = aabb._min; //aabb包圍盒最小點座標 Vec3 max = aabb._max; //aabb包圍盒最大點座標 const Vec3& origin = _origin; //射線起始點 const Vec3& dir = _direction; //方向向量 float t; //分別判斷射線與各面的相交情況 //判斷射線與包圍盒x軸方向的面是否有交點 if (dir.x != 0.f) //射線x軸方向分量不為0 若射線方向向量的x軸分量為0,射線不可能經過包圍盒朝x軸方向的兩個面 { /* 使用射線與平面相交的公式求交點 */ if (dir.x > 0)//若射線沿x軸正方向位移 t = (min.x - origin.x) / dir.x; else //射線沿x軸負方向位移 t = (max.x - origin.x) / dir.x; if (t > 0.f) //t>0時則射線與平面相交 { ptOnPlane = origin + t * dir; //計算交點座標 //判斷交點是否在當前面內 if (min.y < ptOnPlane.y && ptOnPlane.y < max.y && min.z < ptOnPlane.z && ptOnPlane.z < max.z) { return true; //射線與包圍盒有交點 } } } //若射線沿y軸方向有分量 判斷是否與包圍盒y軸方向有交點 if (dir.y != 0.f) { if (dir.y > 0) t = (min.y - origin.y) / dir.y; else t = (max.y - origin.y) / dir.y; if (t > 0.f) { ptOnPlane = origin + t * dir; if (min.z < ptOnPlane.z && ptOnPlane.z < max.z && min.x < ptOnPlane.x && ptOnPlane.x < max.x) { return true; } } } //若射線沿z軸方向有分量 判斷是否與包圍盒y軸方向有交點if (dir.z != 0.f){ if (dir.z > 0) t = (min.z - origin.z) / dir.z; else t = (max.z - origin.z) / dir.z; if (t > 0.f) { ptOnPlane = origin + t * dir; if (min.x < ptOnPlane.x && ptOnPlane.x < max.x && min.y < ptOnPlane.y && ptOnPlane.y < max.y) { return true; } } } return false;}