중급 튜토리얼 2-2
Ogre3D 삽질란/Intermediate Tutorial 2 2008. 12. 10. 18:32
장면 설정하기
MouseQueryApplication::createScene함수로 갑시다. 다음코드는 별로 어렵지 않을겁니다. 만약 잘 모르는 코드가 보인다면 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));
기초적인 world geometry가 설정되었습니다. 이제 마우스 커서를 보이게 할 차례입니다. 그럴려면 CEGUI함수를 호출하여야 하므로 먼저 CEGUI를 구동시켜야 합니다. 우선 OgreCEGUIRenderer를 생성합니다. 그 다음 시스템 객체를 생성하고 방금 생성시켰던 렌더러를 전달합니다. CEGUI의 자세한 설정방법은 나중에 있을 튜토리얼에서 다뤄질 계획입니다. 일단 지금은 CEGUI로 어떤 SceneManager가 쓰일것인지와 마지막 매개변수로 mGUIRenderer가 들어간다는 것만 기억하세요.
// CEGUI setup
mGUIRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);
mGUISystem = new CEGUI::System(mGUIRenderer);
이제 실제로 마우스 커서를 보이게 할 차례입니다. 코드에 대한 설명은 생략하겠습니다. 나중에 있을 튜토리얼에서 알려드리겠습니다.
// Mouse
CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");
컴파일 후 실행시키면 화면 한가운데에서 커서를 발견할 것입니다. 하지만 움직이지는 않습니다(아직은).
FrameListener 소개
프로그램에서 필요한건 모두 끝냈습니다. FrameListener는 코드에 복잡하게 관여하는 부분입니다. 그러므로 실제로 구현하기전에 뭐가 어떻게 돌아갈지에 대해서 설명을 드려야 구현하기전에 머릿속에서 대충 정리가 될 것입니다.
첫번째, 마우스 우클릭버튼을 “mouse look”버튼으로 사용할 겁니다. 마우스로 자유롭게 주변상황을 볼 수 없다는것은 좀 귀찮거든요. 그래서 첫번재 우선과제는 마우스 컨트롤기능을 프로그램에 넣는 것 입니다(마우스 우클릭을 유지한 상태에서만 동작됩니다).
두번째, 카메라가 지형을 뚫고 지나가지 않게끔 할 계획입니다. 지형에 가까이 다가갈 수 있도록 보통 흔히들 떠올리는 그런 동작을 보여줄 겁니다.
세번째, 왼쪽클릭으로 지형의 어떤 공간에라도 엔티티를 추가할 수 있도록 할 계획입니다.
마지막으로 엔티티를 “drag” 할 수 있도록 만들겁니다. 왼쪽버튼을 누르고 유지시키면 엔티티를 놓고 싶은 위치까지 움직일 수 있게 됩니다. 마우스버튼을 떼면 그 자리에 위치하게 됩니다.
기능구현을 위해서 몇몇 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
mRaySceneQuery는 지형의 좌표를 찾는데 쓰일 RaySceneQuery의 복사본을 가집니다. mLMouseDown, mRMouseDown변수는 어떤 마우스 버튼이 눌렸는지를 체크합니다(mLMouseDown변수가 true면 왼쪽버튼이 눌린상태로 유지되고 있다는 의미며 false는 그렇지 않다는 의미입니다). mCount변수는 화면상에 몇개의 엔티티가 있는지를 헤아립니다. mCurrentObject변수는 가장 최근에 생성된 SceneNode(“drag”하는데 쓰일 엔티티로 사용될겁니다)를 가리킵니다. 마지막으로 mGUIRenderer는 CEGUI를 업데이트하는데 사용될 CEGUI 렌더러의 포인터를 가집니다.
Mouse listener에는 수많은 함수들이 연관되어 있습니다. 이 데모에서 모든 기능을 사용하지는 않지만 적어도 정의는 해둬야 컴파일시 에러를 발생시키지 않습니다.
FrameListener 설정
MouseQueryListener생성자로 가서 다음 초기화 코드를 추가하세요. 유심히 보셔야 할 부분은 지형크기가 줄어듬에 따라서 이동속도와 회전속도역시 감소했다는 점 입니다.
// Setup default variables
mCount = 0;
mCurrentObject = NULL;
mLMouseDown = false;
mRMouseDown = false;
mSceneMgr = sceneManager;
// Reduce move speed
mMoveSpeed = 50;
mRotateSpeed /= 500;
MouseQueryListener가 마우스 이벤트를 받기위해서 이 클래스를 MouseListener로서 등록을 해야 합니다. 무슨말인지 이해가 되지 않는다면 Basic Tutorial 5를 참고하세요
// Register this so that we get mouse events.
mMouse->setEventCallback(this);
마지막으로 이 생성자에서 RaySceneQuery객체를 생성해야 합니다. SceneManager에서 해야 하는 호출은 이게 전부입니다:
// Create RaySceneQuery
mRaySceneQuery = mSceneMgr->createRayQuery(Ray());
생성자에서 필요한건 모두 다 했지만 RaySceneQuery를 생성했으면 나중에는 꼭 파괴시켜줘야 합니다. MouseQueryListener의 파괴자로 가서 (~MouseQueryListener) 다음코드를 추가하세요 :
// We created the query, and we are also responsible for deleting it.
mSceneMgr->destroyQuery(mRaySceneQuery);
다음 섹션으로 넘어가기전에 컴파일이 제대로 되는지 확인하세요.
Mouse Look 추가
마우스의 우클릭을 마우스룩 모드로 바인딩할 차례입니다. 다음과 같은 절차로 구현됩니다 :
마우스가 움직이면 CEGUI를 업데이트합니다(커서도 움직입니다)
버튼이 눌려지면 mRMouseButton변수는 true값을 가집니다.
버튼이 떨어지면 mRMouseButton변수는 false 값을 가집니다.
“드래그” 되면 view를 바꿉니다(버튼을 누른상태로 움직인다는 의미입니다)
마우스가 드래그중일 경우에는 마우스 커서를 숨깁니다
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변수값을 토글시키고 마우스커서를 다시 보여지게 해야 합니다. 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
미리 작성해야할 모든 코드가 완성되었습니다. 이제는 마우스우클릭 드래깅의 경우 view를 바꿔야 합니다. 마지막 순간에서 지금까지 마우스가 움직인 거리를 읽어내야 합니다. Basic Tutorial 5 에서 구현했던 카메라 회전과 동일한 방법으로 구현합니다. MouseQueryListener::mouseMoved함수를 찾아서 다음 코드를 return구문 이전에 위치하도록 추가하세요 :
// 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 unit만큼 상위에 위치하게끔 만들겁니다. 만약 충분히 높은위치의 경우에는 제약없이 이동됩니다. 이 코드를 유심히 읽어주세요. 이 튜토리얼의 남은부분을 진행하면서 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's frameStarted함수는 카메라를 이동시키고 이 함수의 모든 행동은 이 함수가 수행된 이후에 수행되어야 하기 때문입니다. 우리의 목표는 카메라의 현재 좌표를 얻고 지형을 향해서 수직하단방향으로 광선을 쏩니다. 이것이 바로 RaySceneQuery이며 지형으로부터 얼마나 높이 위치하고 있는지를 알려줍니다. 카메라의 현재위치를 얻은 이후 광선을 생성해야 합니다. 광선은 원점(광선이 시작되는 위치)과 방향을 필요로 합니다. 방향에 있어서는 NEGATIVE_UNIT_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함수만 알아두세요. If the result.begin() == result.end() 의 경우는 리턴할 결과값이 아무것도 없다는 것을 의미합니다. 다음 데모에서는 다수의 SceneQuery에 대한 리턴값을 다루게 될 것입니다. 지금은 일단 간단한 것만 알아두고 넘어갑시다. 다음코드는 최소한 하나의 결과값(itr != result.end())을 리턴하며 그 결과값은 지형입니다(itr->worldFragment).
// Get the results, set the camera height
if (itr != result.end() && itr->worldFragment)
{
worldFragment구조체는 광선이 지형 어느 부분에 hit 했는지를 singleIntersection변수(Vector3형식)에 담아둡니다. 지형으로부터의 높이를 이 변수의 y변수값으로부터 얻어낼 수 있습니다. 높이를 알아낸 다음 만약 카메라가 일정높이 아래에 위치한다면 특정 높이로 카메라를 끌어 올리게 됩니다. 카메라는 지형으로부터 10 unit만큼 떨어지게 될 것입니다. 그렇게 해서 카메라가 지형으로부터 너무 가까이 위치해서 지형을 뚫고 볼수 없게끔 만들어 줍니다.
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함수의 내용을 변경해야 합니다. MouseQueryListener::mousePressed함수를 찾아서 if 구문 안에 다음 내용을 추가하세요.
// Left mouse button down
if (id == OIS::MB_Left)
{
mLMouseDown = true;
} // if
첫 문장은 익숙한 내용입니다. mRaySceneQuery객체에서 사용될 광선을 생성하고 설정합니다. 오우거에서 제공하는 Camera::getCameraToViewportRay함수가 있습니다; 상당히 유용한 함수입니다. 화면상에서 클릭된 좌표(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를 얻었고 해당 좌표에 객체를 생성해야 합니다. 첫번째 난관은 오우거에서의 모든 엔티티, SceneNode는 중복되지 않는 이름을 가져야 합니다. 이 문제를 해결하기위해서 엔티티 각각에 대해서 Entity "Robot1", "Robot2", "Robot3"… 이름을 설정하고 SceneNode에 대해서는 “Robot1Node", "Robot2Node", "Robot3Node"...이런식으로 설정할 것 입니다. 우선 이름을 생성합시다(C언어 문법책에서 sprintf에 대한 내용을 참고하세요).
char name[16];
sprintf( name, "Robot%d", mCount++ );
그 다음 엔티티와 SceneNode를 생성합니다. itr->worldFragment->singleIntersection를 통해서 로봇의 최초위치를 설정합니다. 그리고 지형의 크기를 고려해서 로봇을 1/10 크기로 사이즈를 줄입니다. 지금 새롭게 생성된 객체는 mCurrentObject변수에 저장되는것을 눈여겨 봐두세요. 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
그다지 설명이 필요없는 코드입니다. 현재 마우스의 위치에서 광선을 생성하고 RaySceneQuery를 이용하여 객체를 새로운 위치로 이동시킵니다. 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
컴파일 후 실행시켜보세요. 완성했습니다! 신경써서 클릭한다면 다음과 같은 결과물도 만들 수 있습니다 :
주의사항: TerrainSceneManager를 이용해서 RaySceneQuery로 교차지점을 얻고자 할 시 반드시 지형위에서 수행되어야 합니다.