中級教程2:射線場景查詢及基礎鼠標用法
有關這篇教程,無論遇到任何問題,都可以到論壇發帖尋求幫助。
目錄
[隱藏]
1 介紹
2 前期準備
3 開始
4 創建場景
5 幀監聽器介紹
6 創建幀監聽器
7 增加鼠標查看
8 地形碰撞檢測
9 地形選擇
10 進階練習
10.1 簡單練習
10.2 中級練習
10.3 高級練習
10.4 進階練習
介紹
本教程中,我們會初步創建一個基礎場景編輯器。在過程之中,我們會涉及到:
如何使用RaySceneQueries阻止鏡頭穿透地面
如何使用MouseListener和MouseMotionListener接口
使用鼠標選取地面上的x和y座標
你可以在這裏找到完整代碼。跟隨着教程,你會慢慢地向你自己的工程項目中增加代碼,並且隨着編譯看到結果。
前期準備
本教程假設你已經知道了如何創建Ogre工程,並且可以成功編譯。假設你已經瞭解了基本的Ogre對象(場景節點,實體,等等)。你也應該熟悉STL迭代器基本的使用方法,因爲本教程會用到。(Ogre也大量用到STL,如果你還不熟悉STL,那麼你需要花些時間學習一下。)
開始
首先,你需要爲此演示程序創建一個新工程。在創建工程時,選空工程、自己的框架,以及初始化進度條和CEGUI支持,不選編譯後拷貝。向工程中,增加一個名叫“MouseQuery.cpp”的文件,並向其中添加如下代碼:
#include <CEGUI/CEGUISystem.h>
#include <CEGUI/CEGUISchemeManager.h>
#include <OgreCEGUIRenderer.h>
#include "ExampleApplication.h"
class MouseQueryListener : public ExampleFrameListener, public OIS::MouseListener
{
public:
MouseQueryListener(RenderWindow* win, Camera* cam, SceneManager *sceneManager, CEGUI::Renderer *renderer)
: ExampleFrameListener(win, cam, false, true), mGUIRenderer(renderer)
{
} // MouseQueryListener
~MouseQueryListener()
{
}
bool frameStarted(const FrameEvent &evt)
{
return ExampleFrameListener::frameStarted(evt);
}
/* MouseListener callbacks. */
bool mouseMoved(const OIS::MouseEvent &arg)
{
return true;
}
bool mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{
return true;
}
bool mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{
return true;
}
protected:
RaySceneQuery *mRaySceneQuery; // The ray scene query pointer
bool mLMouseDown, mRMouseDown; // True if the mouse buttons are down
int mCount; // The number of robots on the screen
SceneManager *mSceneMgr; // A pointer to the scene manager
SceneNode *mCurrentObject; // The newly created object
CEGUI::Renderer *mGUIRenderer; // CEGUI renderer
};
class MouseQueryApplication : public ExampleApplication
{
protected:
CEGUI::OgreCEGUIRenderer *mGUIRenderer;
CEGUI::System *mGUISystem; // cegui system
public:
MouseQueryApplication()
{
}
~MouseQueryApplication()
{
}
protected:
void chooseSceneManager(void)
{
// Use the terrain scene manager.
mSceneMgr = mRoot->createSceneManager(ST_EXTERIOR_CLOSE);
}
void createScene(void)
{
}
void createFrameListener(void)
{
mFrameListener = new MouseQueryListener(mWindow, mCamera, mSceneMgr, mGUIRenderer);
mFrameListener->showDebugOverlay(true);
mRoot->addFrameListener(mFrameListener);
}
};
#if OGRE_PLATFORM == PLATFORM_WIN32 || OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT)
#else
int main(int argc, char **argv)
#endif
{
// Create application object
MouseQueryApplication app;
try {
app.go();
} catch(Exception& e) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
MessageBox(NULL, e.getFullDescription().c_str(), "An exception has occurred!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
fprintf(stderr, "An exception has occurred: %s/n",
e.getFullDescription().c_str());
#endif
}
return 0;
}
在繼續下面教程以前,先確保上面代碼可以正常編譯。
創建場景
找到MouseQueryApplication::createScene方法。下面的代碼應該都很熟悉了。如果你不知道其中某些是做什麼用的,請在繼續本教程前,參考Ogre
API。向createScene中,增加如下代碼:
// Set ambient light
mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));
mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);
// World geometry
mSceneMgr->setWorldGeometry("terrain.cfg");
// Set camera look point
mCamera->setPosition(40, 100, 580);
mCamera->pitch(Degree(-30));
mCamera->yaw(Degree(-45));
既然我們建立了基本的世界空間,那麼就要打開光標。打開光標,要使用CEGUI函數調用。不過在此之前,我們需要啓用CEGUI。我們首先創建一個OgreCEGUIRenderer,然後創建系統對象並將剛創建的Renderer傳給它。創建CEGUI我們會專門留待後續教程介紹,現在只要知道創建mGUIRenderer時必須以最後一個參數告訴CEGUI你要用那個場景管理器。
// CEGUI setup
mGUIRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);
mGUISystem = new CEGUI::System(mGUIRenderer);
現在我們需要實際顯示光標了。同樣地,我不打算過多解釋這些代碼。我們會在後面的教程中詳細介紹。(其實也沒什麼,就是設置了一下CEGUI的窗口和鼠標的樣式。——Aaron註釋)
// Mouse
CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");
如果你編譯並運行這個程序,你會發現一個光標出現在屏幕中央,但它還動不了。
幀監聽器介紹
這是程序要做的全部事情。FrameListener是代碼中複雜的部分,所以我會花一些時間強調我們要完成的東西,以便在我們開始實現它之前,使你有一個大體的印象。
首先,我們想要將鼠標右鍵綁定到“鼠標觀察”模式。不能使用鼠標四下看看是相當鬱悶的,所以我們首先對程序增加鼠標控制(儘管只是在我們保持鼠標右鍵按下時)。
第二,我們想要讓鏡頭不會穿過地表。這會使它更接近我們期望的樣子。
第三,我們想要在地表上用鼠標左鍵點擊一下,就在那裏增加一個實體。
最後,我們想要能“拖拽”實體。即選中我們想要看到的實體,按住鼠標左鍵不放,將它移動到我們想要放置的地方。鬆開鼠標左鍵,就又會將它鎖定在原地。
要做到這幾點,我們要使用幾個受保護的變量(這些已經加到類中了):
RaySceneQuery *mRaySceneQuery; // 射線場景查詢指針
bool mLMouseDown, mRMouseDown; // 如果按下鼠標按鈕,返回True
int mCount; // 屏幕上機器人的數量
SceneManager *mSceneMgr; // 指向場景管理器的指針
SceneNode *mCurrentObject; // 新創建的物休
CEGUI::Renderer *mGUIRenderer; // CEGUI渲染器
變量mRaySceneQuery握有RaySceneQuery的一個拷貝,我們會它來尋找地面上的座標。變量mLMouseDown和mRMouseDon會追蹤我們是否按下鼠標鍵(例如:如果按下鼠標左鍵,則mLMouseDown爲true;否則,爲false)。mCount計數屏幕上有的實體數。mCurrentObject握有指向最近創建的場景節點的指針(我們將用這個“拖拽”實體)。最後,mGUIRenderer握有指向CEGUI
Renderer的指針,我們將用它更新CEGUI。
還要注意的是,有許多和鼠標監聽器相關的函數。在本演示程序中,我們不會全部用到,但是它們必須全部在那兒,否則編譯會報錯說你沒定義它們。
創建幀監聽器
找到MouseQueryListener構造函數,增加如下初始化代碼。注意,由於地形相當小,所以我們也要減少移動和旋轉速度。
// Setup default variables
mCount = 0;
mCurrentObject = NULL;
mLMouseDown = false;
mRMouseDown = false;
mSceneMgr = sceneManager;
// Reduce move speed
mMoveSpeed = 50;
mRotateSpeed /= 500;
爲了MouseQueryListener能收到鼠標事件,我們必須把它註冊爲一個鼠標監聽器。如果對此不太熟悉,請參考基礎教程5。
// Register this so that we get mouse events.
mMouse->setEventCallback(this);
最後,在構造函數中我們需要創建一個RaySceneQuery對象。用場景管理器的一個調用創建:
// Create RaySceneQuery
mRaySceneQuery = mSceneMgr->createRayQuery(Ray());
這是我們需要的全部構造函數了,但是如果我們創建一個RaySceneQuery,以後我們就必須銷燬它。找到MouseQueryListener析構函數(~MouseQueryListener),增加如下代碼:
// We created the query, and we are also responsible for deleting it.
mSceneMgr->destroyQuery(mRaySceneQuery);
在進入下一階段前,請確保你的代碼可以正常編譯。
增加鼠標查看
我們要將鼠標查看模式綁定到鼠標右鍵上,需要:
當鼠標被移動時,更新CEGUI(以便光標也移動)
當鼠標右鍵被按下時,設置mRMouseButton爲true
當鼠標右鍵被鬆開時,設置mRMouseButton爲false
當鼠標被“拖拽”時,改變視圖
當鼠標被“拖拽”時,隱藏鼠標光標
找到MouseQueryListener::mouseMoved方法。我們將要增加代碼使每次鼠標移動時移動鼠標光標。向函數中增加代碼:
// Update CEGUI with the mouse motion
CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);
現在找到MouseQueryListener::mousePressed方法。這段代碼當鼠標右鍵按下時,隱藏光標,並設置變量mRMouseDown爲true。
// Left mouse button down
if (id == OIS::MB_Left)
{
mLMouseDown = true;
} // if
// Right mouse button down
else if (id == OIS::MB_Right)
{
CEGUI::MouseCursor::getSingleton().hide();
mRMouseDown = true;
} // else if
接下來,當鼠標右鍵擡起時,我們需要再次顯示光標,並將mRMouseDown設置爲false。找到mouseReleased函數,增加如下代碼:
// Left mouse button up
if (id == OIS::MB_Left)
{
mLMouseDown = false;
} // if
// Right mouse button up
else if (id == OIS::MB_Right)
{
CEGUI::MouseCursor::getSingleton().show();
mRMouseDown = false;
} // else if
現在,我們有了全部準備好的代碼,我們想要在按住鼠標右鍵移動鼠標時改變視圖。我們要做的就是,讀取他自上次調用方法後移動的距離。這可以用與基礎教程5中旋轉攝像機鏡頭一樣的方法實現。找到TutorialFrameListener::mouseMoved函數,就在返回狀態前,增加如下代碼:
// If we are dragging the left mouse button.
if (mLMouseDown)
{
} // if
// If we are dragging the right mouse button.
else if (mRMouseDown)
{
mCamera->yaw(Degree(-arg.state.X.rel * mRotateSpeed));
mCamera->pitch(Degree(-arg.state.Y.rel * mRotateSpeed));
} // else if
現在如果你編譯並運行這些代碼,你將能夠通過按住鼠標右鍵控制攝像機往哪裏看。
地形碰撞檢測
我們現在要實現它,以便當我們向着地面移動時,能夠不穿過地面。因爲BaseFrameListener已經處理了攝像機移動,所以我們就不用碰那些代碼了。替代地,在BaseFrameListener移動了攝像機後,我們要確保攝像機在地面以上10個單位處。如果它不在,我們要把它移到那兒。請跟緊這段代碼。我們將在本教程結束前使用RaySceneQuery做幾件別的事,而且在這段結束後,我不會再做如此詳細的介紹。
找到MouseQueryListener::frameStarted方法,移除該方法的全部代碼。我們首先要做的事是調用ExampleFrameListener::frameStarted方法。如果它返回false,則我們也會返回false。
// Process the base frame listener code. Since we are going to be
// manipulating the translate vector, we need this to happen first.
if (!ExampleFrameListener::frameStarted(evt))
return false;
我們在frameStarted函數的最開始處做這些,是因爲ExampleFrameListener的frameStarted成員函數移動攝像機,並且在此發生後我們需要在函數中安排我們的剩餘行動。我們的目標及時找到攝像機的當前位置,並沿着它向地面發射一條射線。這被稱爲射線場景查詢,它會告訴我們我們下面的地面的高度。得到了攝像機的當前位置後,我們需要創建一條射線。這條射線有一個起點(射線開始的地方),和一個方向。在本教程的情況下,我們的方向是Y軸負向,因爲我們指定射線一直向下。一旦我們創建了射線,我們就告訴RaySceneQuery對象使用它。
// Setup the scene query
Vector3 camPos = mCamera->getPosition();
Ray cameraRay(Vector3(camPos.x, 5000.0f, camPos.z), Vector3::NEGATIVE_UNIT_Y);
mRaySceneQuery->setRay(cameraRay);
注意,我們已經使用了5000.0f高度代替了攝像機的實際位置。如果我們使用攝像機的Y座標代替這個高度,如果攝像機在地面以下,我們會錯過整個地面。現在我們需要執行查詢,得到結果。查詢結果是std::iterator類型的。
// Perform the scene query
RaySceneQueryResult &result = mRaySceneQuery->execute();
RaySceneQueryResult::iterator itr = result.begin();
在本教程中的這個地形條件下,查詢結果基本上是一個worldFragment的列表和一個可移動物體(稍後的教程會介紹到)的列表。如果你對STL迭代器不太熟悉,只要知道調用begin方法獲得迭代器的第一個元素。如果result.begin()
==
result.end(),那麼無返回結果。在下一個演示程序裏,我們將處理SceneQuery的多個返回值。目前,我們只要揮揮手,在其間移動。下面的這行代碼保證了至少返回一個查詢結果(itr
!= result.end()),那個結果是地面(itr->worldFragment)。
// Get the results, set the camera height
if (itr != result.end() && itr->worldFragment)
{
worldFragment結構包含有在變量singleIntersection(一個Vector3)中射線擊中地面的位置。我們要得到地面的高度,依靠將這個向量的Y值賦值給一個本地變量。一旦我們有了高度,我們就要檢查攝像機是否低於這一高度,如果低於這一高度,那麼我們要將攝像機向上移動至地面高度。注意,我們實際將攝像機多移動了10個單位。這樣保證我們不能由於太靠近地面而看穿地面。
Real terrainHeight = itr->worldFragment->singleIntersection.y;
if ((terrainHeight + 10.0f) > camPos.y)
mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
}
return true;
最後,我們返回true,繼續渲染。此時,你應該編譯測試你的程序了。
地形選擇
在這部分中,每次點擊鼠標左鍵,我們將向屏幕上創建和添加對象。每次你點擊、按住鼠標左鍵,就會創建一個對象並跟隨你的光標。你可以移動對象,直到你鬆開鼠標左鍵,同時對象也鎖定在那一點上。要做到這些,我們需要改變mousePressed函數。在MouseQueryLlistener::mousePressed函數中,找到如下代碼。我們將要在這個if語句中增加一些代碼。
// Left mouse button down
if (id == OIS::MB_Left)
{
mLMouseDown = true;
} // if
第一段代碼看起來會很熟悉。我們會創建一條射線以供mRaySceneQuery對象使用,設置射線。Ogre給我們提供了Camera::getCameraToViewpointRay;一個將屏幕上的點擊(X和Y座標)轉換成一條可供RaySceneQuery對象使用的射線的好用函數。
// Left mouse button down
if (id == OIS::MB_Left)
{
// Setup the ray scene query, use CEGUI's mouse position
CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
mRaySceneQuery->setRay(mouseRay);
接下來,我們將執行查詢,並確保它返回一個結果。
// Execute query
RaySceneQueryResult &result = mRaySceneQuery->execute();
RaySceneQueryResult::iterator itr = result.begin( );
// Get results, create a node/entity on the position
if (itr != result.end() && itr->worldFragment)
{
既然我們有了worldFragment(也就是點擊的位置),我們就要創建對象並把它放到位。我們的第一個難題是,Ogre中每個實體和場景節點都需要一個唯一的名字。要完成這一點,我們要給每個實體命名爲“Robot1”,“Robot2”,“Robot3”……同樣將每個場景節點命名爲“Robot1Node”,“Robot2Node”,“Robot3Node”……等等。首先,我們創建名字(更多關於sprintf的信息,請參考C語言)。
char name[16];
sprintf( name, "Robot%d", mCount++ );
接下來,我們創建實體和場景節點。注意,我們使用itr->worldFragment->singleIntersection作爲我們的機器人的默認位置。由於地形太小所以我們也把他縮小爲原來的十分之一。注意我們要將這個新建的對象賦值給成員變量mCurrentObject。我們將在下一段要用到它。
Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");
mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", itr->worldFragment->singleIntersection);
mCurrentObject->attachObject(ent);
mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
} // if
mLMouseDown = true;
} // if
現在編譯運行程序。你可以在場景中點擊地形上任意地點放置機器人。我們幾乎完全控制了我們的程序,但是在結束前,我們需要實現對象拖拽。我們要在這個if語句段中添加代碼:
// If we are dragging the left mouse button.
if (mLMouseDown)
{
} // if
接下來的代碼段現在應該是不言而喻的。我們創建了一條基於鼠標當前位置的射線,然後我們執行了射線場景查詢且將對象移動到新位置。注意我們不必檢查mCurrentObject看看它是不是有效的,因爲如果mCurrentObject未被mousePressed設置,那麼mLMouseDown不會是true。
if (mLMouseDown)
{
CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width),mousePos.d_y/float(arg.state.height));
mRaySceneQuery->setRay(mouseRay);
RaySceneQueryResult &result = mRaySceneQuery->execute();
RaySceneQueryResult::iterator itr = result.begin();
if (itr != result.end() && itr->worldFragment)
mCurrentObject->setPosition(itr->worldFragment->singleIntersection);
} // if
編譯運行程序。現在全部都完成了。點擊幾次後,你得到的結果應該看起來如下圖所示。 [image:Intermediate_Tutorial_2.jpg]
進階練習
簡單練習
要阻止攝像機鏡頭看穿地形,我們選擇地形上10個單位。這一選擇是任意的。我們可以改進這一數值使之更接近地表而不穿過嗎?如果可以,設定此變量爲靜態類成員並給它賦值。
有時我們確實想要穿越地形,特別是在場景編輯器中。創建一個標記控制碰撞檢測開關,並綁定到一個鍵盤上的按鍵上。確保碰撞檢測被關閉時,你不會在frameStarted中進行SceneQuery場景查詢。
中級練習
當前我們每幀都要做場景查詢,無論攝像機是否實際移動過。修補這個問題,如果攝像機移動了,只做一次場景查詢。(提示:找到ExampleFrameListener中的移動向量,調用函數後,測試它是否爲Vector3::ZERO。)
高級練習
注意到,每次我們執行一個場景查詢調用時,有許多代碼副本。將所有場景查詢相關功能打包到一個受保護的函數中。確保處理地形一點不交叉的情況。
進階練習
在這個教程中,我們使用了RaySceneQueries來放置地形上的對象。我們也許可以用它做些別的事情。拿來中級教程1的代碼,完成困難問題1和專家問題1。然後將那個代碼融合到這個代碼中,使機器人行走在地面上,而不是虛空中。
增加代碼,使每次你點擊場景中的一點時,機器人移動到那個位置。