중급 튜토리얼 4-2
ManualObjects
짤막한 3D객체 특강
메쉬를 만들기 전에 메쉬가 뭔지, 무엇으로 구성되는지 잠시 알고 넘어가도록 합시다. 간략하게 요약하면 메쉬는 전체적으로 크게 2파트로 구성됩니다: (vertex buffer)정점버퍼와 (index buffer)인덱스버퍼로 나뉩니다.
정점버퍼는 3D공간에서의 점들을 정의합니다. 정점버퍼에 대한 각각의 요소에 대해서 몇가지 속성들이 정의가능합니다. 필수적으로 설정해야 하는 속성은 정점의 위치입니다. 그 밖에도 정점의 색상, 텍스쳐좌표, 기타등등과 같은 선택가능한 수많은 설정용 옵션들이 있습니다. 메쉬로 무엇을 하느냐에 따라서 실제로 사용될 옵션은 가변적 입니다.
인덱스버퍼는 정점버퍼로부터 선택된 “점들간의 연결”입니다. GPU에 의해서 그려지는 한개의 삼각형당 3개의 인덱스가 설정됩니다. 인덱스버퍼에서 선택되는 정점의 순서에 따라서 어느면이 앞면인지를 결정하게 됩니다. 삼각형의 앞면은 시계 반대방향으로 그려지게 됩니다. 시계방향으로 그려지는 반대면은 뒷면이 됩니다. 보통 삼각형의 앞면만 그려지게되므로 삼각형을 설정하는데 있어서 신중해야 합니다.
모든 메쉬가 정점버퍼를 가지고 있긴 하지만 인덱스버퍼는 없을 수도 있습니다. 예를 들어서 앞으로 사용될 텅빈 사각형(안이 채워진 사각형은 반대의 경우)의 경우는 인덱스버퍼를 가지고 있지 않게 됩니다. 마지막으로 설명할 내용은 정점과 인덱스 버퍼는 대개 비디오카드의 메모리에 저장됩니다. 그러므로 프로그램은 미리 정의된 버퍼를 참조해 전체적인 3D메쉬가 그려지게하기 위해서 비디오카드로 적절한 명령조합들을 보내기만 하면 됩니다.
소개
오우거에서 메쉬를 생성하는 방법에는 2가지가 있습니다. 첫번째 방법은 SimpleRenderable클래스를 상속받아서 정점과 인덱스버퍼를 바로 넣어주는 방법입니다. 가장 직관적으로 생성하는 방법입니다. 하지만 가장 어려운 방법이기도 합니다. The GeneratingAMesh code snippet에 이것에 대한 예제가 있습니다. 좀 더 쉽게 하기 위해서 오우거는 ManualObject라는 훨씬 괜찮은 인터페이스를 제공합니다. raw데이터를 버퍼객체에 넣는것 대신에 간단한 함수를 이용해서 메쉬를 정의할 수 있도록 도와줍니다. 위치, 색상등 버퍼로 입력해줘야 하는 작업들이 간단하게 "position", "colour”함수를 호출하는것으로 해결됩니다.
이 튜토리얼에서는 마우스드래그로 선택될 객체들을 표시하기위해서 흰색 사각형을 생성해야 합니다. 오우거에서는 2D 사각형을 표시하는데 쓸 수 있는 클래스가 마땅치 않습니다. 어떻게 해야할지 따로 방법을 찾아내야 합니다. 오버레이를 이용해서 선택영역을 표시하고 리사이징을 할 수 있습니다. 그러나 문제는 선택영역으로 쓰일 사각형은 크기가 매우 가변적일 것이기 때문에 내부의 이미지를 늘리고 줄이는 과정에서 모양새가 깨질 가능성이 있습니다. 그러므로 선택영역용 사각형으로 동작될 매우 단순한 2D 메쉬를 생성시킬 것 입니다.
소스코드
선택용 사각형을 생성할때 2D처럼 보이게 해야 합니다. 그리고 그려질 오버레이를 다른 객체들 중에서 가장 상단에 위치하게 하여야 합니다. 이 과정은 어렵지 않습니다. SelectionRectangle생성자로 가서 다음 코드를 추가하세요 :
setRenderQueueGroup(RENDER_QUEUE_OVERLAY);
setUseIdentityProjection(true);
setUseIdentityView(true);
setQueryFlags(0);
첫번재 함수는 오버레이queue가 될 렌더queue를 설정합니다. 다음 2개의 함수들은 projection과 view 행렬이 identity화 되도록 설정합니다. Projection과 view 행렬은 대부분의 렌더링 시스템(OpenGL이나 DirectX도 포함하여)에서 객체들이 어디에서 보여야 하는지에 대해 사용됩니다. 오우거에서는 이러한 과정들을 간략화시켜 줍니다. 이러한 행렬값들이 구체적으로 어떤 과정을 거치는지는 다루지 않을겁니다. 대신 최소한 알아둬야 할 것은 지금상황처럼 projection과view 매트릭스를 identity화 시키는 경우는 기본적으로 2D객체를 생성시켜야 할때 입니다. 객체를 생성시킬때의 좌표시스템이 약간 바뀝니다. 더 이상 Z축은 의미가 없어집니다(Z축값이 필요한 경우는 -1을 대입합니다). 대신에 X, Y가 -1부터 1까지로 표시되는 새로운 좌표시스템을 얻게 됩니다. 마지막으로 선택용 사각형 자체가 쿼리결과에 포함되지 않도록 객체의 쿼리flag값은 0으로 설정합니다.
이제 객체가 설정되었으므로 실제로 사각형을 만들차례 입니다. 그런데 시작하기전에 작은 골칫거리가 있습니다. 이 함수는 마우스위치와 연관되서 호출될겁니다. 0~1사이의 x, y좌표를 얻기전에 [-1, 1]사이의 범위로 변환시켜주는 코드가 필요합니다. 약간의 계산이 필요합니다. 게다가 y축은 반대방향입니다. CEGUI에서는 마우스커서가 화면 상단에 위치할때 0, 하단일 경우1의 값을 가집니다. 하지만 새로운 좌표시스템에서는 화면 상단은 +1, 하단은 -1입니다. 다행스럽게도 이 문제를 해결하는 간단한 변환식이 있습니다. setCorners함수를 찾아서 다음 코드를 추가하세요 :
left = left * 2 - 1;
right = right * 2 - 1;
top = 1 - top * 2;
bottom = 1 - bottom * 2;
이제 새로운 시스템에 맞춰서 위치를 잡았습니다. 이제 실제로 객체를 생성합니다. 그러기 위해서 begin멤버함수를 호출합니다. 2개의 매개변수를 받는데 이 객체에서 쓰일 재질이름과 그리는데 사용될 render operation입니다. 텍스쳐는 넣지 않을 계획이므로 재질란은 공백으로 놔둡니다. 두번째 매개변수는 RenderOperation입니다. 메쉬를 그리는데 점 또는 선, 삼각형이 사용될 수 있습니다. 완전한 메쉬를 그리고 싶다면 삼각형을 사용하면 됩니다. 하지만 지금은 텅빈 사각형만 그릴것이므로 line strip을 쓰게 될 것입니다. Line strip은 미리 정의된 이전 정점으로부터 현재정점까지의 선을 그려나가는 방식입니다. 그러므로 사각형을 그리기 위해서는 총 5개의 점이 필요합니다(처음과 마지막점은 같은 위치로 연결되어 전체 사각형을 연결합니다) :
clear();
begin("", RenderOperation::OT_LINE_STRIP);
position(left, top, -1);
position(right, top, -1);
position(right, bottom, -1);
position(left, bottom, -1);
position(left, top, -1);
end();
자주 불려지게 될 함수이므로 새로 그려지기전에 이전에 그려졌던 사각형을 지우는 과정이 필요합니다. Manual object를 정의할때 begin/end 를 여러번 호출하여 다수의 sub-mesh를 만들수도 있습니다(다른 타입의 재질/ RenderOperation을 사용해도 됩니다). 2D객체를 정의하므로 Z축은 쓰지 않습니다. 그러므로 Z축 매개변수는 -1이 되어야 합니다. -1로 설정하면 그려질때 카메라 위 또는 뒤에 위치하지 않게 됩니다.
마지막으로 해야 할 일은 객체를 위해 바운딩박스를 설정하는 일 입니다. 많은 SceneManager에서는 객체가 화면에서 벗어날때 그려지는 대상에서 제외됩니다. 기본적으로는 2D객체를 생성하지만 오우거는 여전히3D엔진으로 동작됩니다. 2D객체라고해도 3D공간에 놓여지게 됩니다. 즉, 이렇게 2D객체를 생성하고 SceneNode에 attach한 이후(다음 섹션에서 하게 될 겁니다) 다른곳으로 시야를 돌리면 사라지게 된다는 의미입니다. 이 문제점을 고치기 위해서 객체의 바운딩박스가 infinite속성을 가지도록 설정합니다. 그렇게 하면 카메라는 항상 바운딩박스 안에 위치하게 될 것입니다 :
AxisAlignedBox box;
box.setInfinite();
setBoundingBox(box);
참고로 바운딩박스를 모두 지우는 함수가 호출된 이후에 이 코드가 추가되었습니다. 매번 ManualObject::clear가 호출될때마다 바운딩박스는 리셋될 것입니다. 자주 지워지는 또 다른 ManualObject를 생성할시 유의하세요. 왜냐하면 다시 생성될때마다 바운딩박스를 다시 설정해줘야 하기 때문입니다.
SelectionRectangle에 대한 코딩은 끝입니다. 지금 컴파일이 잘 되는지 시험해 보세요. 하지만 아직까지 새로 추가된 기능은 없습니다.
영역 선택
설정
영역선택관련 코드로 넘어가기전에 몇가지 설정할 것이 있습니다. 우선은 SelectionRectangle클래스의 객체를 생성해야 합니다. 그리고 SceneManager로 영역쿼리를 생성해야 합니다. 다음 코드를 DemoListener생성자에 추가하세요 :
mRect = new SelectionRectangle("Selection SelectionRectangle");
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(mRect);
mVolQuery = mSceneMgr->createPlaneBoundedVolumeQuery(PlaneBoundedVolumeList());
그 다음 frame listener를 다 썼으면 지워주는 코드가 필요합니다. ~DemoListener에 다음 코드를 추가하세요 :
mSceneMgr->destroyQuery(mVolQuery);
delete mRect;
이제 자동으로 SceneManager가 쿼리를 삭제시켜줄 수 있게 되었습니다.
Mouse 핸들러
구현하고자 하는 기능은 영역선택기능 입니다. 만약 유저가 마우스를 클릭후 움직이면서 화면을 드래그하면 사각형이 그려질 것 입니다. 마우스버튼을 뗀다면 그 사각형 안에 있는 모든 객체들이 선택될 것 입니다. 우선 마우스버튼이 눌렸을때의 상황을 처리해야 합니다. 사각형 시작지점을 저장하고 SelectionRectangle이 보이도록 설정해야 합니다. mousePressed함수를 찾아서 다음 코드를 추가하세요 :
if (id == OIS::MB_Left)
{
CEGUI::MouseCursor *mouse = CEGUI::MouseCursor::getSingletonPtr();
mStart.x = mouse->getPosition().d_x / (float)arg.state.width;
mStart.y = mouse->getPosition().d_y / (float)arg.state.height;
mStop = mStart;
mSelecting = true;
mRect->clear();
mRect->setVisible(true);
}
알아두실 점은 OIS용 마우스좌표가 아닌 CEGUI::MouseCursor의 x, y좌표를 사용한다는것 입니다. 그 이유는 OIS는 가끔씩 CEGUI가 출력하는 위치와는 다른위치에 있다고 잘못 인식하기 때문입니다. 유저가 실제로 바라보는 화면과 일치시키기 위해서 CEGUI의 마우스 좌표를 사용하는 것 입니다.
다음은 사용자가 마우스버튼을 해제시켰을때 선택용 사각형출력을 중단하고 선택쿼리를 수행할 차례입니다. MouseReleased에 다음 코드를 추가하세요 :
if (id == OIS::MB_Left)
{
performSelection(mStart, mStop);
mSelecting = false;
mRect->setVisible(false);
}
그리고 마지막으로 해줄 작업은 마우스가 움직이면 사각형을 새로운 좌표로 업데이트 시켜줘야 합니다 :
if (mSelecting)
{
CEGUI::MouseCursor *mouse = CEGUI::MouseCursor::getSingletonPtr();
mStop.x = mouse->getPosition().d_x / (float)arg.state.width;
mStop.y = mouse->getPosition().d_y / (float)arg.state.height;
mRect->setCorners(mStart, mStop);
}
마우스가 움직일때 마다 mStop 벡터값을 교정해 주기때문에 setCorners멤버함수에서 간단하게 사용될 수 있습니다. 컴파일 후 실행시켜 보세요. 이제 마우스를 이용해서 사각형을 그릴 수 있습니다.
PlaneBoundedVolumeListSceneQuery
이제 SelectionRectangle객체를 제대로 그릴 수 있게 되었습니다. 이제는 영역선택이 수행되도록 만들어야 합니다. performSelection함수에 다음 코드를 추가하세요 :
float left = first.x, right = second.x,
top = first.y, bottom = second.y;
if (left > right)
swap(left, right);
if (top > bottom)
swap(top, bottom);
이 부분에서는 매개변수의 left, right, top, bottom변수를 대입했습니다. If구문에서는 실제로 가장 낮은 값을 left와 top에 대입합니다. (만약 사각형이 “거꾸로” 그려졌다면 우측하단은 좌측상단으로 값이 설정되기때문에 값을 바꾸는 작업을 해줘야 합니다.)
그 다음에는 사각형 영역이 얼마나 큰지를 검사해야 합니다. 만약 사각형이 너무 작다면 영역경계를 제대로 설정할 수가 없게되며 객체가 너무 많게 혹은 적게 선택될 것 입니다. 만약 사각형이 화면크기에 비해서 너무 낮은 비율의 크기를 가진다면 선택동작은 수행되지 않을 것 입니다. 여기서는 적당히 0.0001값을 쿼리취소 경계값으로 두었습니다. 이 값은 여러분들이 테스트 해보고 적당히 조절해도 됩니다. 실제로 쓰이는 프로그램에서는 사각형의 중심점을 찾아서 아무것도 하지않는것 대신에 일반적인 RaySceneQuery를 수행하기도 합니다 :
if ((right - left) * (bottom - top) < 0.0001)
return;
바로 이 부분이 이 함수의 핵심부분입니다. 이제 자체적인 쿼리를 수행할 차례입니다. PlaneBoundedVolumeQuery는 평면으로 닫힌공간을 정의합니다. 그 다음 그 공간에 포함된 객체들을 선택하게 됩니다. 이 예제를 위해서 안쪽으로 향한 5개의 평면으로 구성된 닫힌공간을 생성할 것 입니다. 이 사각형을 위한 평면들을 생성하기 위해서 각각의 사각형 꼭지점을 담당하게 될 4개의 광선을 생성합니다. 4개의 광선을 생성하고 광선을 따라서 평면을 생성하게될 적당한 포인트를 잡게 될 것입니다 :
Ray topLeft = mCamera->getCameraToViewportRay(left, top);
Ray topRight = mCamera->getCameraToViewportRay(right, top);
Ray bottomLeft = mCamera->getCameraToViewportRay(left, bottom);
Ray bottomRight = mCamera->getCameraToViewportRay(right, bottom);
이제 평면을 생성해 봅시다. 광선을 따라서 100 unit만큼 떨어진 지점을 잡습니다. 이 값 역시 적당히 정한수치 입니다. 100대신에 2로도 할 수 있습니다. 평면에서 매우 가까운 정면값으로 카메라로 부터 3 unit만큼 떨어진 위치를 시작점으로 정할 것 입니다.
PlaneBoundedVolume vol;
vol.planes.push_back(Plane(topLeft.getPoint(3), topRight.getPoint(3), bottomRight.getPoint(3))); // front plane
vol.planes.push_back(Plane(topLeft.getOrigin(), topLeft.getPoint(100), topRight.getPoint(100))); // top plane
vol.planes.push_back(Plane(topLeft.getOrigin(), bottomLeft.getPoint(100), topLeft.getPoint(100))); // left plane
vol.planes.push_back(Plane(bottomLeft.getOrigin(), bottomRight.getPoint(100), bottomLeft.getPoint(100))); // bottom plane
vol.planes.push_back(Plane(topRight.getOrigin(), topRight.getPoint(100), bottomRight.getPoint(100))); // right plane
이 평면들은 “open box” 로써 카메라 정면으로부터 무한히 확장되는 형태 입니다. 그려지는 사각형은 카메라 정면으로부터 종점까지의 포인트를 잇는 박스라고 생각해도 됩니다. 평면들이 모두 생성되었으므로 이제 쿼리를 실행할 차례입니다 :
PlaneBoundedVolumeList volList;
volList.push_back(vol);
mVolQuery->setVolumes(volList);
SceneQueryResult result = mVolQuery->execute();
이제 마지막 단계로 쿼리에 대한 결과를 처리할 차례입니다. 먼저 이전에 선택되었던 모든 객체들을 선택해제시키며 쿼리로 찾아낸 객체들을 선택하게 됩니다. deselectObjects, selectObject함수는 이전 튜토리얼에서 다룬내용으로 작성되었으므로 이미 완성되어 있습니다 :
deselectObjects();
SceneQueryResultMovableList::iterator itr;
for (itr = result.movables.begin(); itr != result.movables.end(); ++itr)
selectObject(*itr);
쿼리를 위한 모든 작업이 끝났습니다. 비록 이 튜토리얼에서는 다루지 않았지만 영역 선택을 위해서도 쿼리flag가 사용될 수 있습니다. 이전 튜토리얼을 참고하여 쿼리flag에 대한 더 자세한내용을 참고하세요.
컴파일 후 실행시켜 보세요. 영역선택을 이용해서 객체들을 선택할 수 있습니다!