15. LoadIrrFile

Posted 2007/09/15 22:11 by 수달

2006년 8월 6일 부로 1.1 버전과 irrEdit 0.3이 공개되었다.


이중 irrEdit은 씬노드를 만드는 툴로써 일종의 맵툴이라 할 수 있다.


물론 게임툴이라고 불릴 정도의 레벨 디자인이 가능한 툴은 아니고, 여러 그래픽 자원을 배치 할 수 있는 정도이다.


하지만 원래부터 IRRLICHT엔진은 그래픽 엔진임을 생각해야 한다.


고맙게도 이번 버전은 irrEdit의 전용 포맷인 *.irr에 대한 것이다.


irrEdit에 대한 좀 더 심도 있는 연구가 필요할 것 같다.


--------------------------------------------------------------------------------------

IRRLICHT 1.1버전부터 XML에 기반을 둔 *.irr 파일의 모든 그래픽스를 저장하고 읽을 수 있게 되었다. http://irredit.irrlicht3d.org를 참조하는 것이 좋겠다. 단순하게만 말하자면 *.irr를 만드는 irrEdit은 월드와 파티클 툴로써 사용할 수 있을 것이다. 이번 예제에서는 *.irr파일 다루는 법을 알려준다.

자, 시작해 보자. 일단 윈도를 설정하고 엔진 장치를 만들자.


#include <irrlicht.h>
#include <iostream>
using namespace irr;

#pragma comment(lib, "Irrlicht.lib")

int main()
{


드라이버 타입을 묻는다.


video::E_DRIVER_TYPE driverType;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\
" (d) Software Renderer\n (e) Apfelbaum Software Renderer\n"\
" (f) NullDevice\n (otherKey) exit\n\n");

char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECT3D9;break;
case 'b': driverType = video::EDT_DIRECT3D8;break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_SOFTWARE2;break;
case 'f': driverType = video::EDT_NULL; break;
default: return 1;
}


장치를 만들고 실패시 종료한다.


IrrlichtDevice* device =
createDevice(driverType, core::dimension2d<s32>(640, 480));

if (device == 0)
return 1;

device->setWindowCaption(L"Load .irr file example");

video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();

자 이제 *.irr를 읽어보자. *.irr파일은 애니메이터, 재질, 파티클을 포함하는 모든 씬 그래프(역자 주 : 아마도 씬 노드를 비롯한 엔진의 모든 그래픽 지원을 이르는 말 같다.)를 저장할 수 있다. 또한 파일 속의 각각의 씬 노드에 대해 임의의 사용자 데이터를 추가할 수도 있다. 예제를 단순하게 하기 위해 모든 것을 다 말하진 않겠고, 이에 대해서 ISceneManager::loadScene와 ISceneManager::saveScene을 문서(Irrlicht.chm)에서 찾아보길 바란다. 자, 여기서는 단지 loadScene()을 부르기만 하면 된다.

smgr->loadScene("../../media/example.irr");


준비가 다 되었다. 카메라를 더하고 씬을 그려보자.


smgr->addCameraSceneNodeFPS();

while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, video::SColor(0,200,200,200));
smgr->drawAll();
driver->endScene();
}

device->drop();

return 0;
}


쉽지 않은가?


----------------------------------------------------------------------------------------

irrEdit과 *.irr가 얼마나 중대한 기점인지 느껴진다. 지금까지 작은 연습 프로젝트 하나 하려고 해도 직접 수작업을 해줬는데, 이제는 많은 부분을 툴에 넘겨 줄 수 있게 되었다. 실제 내가 참여중인 모종의 아마추어 프로젝트에서 요구된 애니메이션 월드, 파티클 시스템 등이 다 해결 될 것 같다. irrEdit이 아니었다면 이런 작업 툴 만드는데만 6개월은 넘게 걸렸을 것이다.


점점 더 멋진 엔진으로 발전하고 있는 IRRLICHT엔진과 개발자에게 감사한다.


14. Win32Window

Posted 2007/09/15 19:28 by 수달

--------------------------------------------------------------------------

튜토리얼 : 14. 32비트 윈도형 예제

-------------------------------


이 예제는 윈도OS에서만 실행되며, IRRLICHT가 Win32 체제에서 어떻게 구현되는지 보여준다.


#include <irrlicht.h>
#include <windows.h> // this example only runs with windows

using namespace irr;

#pragma comment(lib, "irrlicht.lib")

HWND hOKButton;
HWND hWnd;

static LRESULT CALLBACK CustomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
HWND hwndCtl = (HWND)lParam;
int code = HIWORD(wParam);

if (hwndCtl == hOKButton)
{
DestroyWindow(hWnd);
PostQuitMessage(0);
return 0;
}
}
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;

}

return DefWindowProc(hWnd, message, wParam, lParam);
}



int main()
//int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hpre, LPSTR cmd, int cc)
{
HINSTANCE hInstance = 0;
// create dialog

const char* Win32ClassName = "CIrrlichtWindowsTestDialog";

WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)CustomWndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = DLGWINDOWEXTRA;
wcex.hInstance = hInstance;
wcex.hIcon = NULL;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW);
wcex.lpszMenuName = 0;
wcex.lpszClassName = Win32ClassName;
wcex.hIconSm = 0;

RegisterClassEx(&wcex);

DWORD style = WS_SYSMENU | WS_BORDER | WS_CAPTION |
WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_SIZEBOX;

int windowWidth = 440;
int windowHeight = 380;

hWnd = CreateWindow( Win32ClassName, "Irrlicht Win32 window example",
style, 100, 100, windowWidth, windowHeight,
NULL, NULL, hInstance, NULL);

RECT clientRect;
GetClientRect(hWnd, &clientRect);
windowWidth = clientRect.right;
windowHeight = clientRect.bottom;

// create ok button

hOKButton = CreateWindow("BUTTON", "OK - Close", WS_CHILD | WS_VISIBLE | BS_TEXT,
windowWidth - 160, windowHeight - 40, 150, 30, hWnd, NULL, hInstance, NULL);

// create some text

CreateWindow("STATIC", "This is Irrlicht running inside a standard Win32 window.\n"\
"Also mixing with MFC and .NET Windows.Forms is possible.",
WS_CHILD | WS_VISIBLE, 20, 20, 400, 40, hWnd, NULL, hInstance, NULL);

// create window to put irrlicht in

HWND hIrrlichtWindow = CreateWindow("BUTTON", "", WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
50, 80, 320, 220, hWnd, NULL, hInstance, NULL);

// create irrlicht device in the button window

irr::SIrrlichtCreationParameters param;
param.WindowId = reinterpret_cast<s32>(hIrrlichtWindow); // hColorButton
param.DriverType = video::EDT_OPENGL;

irr::IrrlichtDevice* device = irr::createDeviceEx(param);

// setup a simple 3d scene

irr::scene::ISceneManager*smgr = device->getSceneManager();
video::IVideoDriver* driver = device->getVideoDriver();

scene::ICameraSceneNode* cam = smgr->addCameraSceneNode();
cam->setTarget(core::vector3df(0,0,0));

scene::ISceneNodeAnimator* anim = smgr->createFlyCircleAnimator(core::vector3df(0,10,0), 30.0f);
cam->addAnimator(anim);
anim->drop();

scene::ISceneNode* cube = smgr->addTestSceneNode(25);

cube->setMaterialTexture(0, driver->getTexture("../../media/rockwall.bmp"));

smgr->addSkyBoxSceneNode(
driver->getTexture("../../media/irrlicht2_up.jpg"),
driver->getTexture("../../media/irrlicht2_dn.jpg"),
driver->getTexture("../../media/irrlicht2_lf.jpg"),
driver->getTexture("../../media/irrlicht2_rt.jpg"),
driver->getTexture("../../media/irrlicht2_ft.jpg"),
driver->getTexture("../../media/irrlicht2_bk.jpg"));

// show and execute dialog

ShowWindow(hWnd , SW_SHOW);
UpdateWindow(hWnd);


메세지 큐를 넣는 곳. 하지만 예제처럼 GetMessage를 이용하는 Win32 형식을 사용해도 된다. Device->run()을 호출하면 IRRLICHT는 내부적으로 메세지를 번역처리(dispatch) 해 줄 것이다. (역자 주 : Win32 메세지 처리에서 DispatchMessage()한다. 이에 대해선 www.winapi.co.kr 을 참고하라.) Device->run()을 사용하지 않는다면 IRRLICHT는 사용자의 입력을 처리할 수 없다. 이 경우 개발자는 윈도 메세지나 DirectInput 등 다른 방식으로 처리해야 할 것이다.


while (device->run())
{
driver->beginScene(true, true, 0);
smgr->drawAll();
driver->endScene();
}

// the alternative, own message dispatching loop without Device->run() would
// look like this:

/*MSG msg;
while (true)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);

if (msg.message == WM_QUIT)
break;
}

// advance virtual time
device->getTimer()->tick();

// draw engine picture
driver->beginScene(true, true, 0);
smgr->drawAll();
driver->endScene();
}*/

device->closeDevice();
device->drop();

return 0;
}


---------------------------------------------------------------------------

드디어 튜토리얼이 끝났다.


사실 이번 예제는 윈도 개발자를 위한 보너스 같은 것이다.


예제 작성자의 굿 센스에 경의를 보낸다.


그래서인지 설명할 것도 적다. 도리어 윈도 프로그래밍 관련 정보를 보는게 좋겠다.


남은 것이 있다면, examples 폴더에는 여러 예제들 말고 Demo 폴더가 있는 것이다.


데모를 돌려본 결과, 그간의 예제를 한꺼번에 보여주는 것이었다.


음...이런 것에 대해선 읽고 주석을 달게 아닌 것 같다.




예제가 끝났으니, 앞으로는 IRRLICHT 관련 코드와 내용을 (찾으면) 올리도록 하겠다.


또한 IRRLICHT를 이용해 작고 순수한 아마추어 게임을 만드는 과정을 올리고 싶다.


될진 모르겠지만.


13. RenderToTexture (+06.08.04)

Posted 2007/09/15 19:28 by 수달

--------------------------------------------------------------------------

튜토리얼 : 13. 텍스처에 그리기

----------------------------

이번 예제는 IRRLICHT에서 한 텍스처에 어떤 그림을 어떻게 그리는지 보여줄 것이다. 텍스처에 무언가를 그리는 것은 꽤 멋진 효과를 만들어 낸다. 또한 이 예제는 반사광(specular)을 어떻게 나타내야 할지 보여준다.


+ 여기서 텍스처에 무언가를 그린 다는 개념은, FPS 게임 프레이의 이공간을 이동할 때 사용되는 포탈 모습과 비슷하다. 즉, 어떤 대상 텍스처에 또하나의 카메라에 보이는 모습을 그린다는 뜻이다. 아래 코드를 보면, 사용자가 쓰는 FPS 카메라와 고정 카메라가 나온다. 고정 카메라에 비추는 모습을 대상 텍스처에 그리게 된다. 아래 그림은 프레이의 한 장면들로, 좌측 붉게 타오르는 둥근 원이 포탈인데, 주요 화면과 포탈 속 화면의 보여지는게 다름을 알 수 있다.




--------------------------------------------------------------------------

시작

----

시작부는 다른 예제와 같다. 헤더 파일을 넣고, 렌더링 방식을 물은 다음, IRRLICHT 엔진을 만들자.


#include <irrlicht.h>
#include <iostream>

using namespace irr;

#pragma comment(lib, "Irrlicht.lib")

int main()
{
// let user select driver type

video::E_DRIVER_TYPE driverType = video::EDT_DIRECT3D9;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\
" (d) Software Renderer\n (e) Apfelbaum Software Renderer\n"\
" (f) NullDevice\n (otherKey) exit\n\n");

char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECT3D9;break;
case 'b': driverType = video::EDT_DIRECT3D8;break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_SOFTWARE2;break;
case 'f': driverType = video::EDT_NULL; break;
default: return 1;
}

// create device and exit if creation failed

IrrlichtDevice *device =
createDevice(driverType, core::dimension2d<s32>(640, 480),
16, false, false);

if (device == 0)
return 1; // could not create selected driver.

video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* env = device->getGUIEnvironment();


화면에 보여질 애니메이션하는 메시를 읽자. 다른 예제에서 많이 쓰인 MD2 모델 페어리를 사용하기로 한다. 다른 예제와의 차이점은 모델의 밝기(광택?) 값이 기본 값 0이 아니라는 것이다. 밝기가 0이 아니고 동적 광원이 있는 경우, 모델에 대한 반사광이 가능하다. 그리고 그 값은 하이라이트의 크기에도 영향을 준다.


// load and display animated fairy mesh

scene::IAnimatedMeshSceneNode* fairy = smgr->addAnimatedMeshSceneNode(
smgr->getMesh("../../media/faerie.md2"));

if (fairy)
{
fairy->setMaterialTexture(0, driver->getTexture("../../media/faerie2.bmp")); // set diffuse texture
fairy->setMaterialFlag(video::EMF_LIGHTING, true); // enable dynamic lighting
fairy->getMaterial(0).Shininess = 20.0f; // set size of specular highlights
fairy->setPosition(core::vector3df(-10,0,-100));
}

모델에 반사광을 반들기 위해 씬 안에 동적 광원이 필요하다. 모델 근처에 광원을 바로 놓자. 그리고 모델을 지나치게 어둡지 않게 회색의 주변광(ambient)을 설정하자.


// add white light
scene::ILightSceneNode* light = smgr->addLightSceneNode(0,
core::vector3df(-15,5,-105), video::SColorf(1.0f, 1.0f, 1.0f));

// set ambient light
driver->setAmbientLight(video::SColor(0,60,60,60));

다음 것들은 일반적인 내용들이다. 씬에 사용자가 다루는 카메라를 넣고, 마우스 커서를 끄며, 테스트용 정육면체를 만들어 회전시켜 보겠다.


// add fps camera
scene::ICameraSceneNode* fpsCamera = smgr->addCameraSceneNodeFPS();
fpsCamera->setPosition(core::vector3df(-50,50,-150));

// disable mouse cursor
device->getCursorControl()->setVisible(false);

// create test cube
scene::ISceneNode* test = smgr->addTestSceneNode(60);

// let the cube rotate and set some light settings
scene::ISceneNodeAnimator* anim = smgr->createRotationAnimator(
core::vector3df(0.3f, 0.3f,0));

test->setPosition(core::vector3df(-100,0,-100));
test->setMaterialFlag(video::EMF_LIGHTING, false); // enable dynamic lighting
test->addAnimator(anim);
anim->drop();

// set window caption
device->setWindowCaption(L"Irrlicht Engine - Render to Texture and Specular Highlights example");


텍스처에 그리기를 해보기 위해선, 대상 텍스처가 필요하다. 일반적인 텍스처와는 다르지만 어쨌건 일단 생성은 되야 한다. IVideoDriver::createRenderTargetTexture()를 호출하고 텍스처 크기를 맞추자. 대상 텍스처는 프레임 버퍼와 Z버퍼를 공유하기 때문에, 프레임 버퍼보다 큰 사이즈면 안 된다. 예제가 표현하려는 것을 모두 보여주기 위해, 장면에 고정된 카메라를 더할 것이다. (역자 주 : 파란 테스트 정육면체의 페어리는 항상 같은 방향에서만 보인다. 고정 카메라는 이것을 의미하며, 전체 씬은 여전히 방향키와 마우스로 자유롭게 볼 수 있다.) 이 모든 것 이전에 과연 현재 장치가 텍스처 그리기를 지원하는지 살펴야 한다. 만약 못할 경우 경고 문구를 보낼 것이다.


// create render target
video::ITexture* rt = 0;
scene::ICameraSceneNode* fixedCam = 0;

if (driver->queryFeature(video::EVDF_RENDER_TO_TARGET))
{
rt = driver->createRenderTargetTexture(core::dimension2d<s32>(256,256));
test->setMaterialTexture(0, rt); // set material of cube to render target

// add fixed camera
fixedCam = smgr->addCameraSceneNode(0, core::vector3df(10,10,-80),
core::vector3df(-10,10,-100));
}
else
{
// create problem text
gui::IGUISkin* skin = env->getSkin();
gui::IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp");
if (font)
skin->setFont(font);

gui::IGUIStaticText* text = env->addStaticText(
L"Your hardware or this renderer is not able to use the "\
L"render to texture feature. RTT Disabled.",
core::rect<s32>(150,20,470,60));

text->setOverrideColor(video::SColor(100,255,255,255));
}

거의 끝나간다. 매 프레임마다 장면을 두번씩 그린다. 한번은 대상 텍스처에 고정 카메라로, 한번은 일반적으로 말이다. 대상 텍스처에 그리기가 실행될 때는 테스트 육면체를 안보이게 해야한다. 그래야 대상 텍스처 그리기가 적용된다.


while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, 0);

if (rt)
{
// draw scene into render target

// set render target texture
driver->setRenderTarget(rt, true, true, video::SColor(0,0,0,255));

// make cube invisible and set fixed camera as active camera
test->setVisible(false);
smgr->setActiveCamera(fixedCam);

// draw whole scene into render buffer
smgr->drawAll();

// set back old render target
driver->setRenderTarget(0);

// make the cube visible and set the user controlled camera as active one
test->setVisible(true);
smgr->setActiveCamera(fpsCamera);
}

// draw scene normally
smgr->drawAll();
env->drawAll();

driver->endScene();
}

if (rt)
rt->drop(); // drop render target because we created if with a create() method

device->drop(); // drop device
return 0;
}


---------------------------------------------------------------------------

텍스처에 임의의 그림을 그리는 기술을 소개했다.


+ 서로 다른 카메라에 비추는 3D 공간을 표현하는 매우 대단한 기술이다.


12. TerrainRendering

Posted 2007/09/15 19:27 by 수달

-------------------------------------------------------------------------

튜토리얼 : 12. 지형 그리기

------------------------

이번 예제는 IRRLICHT에서 지형(terrain)을 어떻게 그리는지 간단히 보여줄 것이다. 또한 지형 충돌 확인을 위한 '지형 삼각형 선택자(the terrain renderer triangle selector)'를 보일 것이다. 주지할 것은 IRRLICHT의 지형 그리기는 Sprintz의 GeoMipMapSceneNode에 기반한다는 것이며, 매우 감사하게 생각한다.


-------------------------------------------------------------------------

시작

----


시작은 특별한 것이 없다. 헤더 파일을 넣고, 사용자가 와이어 프레임을 보려 할때 쓸 'W'와 지형을 세밀하게 볼지 안 볼지 결정케 하는 'D' 키를 받는 이벤트 리시버만 있으면 된다.


#include <irrlicht.h>
#include <iostream>

using namespace irr;

#pragma comment(lib, "Irrlicht.lib")


class MyEventReceiver : public IEventReceiver
{
public:

MyEventReceiver(scene::ISceneNode* terrain)
{
//지형 포인터를 저장해 원하는데로 그리기 모드를 바꿀 수 있게 한다.
Terrain = terrain;
}

bool OnEvent(SEvent event)
{
// check if user presses the key 'W' or 'D'
if (event.EventType == irr::EET_KEY_INPUT_EVENT && !event.KeyInput.PressedDown)
{
switch (event.KeyInput.Key)
{
case irr::KEY_KEY_W: // switch wire frame mode
Terrain->setMaterialFlag(video::EMF_WIREFRAME, !Terrain->getMaterial(0).Wireframe);
return true;
case irr::KEY_KEY_D: // toggle detail map
Terrain->setMaterialType(
Terrain->getMaterial(0).MaterialType == video::EMT_SOLID ?
video::EMT_DETAIL_MAP : video::EMT_SOLID);
return true;
}
}

return false;
}

private:
scene::ISceneNode* Terrain;
};


main()함수의 시작은 다른 예제와 같다. 사용자에게 원하는 그리기 방식을 묻고 장치를 설정하자.


int main()
{
// let user select driver type

video::E_DRIVER_TYPE driverType = video::EDT_DIRECT3D9;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\
" (d) Software Renderer\n (e) Apfelbaum Software Renderer\n"\
" (f) NullDevice\n (otherKey) exit\n\n");

char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECT3D9;break;
case 'b': driverType = video::EDT_DIRECT3D8;break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_SOFTWARE2;break;
case 'f': driverType = video::EDT_NULL; break;
default: return 1;
}

// create device

IrrlichtDevice* device = createDevice(driverType, core::dimension2d<s32>(640, 480));

if (device == 0)
return 1; // could not create selected driver.


우선 자질구레한 것부터 추가하자. IRRLICHT 엔진 로고를 넣고, 작은 도움말을 넣으며, 마우스 커서를 없애는 대신 사용자가 이용 할 수 있는 카메라를 만들자.

video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* env = device->getGUIEnvironment();

driver->setTextureCreationFlag(video::ETCF_ALWAYS_32_BIT, true);

// add irrlicht logo
env->addImage(driver->getTexture("../../media/irrlichtlogoalpha.tga"),
core::position2d<s32>(10,10));

// add some help text
gui::IGUIStaticText* text = env->addStaticText(
L"Press 'W' to change wireframe mode\nPress 'D' to toggle detail map",
core::rect<s32>(10,453,200,475), true, true, 0, -1, true);

// add camera
scene::ICameraSceneNode* camera =
smgr->addCameraSceneNodeFPS(0,100.0f,1200.0f);

camera->setPosition(core::vector3df(1900*2,255*2,3700*2));
camera->setTarget(core::vector3df(2397*2,343*2,2700*2));
camera->setFarValue(12000.0f);

// disable mouse cursor
device->getCursorControl()->setVisible(false);


'지형 그리기' 씬 노드를 구성할 차례다. ISceneManager::addTerrainSceneNode()를 사용해 다른 씬 노드처럼 더하기만 하면 된다. 하나뿐인 인자는 사용할 높이맵의 파일 이름이다. 높이맵은 보통 흑백의 텍스처다. '지형 그리기'는 그것을 읽어 3D 지형으로 변환할 것이다.

지형을 좀 더 크게 하기 위해 크기를 (40, 4.4, 40)으로 바꾼다. 이번 예제에선 동적 광원을 쓰지 않을 것이기 때문에, 광원을 끄고 지형 텍스처로 terrain-texture.jpg를 설정하고, 두번째 텍스처로 detailmap3.jpg를 두자. (이렇게 쓰이는 두번째 맵을 이른바 상세 맵 detail map이라고 한다.) 마지막으로 텍스처에 대해 크기값을 넣는다. 첫번째 텍스처는 전체 지형에 한번만 쓰이고, 두번째 상세 맵은 20배로 쓰일 것이다.


// add terrain scene node
scene::ITerrainSceneNode*terrain = smgr->addTerrainSceneNode(
"../../media/terrain-heightmap.bmp");

terrain->setScale(core::vector3df(40, 4.4f, 40));
terrain->setMaterialFlag(video::EMF_LIGHTING, false);

terrain->setMaterialTexture(0, driver->getTexture("../../media/terrain-texture.jpg"));
terrain->setMaterialTexture(1, driver->getTexture("../../media/detailmap3.jpg"));

terrain->setMaterialType(video::EMT_DETAIL_MAP);

terrain->scaleTexture(1.0f, 20.0f);


지형에 대해 충돌 확인을 하기 위해, 삼각형 선택자(triangle selector)를 만든다. 지금 어떤 삼각형이 선택 되었는지 알기 위해, 충돌 예제에 있던 식을 따라하자. 지형 삼각형 선택자는 지형과 카메라의 관계를 정하는 것이다. 예제를 실행해 보면 카메라는 지형을 뚫지 못하고 마치 지형 위를 걷거나 나는 것 처럼 표현된다. 이를 구현하기 위해선 애니메이터와 연결된 충돌 확인을 만든 후 카메라에 붙여 준다. (역자 주 : 글로는 어려우나 직접 예제 exe를 실행해 보라. 마치 1인칭 캐릭터가 지형 위에 서 있거나 나는 듯 보인다.)


// create triangle selector for the terrain
scene::ITriangleSelector* selector
= smgr->createTerrainTriangleSelector(terrain, 0);
terrain->setTriangleSelector(selector);
selector->drop();

// create collision response animator and attach it to the camera
scene::ISceneNodeAnimator* anim = smgr->createCollisionResponseAnimator(
selector, camera, core::vector3df(60,100,60),
core::vector3df(0,0,0),
core::vector3df(0,50,0));
camera->addAnimator(anim);
anim->drop();


사용자가 일반 모드와 와이어(wireframe) 모드를 바꿔보기 위해, 이벤트 리시버를 만들고, IRRLICH 엔진이 그 내용을 할게 해야 한다. 추가로 스카이 박스를 만들어 보자.


// create event receiver
MyEventReceiver receiver(terrain);
device->setEventReceiver(&receiver);

// create skybox
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);

smgr->addSkyBoxSceneNode(
driver->getTexture("../../media/irrlicht2_up.jpg"),
driver->getTexture("../../media/irrlicht2_dn.jpg"),
driver->getTexture("../../media/irrlicht2_lf.jpg"),
driver->getTexture("../../media/irrlicht2_rt.jpg"),
driver->getTexture("../../media/irrlicht2_ft.jpg"),
driver->getTexture("../../media/irrlicht2_bk.jpg"));

driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, true);


모든게 다 되었다. 이제 IRRLICHT에서 지형 만드는 법을 알게 된 것이다.


int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, 0 );

smgr->drawAll();
env->drawAll();

driver->endScene();

// display frames per second in window title
int fps = driver->getFPS();
if (lastFPS != fps)
{
core::stringw str = L"Terrain Renderer - Irrlicht Engine [";
str += driver->getName();
str += "] FPS:";
str += fps;

device->setWindowCaption(str.c_str());
lastFPS = fps;
}
}

device->drop();

return 0;
}


-------------------------------------------------------------------------

일단 애매한 것이 render, draw 가 모두 '그린다'라는 것이다. 여기에 display라고 까지 하면 모니터에 대한 우리 눈의 활동을 표현하는데 많은 표현 방법이 있음을 느낀다. IRRLICHT나 JUPITER엔진에서 보면 draw는 직접 (메모리에?) 찍는다는 의미로 쓰이고, Render는 그것을 (총괄적으로) 화면에 보인다는 의미로 쓰인 듯 하다. 마치 3D 공간상의 점인 vertex와 모니터에 궁극적으로 찍히는 2D 점을 말하는 pixel처럼 말이다. (물론 여전히 이 비교도 정확하진 않다.)

또한 헤깔릴 것이 그냥 render를 서술적으로 '그리기'라고 하는데, 나는 renderer라는 '그리는 것' 역시 명사적으로 '그리기'로 표현했다는 점이다. 알아서 봐야하고, 반드시 소스와 영어 원문을 참조할 일이다.


기술에 있어서 용어의 정립은 개인의 개발에서나 타인과의 협조에 있어서도 매우 중요하다. 이런 실정에서 억지로 한글화 하는 것은 우스운 일이다. 허나 어쩌랴 한글로 해석해 넣지 않으면 그냥 익힐 때보다 잘 안 익혀지는 않으니 말이다.


여기서 Terrain은 일반적인 '지형'의 의미이면서, 기술적 '지형'을 대표하는 단어다. 보통 터레인이라고 하면, 건물이나 도로와 대비되는 넓은 필드를 의미한다. 이 예제에 의하면 디자이너가 2D로 등고선을 그려놓으면 IRRLICHT엔진은 그것을 바로바로 3D 지형화 해준다는 것을 의미한다. 현재 위치라던가, 멀리 있는 지형에 대한 감소(LOD)는 이 예제에선 논외지만, 터레인을 이렇게 쉽게 만들 수 있음을 보인 것은 멋진 일이다.

11. PerPixelLighting

Posted 2007/09/15 19:27 by 수달

--------------------------------------------------------------------------

튜토리얼 9 : 픽셀 (당) 광원 효과

----------------------------


이번 예제는 IRRLICHT에서 좀 더 복잡한 재질을 만드는 방법을 보여준다. 시차 맵핑(parallax mapping)과 법선 맵(normal map)을 이용해 각 화소(픽셀 : pixel : 보통 모니터 등 2D 화면의 점을 말함. 이에 대해 vertex는 3D상의 점) 당 빛을 먹인 표면을 구현할 것이다. 또한 안개효과와 움직이는 파티클을 구현한다. 두려워하지 마라! 쉐이더에 대한 경험이 전혀 없더라도 IRRLICHT에선 구현이 가능하다.

우선, 항상 그랬듯이 헤더를 읽어오자.


--------------------------------------------------------------------------

시작

----


#include <irrlicht.h>
#include <iostream>


using namespace irr;

#pragma comment(lib, "Irrlicht.lib")


세가지 타입의 재질을 사용자가 선택할 수 있게하는 이벤트 리시버가 필요하다. 추가로 이벤트 리시버는 현재 사용되는 재질을 표시해 주는 작은 GUI 윈도를 만들 것이다. 특별한 것은 없으니 읽고 넘어가도 된다.


class MyEventReceiver : public IEventReceiver
{
public:

MyEventReceiver(scene::ISceneNode* room,
gui::IGUIEnvironment* env, video::IVideoDriver* driver)
{
// store pointer to room so we can change its drawing mode
Room = room;
Driver = driver;

// set a nicer font
gui::IGUISkin* skin = env->getSkin();
gui::IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp");
if (font)
skin->setFont(font);

// add window and listbox
gui::IGUIWindow* window = env->addWindow(
core::rect<s32>(490,390,630,470), false, L"Use 'E' + 'R' to change");

ListBox = env->addListBox(
core::rect<s32>(2,22,135,78), window);

ListBox->addItem(L"Diffuse");
ListBox->addItem(L"Bump mapping");
ListBox->addItem(L"Parallax mapping");
ListBox->setSelected(1);

// create problem text
ProblemText = env->addStaticText(
L"Your hardware or this renderer is not able to use the "\
L"needed shaders for this material. Using fall back materials.",
core::rect<s32>(150,20,470,60));

ProblemText->setOverrideColor(video::SColor(100,255,255,255));

// set start material (prefer parallax mapping if available)
video::IMaterialRenderer* renderer =
Driver->getMaterialRenderer(video::EMT_PARALLAX_MAP_SOLID);
if (renderer && renderer->getRenderCapability() == 0)
ListBox->setSelected(2);

// set the material which is selected in the listbox
setMaterial();
}

bool OnEvent(SEvent event)
{
// check if user presses the key 'E' or 'R'
if (event.EventType == irr::EET_KEY_INPUT_EVENT &&
!event.KeyInput.PressedDown && Room && ListBox)
{
// change selected item in listbox

int sel = ListBox->getSelected();
if (event.KeyInput.Key == irr::KEY_KEY_R)
++sel;
else
if (event.KeyInput.Key == irr::KEY_KEY_E)
--sel;
else
return false;

if (sel > 2) sel = 0;
if (sel < 0) sel = 2;
ListBox->setSelected(sel);

// set the material which is selected in the listbox
setMaterial();
}

return false;
}

private:

// sets the material of the room mesh the the one set in the
// list box.
void setMaterial()
{
video::E_MATERIAL_TYPE type = video::EMT_SOLID;

// change material setting
switch(ListBox->getSelected())
{
case 0: type = video::EMT_SOLID;
break;
case 1: type = video::EMT_NORMAL_MAP_SOLID;
break;
case 2: type = video::EMT_PARALLAX_MAP_SOLID;
break;
}

Room->setMaterialType(type);


해당하는 재질이 완전하게 구현되지 않을 경우 경고를 해주는 게 필요하다. 물론 응급 처치가 되어 별 문제를 일으키진 않겠지만, 적어도 사용자에게 어떤 상태인지 알려 더 좋은 하드웨어를 선택하도록 할 수 있다.

재질이 현 하드웨어에서 완전하게 그려졌는지 확인하는 것은 간단하다. IMaterialRenderer::getRenderer(type);의 값이 0이면 된다.


video::IMaterialRenderer* renderer = Driver->getMaterialRenderer(type);

// display some problem text when problem
if (!renderer || renderer->getRenderCapability() != 0)
ProblemText->setVisible(true);
else
ProblemText->setVisible(false);
}

private:

gui::IGUIStaticText* ProblemText;
gui::IGUIListBox* ListBox;

scene::ISceneNode* Room;
video::IVideoDriver* Driver;
};


자 재미있는(?) 부분이다. IRRLICHT 엔진을 만들고 씬을 설정하자.


int main()
{
// let user select driver type

video::E_DRIVER_TYPE driverType = video::EDT_DIRECT3D9;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\
" (d) Software Renderer\n (e) Apfelbaum Software Renderer\n"\
" (f) NullDevice\n (otherKey) exit\n\n");

char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECT3D9;break;
case 'b': driverType = video::EDT_DIRECT3D8;break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_SOFTWARE2;break;
case 'f': driverType = video::EDT_NULL; break;
default: return 0;
}

// create device

IrrlichtDevice* device = createDevice(driverType, core::dimension2d<s32>(640, 480));

if (device == 0)
return 1; // could not create selected driver.


앞서서 몇가지 할 일이 있다. 엔진의 가장 중요한 부분(비디오 장치, 씬 매니저, GUI 환경)의 포인터들을 저장하고, 엔진 로고와 FPS 스타일의 카메라를 붙이자. 또한 엔진이 앞으로 있을 모든 텍스처를 32비트로 저장하게 설정하자. (이는 시차 맵핑을 위해 필요하다.)


video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* env = device->getGUIEnvironment();

driver->setTextureCreationFlag(video::ETCF_ALWAYS_32_BIT, true);

// add irrlicht logo
env->addImage(driver->getTexture("../../media/irrlichtlogoalpha.tga"),
core::position2d<s32>(10,10));

// add camera
scene::ICameraSceneNode* camera =
smgr->addCameraSceneNodeFPS(0,100.0f,300.0f);
camera->setPosition(core::vector3df(-200,200,-200));

// disable mouse cursor
device->getCursorControl()->setVisible(false);


전체적인 분위기를 으시시하게(scarier) 하기 위해 안개를 넣기로 한다. 안개는 IVideoDriver::setFog()를 호출하면 된다. 여러가지 설정을 할 수가 있으며, 이 안개에 영향을 받는 씬 노드마다 EMF_FOG_ENABLE 표식(flag)을 true로 재질에 해야 한다.


driver->setFog(video::SColor(0,138,125,81), true, 250, 1000, 0, true);


anim8or로 만든 *.3ds 방 모델을 읽자. 이것은 특수 효과시에 쓰인 것과 같다. 그 예제에서도 말했듯 그다지 잘 만든 모델이 아니지만, IMeshManipulator::makePlanarTextureMapping() 등을 이용해 쉽게 분위기를 살릴 수 있다.


scene::IAnimatedMesh* roomMesh = smgr->getMesh(
"../../media/room.3ds");
scene::ISceneNode* room = 0;

if (roomMesh)
{
smgr->getMeshManipulator()->makePlanarTextureMapping(
roomMesh->getMesh(0), 0.003f);


자, 첫번째 재밌는 부분이다. 방 읽기에 성공하면, 거기에 텍스처를 입혀야 할 것이다. 이때 그럴 듯하게 보이기 위해서 단순히 (일반적인, 색상있는) 텍스처만 읽지 말고, (흑백으로 되어 있고, 차후 그 흑백 정도로 요철凹凸연산을 할) 높이맵도 읽어오자. 높이맵을 통해 방의 두번째 텍스처로 쓰인 법선맵을 만들 것이다. 만약 이미 법선맵을 가졌다면, 그것을 바로 쓸 수 있다. 하지만 이번 경우는 적절한 법선맵을 찾질 못했었다.

법선맵은 VideoDriver의 makeNormalMapTexture 함수로 만들어진다. 두번째 인수는 높이맵의 높이를 정한다. 값이 클수록 맵은 더욱 돌덩이처럼, 딱딱하게(rocky) 보일 것이다.


video::ITexture* colorMap = driver->getTexture("../../media/rockwall.bmp");
video::ITexture* normalMap = driver->getTexture("../../media/rockwall_height.bmp");

driver->makeNormalMapTexture(normalMap, 9.0f);


색상을 정하고 법선맵을 만들었다고 다 된 것은 아니다. 재질은 탄젠트나 종법선(binormals : 표면에 수직인 법선)같은 각 3D정점(vertex)마다 추가해야할 정보를 필요로 한다. 그런 계산을 직접하기는 번거로우니, IRRLICHT 엔진에게 맡기자. IMeshManipulator::createMeshWithTangents()를 부르면 해결 된다. 이것은 임의의 메시로부터 탄젠트와 종법선이 적용된 메시 복사본을 만든다. 그렇게 한 후, 이 메시 복사본으로 일반적인 메시 씬 노드를 만들어 색상과 법선을 정하고, 기타 여러 재질 정보를 입력하면 된다.

기억하라! 안개를 만들려면 EMF_FOG_ENABLE을 설정해야 한다.


scene::IMesh* tangentMesh = smgr->getMeshManipulator()->createMeshWithTangents(
roomMesh->getMesh(0));

room = smgr->addMeshSceneNode(tangentMesh);
room->setMaterialTexture(0, colorMap);
room->setMaterialTexture(1, normalMap);

room->getMaterial(0).EmissiveColor.set(0,0,0,0);

room->setMaterialFlag(video::EMF_FOG_ENABLE, true);
room->setMaterialType(video::EMT_PARALLAX_MAP_SOLID);
room->getMaterial(0).MaterialTypeParam = 0.035f; // adjust height for parallax effect

// drop mesh because we created it with a create.. call.
tangentMesh->drop();
}


화소 당 빛을 먹이는(?) 쉐이더 기술로 방을 만든 후, 같은 재질로 움직이는 공을 더하겠다. 그리고 공을 자전 운동하는 행성처럼 보이게 하겠다. 방식은 이전 예제에서 보인 바와 같다. 차이점이라면 *.x 파일로 부터 읽는다는 것이고, (직접 손 델 필요 없이) 이미 색상맵을 갖고 있다는 것이다. 공의 원형은 좀 작다. 그래서 크기 비율을 높여주어야 한다.


// add earth sphere

scene::IAnimatedMesh* earthMesh = smgr->getMesh("../../media/earth.x");
if (earthMesh)
{
// create mesh copy with tangent informations from original earth.x mesh
scene::IMesh* tangentSphereMesh =
smgr->getMeshManipulator()->createMeshWithTangents(earthMesh->getMesh(0));

//모든 점에 200의 알파값을 먹인다.

smgr->getMeshManipulator()->setVertexColorAlpha(tangentSphereMesh, 200);

//50 정도로 크기를 키운다.
smgr->getMeshManipulator()->scaleMesh(
tangentSphereMesh, core::vector3df(50,50,50));

// create mesh scene node
scene::ISceneNode* sphere = smgr->addMeshSceneNode(tangentSphereMesh);
sphere->setPosition(core::vector3df(-70,130,45));

// load heightmap, create normal map from it and set it
video::ITexture* earthNormalMap = driver->getTexture("../../media/earthbump.bmp");
driver->makeNormalMapTexture(earthNormalMap, 20.0f);
sphere->setMaterialTexture(1, earthNormalMap);

// adjust material settings
sphere->setMaterialFlag(video::EMF_FOG_ENABLE, true);
sphere->setMaterialType(video::EMT_NORMAL_MAP_TRANSPARENT_VERTEX_ALPHA);

// add rotation animator
scene::ISceneNodeAnimator* anim =
smgr->createRotationAnimator(core::vector3df(0,0.1f,0));
sphere->addAnimator(anim);
anim->drop();

// drop mesh because we created it with a create.. call.
tangentSphereMesh->drop();
}


각 화소마다 빛을 먹은 재질은 움직이는 광원이 있을 때 그 진가를 발휘한다. 광원을 추가하자! 그러나 그냥 움직이기만 하면 재미없을테니, 여러 빌보드를 추가하고 그중 하나에 파티클을 사용하겠다.


// 광원1을 더함.(빨강에 가깝게) 광원1에는 파티클과 상관없음.
scene::ILightSceneNode* light1 =
smgr->addLightSceneNode(0, core::vector3df(0,0,0),
video::SColorf(0.5f, 1.0f, 0.5f, 0.0f), 200.0f);

//광원1에 회전하며 나는(fly circle) 애니메이션 적용
scene::ISceneNodeAnimator* anim =
smgr->createFlyCircleAnimator (core::vector3df(50,300,0),190.0f, -0.003f);
light1->addAnimator(anim);
anim->drop();

//광원에 빌보드 붙임
scene::ISceneNode* bill =
smgr->addBillboardSceneNode(light1, core::dimension2d<f32>(60, 60));

bill->setMaterialFlag(video::EMF_LIGHTING, false);
bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
bill->setMaterialTexture(0, driver->getTexture("../../media/particlered.bmp"));


같은 식으로 두번째 광원을 만들겠다. 차이점은 파티클 체계(역자 주 : system을 체계라고 했는데, 파티클 한 묶음을 의미하는 말이다.)가 들어간다는 것이다. 여기서 만들 파티클 체계들은 광원의 움직임에 따라다니는 파티클을 만든다. 파티클 체계가 어떻게 만들어지는지는 SpecialFx예제를 참조하라.

광원은 두 개를 쓸 것이다. 여기에는 이유가 있다. 우리가 쓰는 하위 버전 재질은 ps 1.1과 vs 1.1이 적용된 것인데, 이들은 광원을 두 개까지만 허용한다. 세번째 광원을 쓸 수는 있다. 그러나 쉐이더 기능은 하지 못한다. 물론 앞으로 IRRLICHT의 버전이 변하면 더 높은 쉐이더를 지원하게 될 것이다. (어쩌면......)


// add light 2 (gray)
scene::ISceneNode* light2 =
smgr->addLightSceneNode(0, core::vector3df(0,0,0),
video::SColorf(1.0f, 0.2f, 0.2f, 0.0f), 200.0f);

// add fly circle animator to light 2
anim = smgr->createFlyCircleAnimator (core::vector3df(0,150,0),200.0f);
light2->addAnimator(anim);
anim->drop();

// attach billboard to light
bill = smgr->addBillboardSceneNode(light2, core::dimension2d<f32>(120, 120));
bill->setMaterialFlag(video::EMF_LIGHTING, false);
bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
bill->setMaterialTexture(0, driver->getTexture("../../media/particlewhite.bmp"));

// add particle system
scene::IParticleSystemSceneNode* ps =
smgr->addParticleSystemSceneNode(false, light2);

ps->setParticleSize(core::dimension2d<f32>(30.0f, 40.0f));

// create and set emitter
scene::IParticleEmitter* em = ps->createBoxEmitter(
core::aabbox3d<f32>(-3,0,-3,3,1,3),
core::vector3df(0.0f,0.03f,0.0f),
80,100,
video::SColor(0,255,255,255), video::SColor(0,255,255,255),
400,1100);
ps->setEmitter(em);
em->drop();

// create and set affector
scene::IParticleAffector* paf = ps->createFadeOutParticleAffector();
ps->addAffector(paf);
paf->drop();

// adjust some material settings
ps->setMaterialFlag(video::EMF_LIGHTING, false);
ps->setMaterialTexture(0, driver->getTexture("../../media/fireball.bmp"));
ps->setMaterialType(video::EMT_TRANSPARENT_VERTEX_ALPHA);


MyEventReceiver receiver(room, env, driver);
device->setEventReceiver(&receiver);

자, 모든 것을 그려보도록 하자.


int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, 0);

smgr->drawAll();
env->drawAll();

driver->endScene();

int fps = driver->getFPS();

if (lastFPS != fps)
{
core::stringw str = L"Per pixel lighting example - Irrlicht Engine [";
str += driver->getName();
str += "] FPS:";
str += fps;

device->setWindowCaption(str.c_str());
lastFPS = fps;
}
}

device->drop();

return 0;
}


-----------------------------------------------------------------------------

우선 용어부터 간략히 살펴보자.


- Normal Map : 법선 맵 : 원래는 노말 범프맵(normal bump map)이며, 메쉬 표면의 법선 벡터를 텍스쳐 파일에 저장해 놓은 것을 말함. (법선은 폴리곤의 단위 방향) 범프 맵핑(Bump mapping)이라고도 하는데 맵핑 이미지의 명도 단계가 셰이딩(shading) 할 때와 반영할 때의 표면 평균값에 영향을 미치도록하여 마치 객체의 표면이 굴곡지거나 울룩불룩한 것처럼 보이도록 하는 방식이다.


- Parallax mapping : 시차 맵핑 : 은 범프를 계산할 때 노말맵에서 가져오는 픽셀좌표를 높이맵을 이용 정확한 좌표(범프가 표현하는 볼륨에 해당하는)로 보정해 좀 더 뛰어난 볼륨감을 표현하는 기법이다.


즉, 맵핑(텍스처를 입히는 동작?)시 자동으로 좀 더 현실적인 굴곡, 음영이 나타나게 하는 것이다. 예제를 실행해 E와 R 키로 조정하면 기본인 Diffuse와의 차이를 알 수 있을 것이다.


10. Shaders

Posted 2007/09/15 19:26 by 수달

이전까지 IRRLICHT 엔진 0.12.0에 의했고, 이번부터는 0.14.0에 의한다.


그리고 shadow, shading, shader에 대해선 맨 밑에 간략히 적었는데,


더 깊고 정확한 사항은 각자 찾길 바란다.


-------------------------------------------------------------------------

튜토리얼 10 : 쉐이더

-------------------

이번 예제는 엔진에서 D3D8, D3D9, OpenGL을 이용해 어떻게 쉐이더를 쓰는지 보일 것이다. 또한 씬 노드에서 텍스트 사용하는 법과 텍스쳐 읽을 떼 밉맵(mipmap) 생성을 못하게 하는 방법을 볼 것이다.

이 예제는 쉐이더 작업을 어떻게 하는지 설명하지 않는다. 그것들에 관해선 D3D나 OpenGL의 해당 문서를 읽길 바란다.

그럼 언제나 처럼 헤더를 읽고, 시작에 필요한 작업을 하자.


-------------------------------------------------------------------------

시작

----


#include <irrlicht.h>
#include <iostream>


using namespace irr;

#pragma comment(lib, "Irrlicht.lib")


여러가지 흥미로운 쉐이더를 써보기 위해, 색상 연산을 해주는 몇가지 데이터를 설정해야 한다. 예를 들어, 예제에서 구현해 볼 간단한 버텍스 쉐이더(vertex shader : 정점 음영)는 카메라의 위치에 근거해 버텍스의 색상을 계산할 것이다. 이를 위해 쉐이더는 변환 법선용 역(逆) 월드 행렬, 변환 위치용 클립 행렬, 광원 각도 계산을 위한 카메라와 월드 위치, 그리고 광원의 색깔 데이터를 필요로 한다.

매 프레임마다 이 모든 데이터를 쉐이더에게 알려주기 위해 클래스 하나를 만들 것인데, 이 클래스는 IShaderConstantSetCallBack에서 상속받고, OnSetConstants()를 재정의(override) 하게 된다. 이 함수는 재질이 설정되는 매 번마다 호출되게 될 것이다.

IMaterialRendererServices의 setVertexShaderConstant() 함수는 위에 말한 각 데이터를 쉐이더의 필요에 따라 설정하게 해준다. 만약 사용자가 어셈블리 대신 HLSL 같은 고급 레벨 쉐이더 언어를 사용한다면, 레지스터 배열 대신 인자로 변수명을 넣어주면 된다. (역자 주 : 잘 모르겠으나, IRRLICHT 엔진에 적용되는 고급 쉐이더 언어의 식별 이름이 있을 것으로 생각된다.)


IrrlichtDevice* device = 0;
bool UseHighLevelShaders = false;

class MyShaderCallBack : public video::IShaderConstantSetCallBack
{
public:

virtual void OnSetConstants(video::IMaterialRendererServices* services,

s32 userData)
{
video::IVideoDriver* driver = services->getVideoDriver();

//월드 행렬의 역행렬을 구함
//만약 고급 쉐이더 언어를 사용한다면, 그 이름을 설정해야 한다.

core::matrix4 invWorld = driver->getTransform(video::ETS_WORLD);
invWorld.makeInverse();

if (UseHighLevelShaders)
services->setVertexShaderConstant("mInvWorld", &invWorld.M[0], 16);
else
services->setVertexShaderConstant(&invWorld.M[0], 0, 4);

//클립 행렬 설정(월드, 뷰, 프로젝션 행렬)

core::matrix4 worldViewProj;
worldViewProj = driver->getTransform(video::ETS_PROJECTION);
worldViewProj *= driver->getTransform(video::ETS_VIEW);
worldViewProj *= driver->getTransform(video::ETS_WORLD);

if (UseHighLevelShaders)
services->setVertexShaderConstant("mWorldViewProj",

&worldViewProj.M[0], 16);
else
services->setVertexShaderConstant(&worldViewProj.M[0], 4, 4);

//카메라 위치 설정

core::vector3df pos = device->getSceneManager()->
getActiveCamera()->getAbsolutePosition();

if (UseHighLevelShaders)
services->setVertexShaderConstant("mLightPos",

reinterpret_cast<f32*>(&pos), 3);
else
services->setVertexShaderConstant(reinterpret_cast<f32*>(&pos), 8, 1);

//광원 설정

video::SColorf col(0.0f,1.0f,1.0f,0.0f);

if (UseHighLevelShaders)
services->setVertexShaderConstant("mLightColor",

reinterpret_cast<f32*>(&col), 4);
else
services->setVertexShaderConstant(reinterpret_cast<f32*>(&col), 9, 1);

//월드 행렬 변환
core::matrix4 world = driver->getTransform(video::ETS_WORLD);
world = world.getTransposed();

if (UseHighLevelShaders)
services->setVertexShaderConstant("mTransWorld", &world.M[0], 16);
else
services->setVertexShaderConstant(&world.M[0], 10, 4);
}
};


다음은 엔진 시작을 위한 코드다. 대부분 다른 예제와 같으나, 추가로 사용자에게 고급 레벨 쉐이더 언어를 사용할 것인지 아닌지 선택하게 한다.


int main()
{
// let user select driver type

video::E_DRIVER_TYPE driverType = video::EDT_DIRECT3D9;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\
" (d) Software Renderer\n (e) Apfelbaum Software Renderer\n"\
" (f) NullDevice\n (otherKey) exit\n\n");

char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECT3D9;break;
case 'b': driverType = video::EDT_DIRECT3D8;break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_SOFTWARE2;break;
case 'f': driverType = video::EDT_NULL; break;
default: return 1;
}

// ask the user if we should use high level shaders for this example
if (driverType == video::EDT_DIRECT3D9 ||
driverType == video::EDT_OPENGL)
{
printf("Please press 'y' if you want to use high level shaders.\n");
std::cin >> i;
if (i == 'y')
UseHighLevelShaders = true;
}

// create device

device = createDevice(driverType, core::dimension2d<s32>(640, 480));

if (device == 0)
return 1; // could not create selected driver.


video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* gui = device->getGUIEnvironment();


자, 이제 좀 더 재밌는 부분이다. 만약 Direct3D를 사용한다면 버텍스 쉐이더와 픽셀 쉐이더를, OpenGL을 사용한다면 ARB 와 버텍스 프로그램을 사용하고 싶을 것이다.엔진은 이들을 각각 d3d8.ps, d3d8.vs, d3d9.ps, d3d9.vs, opengl.ps, opengl.vs 파일을 통해 적용할 수 있다. 즉, 개발자는 단지 해당하는 파일 이름만 표시하면 된다. 예제에서는 이를 switch() 구문으로 해결한다. 예제에서처럼 꼭 텍스트 파일에 쉐이더를 쓸 필요는 없다. 개발자는 직접 쉐이더를 cpp 소스 파일에 써도 된다. 이 경우 addShaderMaterialFromFiles() 대신 addShaderMaterial()을 쓴다.


c8* vsFileName = 0; // filename for the vertex shader
c8* psFileName = 0; // filename for the pixel shader

switch(driverType)
{
case video::EDT_DIRECT3D8:
psFileName = "../../media/d3d8.psh";
vsFileName = "../../media/d3d8.vsh";
break;
case video::EDT_DIRECT3D9:
if (UseHighLevelShaders)
{
psFileName = "../../media/d3d9.hlsl";
vsFileName = psFileName; // both shaders are in the same file
}
else
{
psFileName = "../../media/d3d9.psh";
vsFileName = "../../media/d3d9.vsh";
}
break;

case video::EDT_OPENGL:
if (UseHighLevelShaders)
{
psFileName = "../../media/opengl.frag";
vsFileName = "../../media/opengl.vert";
}
else
{
psFileName = "../../media/opengl.psh";
vsFileName = "../../media/opengl.vsh";
}
break;
}


하드웨어와 위에서 선택한 렌더링 방식으로, 사용하고자 하는 쉐이더를 실행 할 수 있는가를 확인해 봐야 한다. 만약 불가능하다면 파일 이름에 0을 설정해야 한다. 필수적인 부분은 아니나 이렇게 해두면 유용한 점이 있다. 예를 들어 하드웨어가 버텍스 쉐이더는 실행하나 픽셀 쉐이더는 못할 경우 개발자는 단지 버텍스 쉐이더만을 사용한 새 재질을 만들 수 있다. 그렇지 않으면 엔진은 요구 사항을 완전히 충족하지 못하고 새로운 어떠한 재질도 만들어 낼 수가 없다. 이번 예제에서는 최소한 버텍스 쉐이더의 구현은 볼 수 있을 것이다.


if (!driver->queryFeature(video::EVDF_PIXEL_SHADER_1_1) &&
!driver->queryFeature(video::EVDF_ARB_FRAGMENT_PROGRAM_1))
{
device->getLogger()->log("WARNING: Pixel shaders disabled "\
"because of missing driver/hardware support.");
psFileName = 0;
}

if (!driver->queryFeature(video::EVDF_VERTEX_SHADER_1_1) &&
!driver->queryFeature(video::EVDF_ARB_VERTEX_PROGRAM_1))
{
device->getLogger()->log("WARNING: Vertex shaders disabled "\
"because of missing driver/hardware support.");
vsFileName = 0;
}


그럼 새로운 재질을 만들어 보자. 이전 예제들에서 본 바와 같이, IRRLICHT 엔진의 재질 타입은 SMaterial 구조체의 MaterialType 변수 조절에 의해 쉽게 설정된다. 이번 예제에서 이 32 비트 변수는 video::EMT_SOLID 형으로 정한다. (개발자가 원하는 것으로 정할 수 있다.)

이를 위해 IGPUProgrammingServices의 포인터를 얻고, addShaderMaterialFromFiles()를 호출해 새 32비트 변수 값을 얻는다. 그게 다다. 함수의 인자는 다음과 같다. 첫째 버텍스와 픽셀 쉐이더 코드를 가진 파일 이름(addShaderMaterial()을 대신 사용한다면 직접 문자열을 적으면 된다.), 둘째 IShaderConstantSetCallBack 클래스의 포인터(예제 초반부에 나온다. 원치 않으면 0), 마지막 인자로 기본 재질이 될 엔진상의 인수(예제는 EMT_SOLID와 EMT_TRANSPARENT_ADD_COLOR을 을 보여준다.)


// create materials

video::IGPUProgrammingServices* gpu =

driver->getGPUProgrammingServices();
s32 newMaterialType1 = 0;
s32 newMaterialType2 = 0;

if (gpu)
{
MyShaderCallBack* mc = new MyShaderCallBack();

// create the shaders depending on if the user wanted high level
// or low level shaders:

if (UseHighLevelShaders)
{
// create material from high level shaders (hlsl or glsl)

newMaterialType1 = gpu->addHighLevelShaderMaterialFromFiles(
vsFileName, "vertexMain", video::EVST_VS_1_1,
psFileName, "pixelMain", video::EPST_PS_1_1,
mc, video::EMT_SOLID);

newMaterialType2 = gpu->addHighLevelShaderMaterialFromFiles(
vsFileName, "vertexMain", video::EVST_VS_1_1,
psFileName, "pixelMain", video::EPST_PS_1_1,
mc, video::EMT_TRANSPARENT_ADD_COLOR);
}
else
{
// create material from low level shaders (asm or arb_asm)

newMaterialType1 = gpu->addShaderMaterialFromFiles(vsFileName,
psFileName, mc, video::EMT_SOLID);

newMaterialType2 = gpu->addShaderMaterialFromFiles(vsFileName,
psFileName, mc, video::EMT_TRANSPARENT_ADD_COLOR);
}

mc->drop();
}


각 재질들을 테스팅할 시간이다. 테스트용 직육면체를 만들고 재질을 정하자. 또한 텍스트 씬 노드와 회전 애니메이터를 추가해 좀 더 그럴싸하게 보이게 하자.


//재질 타입 1로 만들어진 첫번째 테스트 씬 노드

scene::ISceneNode* node = smgr->addTestSceneNode(50);
node->setPosition(core::vector3df(0,0,0));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType1);

smgr->addTextSceneNode(gui->getBuiltInFont(),
L"PS & VS & EMT_SOLID",
video::SColor(255,255,255,255), node);

scene::ISceneNodeAnimator* anim = smgr->createRotationAnimator(
core::vector3df(0,0.3f,0));
node->addAnimator(anim);
anim->drop();

//재질 타입 2로 만들어진 두번째 테스트 씬 노드

node = smgr->addTestSceneNode(50);
node->setPosition(core::vector3df(0,-10,50));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType2);

smgr->addTextSceneNode(gui->getBuiltInFont(),
L"PS & VS & EMT_TRANSPARENT",
video::SColor(255,255,255,255), node);

anim = smgr->createRotationAnimator(core::vector3df(0,0.3f,0));
node->addAnimator(anim);
anim->drop();

//쉐이더가 적용되지 않은 비교 용 테스트 씬 노드

node = smgr->addTestSceneNode(50);
node->setPosition(core::vector3df(0,50,25));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
smgr->addTextSceneNode(gui->getBuiltInFont(), L"NO SHADER",
video::SColor(255,255,255,255), node);


마지막으로 배경을 위한 스카이 박스(skybox)를 더하고, 사용자용 카메라를 붙이자. 스카이 박스 텍스쳐에는 밉맵(mipmap)을 적용하지 않는다. 왜냐하면 여기선 필요없기 때문이다. (역자 주 : 게임 개발에서 거리와 크기에 따라 확대 축소하는 텍스쳐 깨짐 현상을 막기 위해, 여러 단계의 텍스쳐를 합성해 만든 후 보간하는 방법을 사용한다. 이때 미리 만들어진 축소본을 밉맵mipmap이라 한다.)


// add a nice skybox

// 밉맵을 안 쓰므로 잠시 끈다.

driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);

smgr->addSkyBoxSceneNode(
driver->getTexture("../../media/irrlicht2_up.jpg"),
driver->getTexture("../../media/irrlicht2_dn.jpg"),
driver->getTexture("../../media/irrlicht2_lf.jpg"),
driver->getTexture("../../media/irrlicht2_rt.jpg"),
driver->getTexture("../../media/irrlicht2_ft.jpg"),
driver->getTexture("../../media/irrlicht2_bk.jpg"));

//밉맵이 다른데서 쓰일 수도 있으므로 다시 켬.

driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, true);

// add a camera and disable the mouse cursor

scene::ICameraSceneNode* cam =

smgr->addCameraSceneNodeFPS(0, 100.0f, 100.0f);
cam->setPosition(core::vector3df(-100,50,100));
cam->setTarget(core::vector3df(0,0,0));
device->getCursorControl()->setVisible(false);


모든 것을 그리자!


int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, video::SColor(255,0,0,0));
smgr->drawAll();
driver->endScene();

int fps = driver->getFPS();

if (lastFPS != fps)
{
core::stringw str =

L"Irrlicht Engine - Vertex and pixel shader example [";
str += driver->getName();
str += "] FPS:";
str += fps;

device->setWindowCaption(str.c_str());
lastFPS = fps;
}
}

device->drop();

return 0;
}


-------------------------------------------------------------------------


3D 프로그래밍에서 쉐이더는 어려운 분야 중 하나다. 사실 나도 잘 모른다. 허나 어떤 이는 '앞으로는 두가지 3D 프로그래머가 존재할 것이다. HLSL을 아는 프로그래머와 그렇지 않은 프로그래머가...'라는 말을 한다. 쩝, 게임은 잘 만드는 것만큼 재미있게 만드는 것도 중요하다고 나름껏 항변하면서도, 가슴이 아픈 것은 어쩔 수 없다.


이번 예제는 매우 오랜 기간이 걸렸다. 이는 용어에서부터 내가 모르는 것이 많았기 때문이다. 일단 예제에서 나오는 몇가지 개념을 내가 찾아본 대로 적기로 한다.


1. Shadow : 쉐도우, 그림자 : 말 그대로 그림자. 빛이 대상을 쏘면 뒤에 남는 검은 것. 현실성을 위해 중요하나 그만큼 부하가 큰 부분.


2. Shading : 쉐이딩, 음영 : 3D 공간상 물체의 표면이 빛에 어떻게 반응할 것인지 분야. 플랫 쉐이딩, 고로 쉐이딩, 퐁 쉐이딩 등 표면과 반사를 3D 장치가 어떻게 처리할 것인지 결정.


3. Shader : 쉐이더 : 보통 '쉐이더 프로그래밍'을 의미. 각 그래픽 카드가 어떻게 버텍스나 픽셀을 만드는지 정하는게 GPU라면, 프로그래머가 직접 이 부분을 제어하게 해주는 것을 말함. 프로그래머로써 3D 최소 단위인 정점을 자신의 뜻에 맞게 다룬다는게 축복이자 저주이다.


여기서 버텍스나 픽셀은 모두 '점'을 의미하나, 버텍스는 3D의 점 - 어찌보면 가상의, 프로세스 내부의 점이고, 그 3D 점이 결국 모니터라는 2D화면에 나타나게 되는데 이때를 픽셀이라 한다. 그래픽 카드마다 이들을 어떻게 처리하는지 조금씩 다르다.


음......좀 더 정확한 내용은 다른 자료를 보기 바란다.


이번 예제는 그 난이도로 인해 적절한 한글화를 하지 못했다. 어떤 경우는 이전 예제에서 한글화 했던 것을 무시한 바도 있다. 아쉽지만 이해와 속도를 위함이었다.


9. Mesh Viewer

Posted 2007/09/15 19:25 by 수달

-------------------------------------------------------------------------

튜토리얼 9 : 메시 뷰어

--------------------

이번 예제는 엔진을 이용해 좀 더 복잡한 어플래케이션을 만드는 법을 보인다. 여기서는 간단한 메시 뷰어(역자 주 : 캐릭터나 사물등 그래픽 디자이너가 만든 3D 모델을 불러서 화면에 보이는 툴. 이 툴을 이용해 전용 포맷을 뽑아내기도 한다. 최악의 경우 바이너리로 WriteFile() 할 수도 있다.)를 만들 것이다. 메시 뷰어는 엔진의 UI API와 씬 메니저를 사용해 만든다. (역자 주 : API는 Application Programing Interface의 약자로 개발자들이 어플리케이션 프로그래밍을 하는데 제공되는 함수나 라이브러리등을 의미한다.)

예제는 버튼, 윈도, 툴바, 메뉴, 콤보박스, 탭컨트롤, 에딧박스, 이미지, 메시지 박스, 스카이 박스를 사용하며, 또한 엔진의 XML 리더기(reader)로 XML 파싱하는 법을 보일 것이다. (역자 주 : XML 파싱이란, 그래픽 툴이 제공하는 포맷 양식이 아닌 XML 양식으로 추출(export)된 3D 관련 파일을 메시 뷰를 통해 읽어 재해석함을 의미한다. 다른 예로 ASE 추출/파싱, X파일 추출/파싱 등이 있을 것이다.)

아래 모습의 프로그램이 만들어 질 것이다.



-------------------------------------------------------------------------

시작

----
시작은 이전 예제와 거의 같다. 필요한 헤더, 라이브러리를 읽고, 각종 전역 변수를 선언한다. 다만 여기서는 필요한 두 개의 'using namespace'만 사용한다. (불필요하게 전체 클래스의 name을 부를 필요는 없다.)


#include <irrlicht.h>

#include <iostream>

using namespace irr;

using namespace gui;

#pragma comment(lib, "Irrlicht.lib")

IrrlichtDevice *Device = 0;

core::stringc StartUpModelFile;

core::stringw MessageText;

core::stringw Caption;

scene::IAnimatedMeshSceneNode* Model = 0;

scene::ISceneNode* SkyBox = 0;


다음 세개의 함수는 메시 뷰어에서 잡다한 일을 처리하는 함수다. 첫번째 함수인 showAboutText()는 메세지 글과 메세지 박스 캡션을 쉽게 다루게 한다. 메세지 글은 stringw 변수인 MessageText에 저장될 것이고 캡션 역시 Caption 변수에 저장 될 것이다.


void showAboutText()
{
// create modal message box with the text
// loaded from the xml file.
Device->getGUIEnvironment()->addMessageBox(
Caption.c_str(), MessageText.c_str());
}


두번째 함수인 loadModel()은 addAnimatedMeshSceneNode와 씬 메니저를 이용해, 모델을 읽고 화면에 표시하게 해 줄 것이다. 어려운 것은 없다. 또한 이 함수는 모델 읽기 실패 시 짧은 메세지 박스를 보일 것이다.


void loadModel(const c8* fn)
{
// modify the name if it a .pk3 file

c8 filename[1024];
strcpy(filename, fn);
c8* found = 0;

if (found = strstr(filename, ".pk3"))
{
Device->getFileSystem()->addZipFileArchive(filename);
strcpy(found +1, "bsp");
}

// load a model into the engine

if (Model)
Model->remove();

Model = 0;

scene::IAnimatedMesh* m =

Device->getSceneManager()->getMesh(filename);

if (!m)
{
// model could not be loaded

if (StartUpModelFile != filename)
Device->getGUIEnvironment()->addMessageBox(
Caption.c_str(), L"The model could not be loaded. " \
L"Maybe it is not a supported file format.");
return;
}

// set default material properties

Model = Device->getSceneManager()->addAnimatedMeshSceneNode(m);
if (!found)
Model->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
Model->setMaterialFlag(video::EMF_LIGHTING, false);
Model->setDebugDataVisible(true);
Model->setAnimationSpeed(1000);
}


세번째 함수는 툴박스 윈도를 만든다. 이번 예제의 간단한 메시 뷰어는 복잡한 기능을 넣지 않는다. 다만 화면에 출력되는 모델의 크기를 변화시키기 위한 세개의 에딧박스를 가진 탭 컨트롤만 있으면 된다.


void createToolBox()
{
// remove tool box if already there
IGUIEnvironment* env = Device->getGUIEnvironment();
IGUIElement* root = env->getRootGUIElement();
IGUIElement* e = root->getElementFromId(5000, true);
if (e) e->remove();

// create the toolbox window
IGUIWindow* wnd = env->addWindow(core::rect<s32>(450,25,640,480),
false, L"Toolset", 0, 5000);

// create tab control and tabs
IGUITabControl* tab = env->addTabControl(
core::rect<s32>(2,20,640-452,480-7), wnd, true, true);

IGUITab* t1 = tab->addTab(L"Scale");
IGUITab* t2 = tab->addTab(L"Empty Tab");

// add some edit boxes and a button to tab one
env->addEditBox(L"1.0", core::rect<s32>(40,50,130,70), true, t1, 901);
env->addEditBox(L"1.0", core::rect<s32>(40,80,130,100), true, t1, 902);
env->addEditBox(L"1.0", core::rect<s32>(40,110,130,130), true, t1, 903);

env->addButton(core::rect<s32>(10,150,100,190), t1, 1101, L"set");

// add senseless checkbox
env->addCheckBox(true, core::rect<s32>(10,220,200,240), t1, -1,

L"Senseless Checkbox");

// add und0cumentated transparent control
env->addStaticText(L"Transparent Control:", core::rect<s32>(10,240,150,260),

true, false, t1);
IGUIScrollBar* scrollbar =

env->addScrollBar(true, core::rect<s32>(10,260,150,275), t1, 104);
scrollbar->setMax(255);

// bring irrlicht engine logo to front, because it
// now may be below the newly created toolbox
root->bringToFront(root->getElementFromId(666, true));
}


GUI 개체들에서 보내오는 여러 이벤트들을 받기 이해 이벤트 리시버(event receiver)를 만들어야 한다. 만약 이벤트가 발생하면 리시버는 해당 ID를 확인하고 그에 맞는 행동을 시작한다. (역자 주 : ID는 이후 main()에서 각 GUI가 등록 되면서 결정된다.)


class MyEventReceiver : public IEventReceiver
{
public:
virtual bool OnEvent(SEvent event)
{
if (event.EventType == EET_GUI_EVENT)
{
s32 id = event.GUIEvent.Caller->getID();
IGUIEnvironment* env = Device->getGUIEnvironment();

switch(event.GUIEvent.EventType)
{
case EGET_MENU_ITEM_SELECTED:
{
// a menu item was clicked

IGUIContextMenu* menu =

(IGUIContextMenu*)event.GUIEvent.Caller;
s32 id = menu->getItemCommandId(menu->getSelectedItem());

switch(id)
{
case 100: // File -> Open Model
env->

addFileOpenDialog(L"Please select a model file to open");
break;
case 200: // File -> Quit
Device->closeDevice();
break;
case 300: // View -> Skybox
SkyBox->setVisible(!SkyBox->isVisible());
break;
case 400: // View -> Debug Information
if (Model)
Model->

setDebugDataVisible(!Model->isDebugDataVisible());
break;
case 500: // Help->About
showAboutText();
break;
case 610: // View -> Material -> Solid
if (Model)
Model->setMaterialType(video::EMT_SOLID);
break;
case 620: // View -> Material -> Transparent
if (Model)
Model->
setMaterialType

(video::EMT_TRANSPARENT_ADD_COLOR);
break;
case 630: // View -> Material -> Reflection
if (Model)
Model->setMaterialType(video::EMT_SPHERE_MAP);
break;
}
break;
}

case EGET_FILE_SELECTED:
{
// load the model file, selected in the file open dialog
IGUIFileOpenDialog* dialog =
(IGUIFileOpenDialog*)event.GUIEvent.Caller;
loadModel(core::stringc(dialog->getFilename()).c_str());
}

case EGET_SCROLL_BAR_CHANGED:

// control skin transparency
if (id == 104)
{
s32 pos = ((IGUIScrollBar*)event.GUIEvent.Caller)->getPos();
for (s32 i=0; i<irr::gui::EGDC_COUNT ; ++i)
{
video::SColor col =

env->getSkin()->getColor((EGUI_DEFAULT_COLOR)i);
col.setAlpha(pos);
env->getSkin()->setColor((EGUI_DEFAULT_COLOR)i, col);
}
}
break;

case EGET_BUTTON_CLICKED:

switch(id)
{
case 1101:
{
// set scale
gui::IGUIElement* root = env->getRootGUIElement();
core::vector3df scale;
core::stringc s;

s = root->getElementFromId(901, true)->getText();
scale.X = (f32)atof(s.c_str());
s = root->getElementFromId(902, true)->getText();
scale.Y = (f32)atof(s.c_str());
s = root->getElementFromId(903, true)->getText();
scale.Z = (f32)atof(s.c_str());

if (Model)
Model->setScale(scale);
}
break;
case 1102:
env->addFileOpenDialog(L"Please select a model file to open");
break;
case 1103:
showAboutText();
break;
case 1104:
createToolBox();
break;
}

break;
}
}

return false;
}
};


어려운 일은 끝났다. 이제 IRRLICHT 엔진 장치를 만들고, 버튼, 메뉴, 툴바를 붙이자. createDevice()를 이용해 일반적으로 엔진을 시작하면 된다. 그런 후 어플리케이션이 이벤트를 붙잡게 하기 위해, 이벤트리시버 인자를 정해준다. (역자 주 : 이 다음으로 #ifdef WIN32 전처리문을 이용한다는 구문이 나오는데, 과거 예제에 있었던 부분 같다. 현재 예제들은 사용자가 직접 플랫폼과 그래픽 디바이스를 선택하게 한다.)

보다시피 IrrlichtDevice::setResizeAble()이라는 특별한 함수가 호출된다. 이 함수는 메시 뷰어에 아주 유용한 능력을 주는데, 바로 윈도 크기를 자유롭게 바꿀 수 있게 한다.


int main()
{
// ask user for driver

video::E_DRIVER_TYPE driverType = video::EDT_DIRECTX8;

printf("Please select the driver you want for the mesh viewer:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.2\n"\
" (d) Software Renderer\n (e) NullDevice\n (otherKey) exit\n\n");

char key;
std::cin >> key;

switch(key)
{
case 'a': driverType = video::EDT_DIRECTX9; break;
case 'b': driverType = video::EDT_DIRECTX8; break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_NULL; break;
default: return 1;
}

// create device and exit if creation failed

MyEventReceiver receiver;
Device = createDevice(driverType, core::dimension2d<s32>(640, 480),
16, false, false, false, &receiver);

if (Device == 0)
return 1; // could not create selected driver.

Device->setResizeAble(true);

Device->setWindowCaption(L"Irrlicht Engine - Loading...");

video::IVideoDriver* driver = Device->getVideoDriver();
IGUIEnvironment* env = Device->getGUIEnvironment();
scene::ISceneManager* smgr = Device->getSceneManager();

driver->setTextureCreationFlag(video::ETCF_ALWAYS_32_BIT, true);


다음으로 설정 파일(config file) 을 읽자. 이는 다음과 같은 형식으로 xml에 저장되어 있다.


<?xml version="1.0"?>

<config>

<startUpModel file="some filename"/>

<messageText caption="Irrlicht Engine Mesh Viewer">
Hello!
</messageText>
</config>


이 예제에 사용하는 StartUpModelFile과 메시지 글, 캡션은 xml 파일에 위의 형식으로 저장되어 있다. 그것을 IRRLICHT 엔진의 XML 파셔를 이용해 푼다.


//config 파일을 읽는다.

io::IXMLReader* xml = Device->getFileSystem()->createXMLReader(
"../../media/config.xml");

while(xml && xml->read())
{
switch(xml->getNodeType())
{
case io::EXN_TEXT:
//xml 파일에서 텍스트인 경우는 메시지 글인 경우다.
MessageText = xml->getNodeData();
break;
case io::EXN_ELEMENT:
{
if (core::stringw("startUpModel") == xml->getNodeName())
StartUpModelFile = xml->getAttributeValue(L"file");
else
if (core::stringw("messageText") == xml->getNodeName())
Caption = xml->getAttributeValue(L"caption");
}
break;
}
}

if (xml)
xml->drop(); //xml을 읽은 후, 다 사용했으면 지워줘야 한다!


별로 어렵지 않았을 것이다.이제 멋진 폰트를 설정하고 메뉴를 만들어 보겠다. 모든 메뉴 아이템은 하위 메뉴를 만들 수 있다. menu->addItem(L"File", -1, true, true)를 호출하는 것은 File이란 이름의 ID -1인 새 메뉴를 더한다는 의미다. 다음 인자 true는 사용 가능/불가능을, 다음 true는 하위 메뉴 가능/불가능을 결정한다. 각 하위 메뉴는 menu->getSubMenu(0)의 형식으로 접근 할 수 있는데, 이때 최상위인 'File'은 메뉴 시작점(Entry) 역할을 한다.


// set a nicer font

IGUISkin* skin = env->getSkin();
IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp");
if (font)
skin->setFont(font);

// create menu

gui::IGUIContextMenu* menu = env->addMenu();
menu->addItem(L"File", -1, true, true);
menu->addItem(L"View", -1, true, true);
menu->addItem(L"Help", -1, true, true);

gui::IGUIContextMenu* submenu;
submenu = menu->getSubMenu(0);
submenu->addItem(L"Open Model File...", 100);
submenu->addSeparator();
submenu->addItem(L"Quit", 200);

submenu = menu->getSubMenu(1);
submenu->addItem(L"toggle sky box visibility", 300);
submenu->addItem(L"toggle model debug information", 400);
submenu->addItem(L"model material", -1, true, true );

submenu = submenu->getSubMenu(2);
submenu->addItem(L"Solid", 610);
submenu->addItem(L"Transparent", 620);
submenu->addItem(L"Reflection", 630);

submenu = menu->getSubMenu(2);
submenu->addItem(L"About", 500);


메뉴바 아래의 툴바에는 색깔있는 버튼과 의미는 없지만 예제를 위해 콤보 박스를 붙여본다.


// create toolbar

gui::IGUIToolBar* bar = env->addToolBar();
bar->addButton(1102, 0, driver->getTexture("../../media/open.bmp"));
bar->addButton(1103, 0, driver->getTexture("../../media/help.bmp"));
bar->addButton(1104, 0, driver->getTexture("../../media/tools.bmp"));

// create a combobox with some senseless texts

gui::IGUIComboBox* box =

env->addComboBox(core::rect<s32>(100,5,200,25), bar);
box->addItem(L"Bilinear");
box->addItem(L"Trilinear");
box->addItem(L"Anisotropic");
box->addItem(L"Isotropic");
box->addItem(L"Psychedelic");
box->addItem(L"No filtering");


작접 툴들을 그럴 듯하게 하기 위해 GUI 요소들이 투명화 되는 것을 막고, IRRLICHT 엔진 로그를 새기겠다. (역자 주 : 이전 예제는 그러했으나 현재 예제는 스크롤 바를 이용해 투명화가 가능하게 했다.) 그리고 현재 초당 프레임을 보여주는 글을 만들고, 윈도 캡션을 바꾼다.


// disable alpha

for (s32 i=0; i<gui::EGDC_COUNT ; ++i)
{
video::SColor col =

env->getSkin()->getColor((gui::EGUI_DEFAULT_COLOR)i);
col.setAlpha(255);
env->getSkin()->setColor((gui::EGUI_DEFAULT_COLOR)i, col);
}

// add a tabcontrol

createToolBox();

// create fps text

IGUIStaticText* fpstext =

env->addStaticText(L"", core::rect<s32>(210,26,270,41), true);

// set window caption

Caption += " - [";
Caption += driver->getName();
Caption += "]";
Device->setWindowCaption(Caption.c_str());


거의 다 끝나간다. 시작할 때 도움말 베시지 박스를 뜨게 하고, 첫 모델을 읽자. 좀 더 그럴 듯하게 하기 위해, 배경에 하늘(skybox)을 만들고 사용자가 다룰 수 있는 카메라를 넣자. (어플리케이션 사용자의 편의가 늘 것이다.) 그리고 끝으로 지금까지 준비한 것을 그린다.


// show about message box and load default model
showAboutText();
loadModel(StartUpModelFile.c_str());

// add skybox

SkyBox = smgr->addSkyBoxSceneNode(
driver->getTexture("../../media/irrlicht2_up.jpg"),
driver->getTexture("../../media/irrlicht2_dn.jpg"),
driver->getTexture("../../media/irrlicht2_lf.jpg"),
driver->getTexture("../../media/irrlicht2_rt.jpg"),
driver->getTexture("../../media/irrlicht2_ft.jpg"),
driver->getTexture("../../media/irrlicht2_bk.jpg"));

// add a camera scene node

smgr->addCameraSceneNodeMaya();

// load the irrlicht engine logo

video::ITexture* irrLogo =
driver->getTexture("../../media/irrlichtlogoaligned.jpg");

// draw everything

while(Device->run() && driver)
if (Device->isWindowActive())
{
driver->beginScene(true, true, video::SColor(150,50,50,50));

smgr->drawAll();
env->drawAll();

// draw irrlicht engine logo
driver->draw2DImage(irrLogo,
core::position2d<s32>(10, driver->getScreenSize().Height - 50),
core::rect<s32>(0,0,108-20,460-429));

driver->endScene();

core::stringw str = L"FPS: ";
str += driver->getFPS();
fpstext->setText(str.c_str());
}

Device->drop();
return 0;
}


--------------------------------------------------------------------------

이번 예제는 이전 IRRLICHT 버전의 설명이 섞여 있는 듯한 인상을 받았다. 하지만 그런 것은 그렇게 중요한게 아니다. 메시 뷰어는 모든 3D 게임의 시작이라 할 수 있다. 이 툴을 통해 전용 포맷을 만들고, 이후 좀 더 심도 있는 게임 세계를 창조할 수 있다.


그렇기에 사실 이번 예제는 IRRLICHT의 예제라기 보다는, 3D 프로그래밍 개발에 있어서 좋은 본보기를 제시한다. 기본 기술을 익힌 프로그래머가 실제 게임을 만들기 위해 준비하는 툴 개발 기술은 참으로 중요한 것이다. (다른 엔진의 경우는 그것을 직접 제공하는 경우가 많다. IRRLICHT의 경우는 예제를 통해 그런 것을 대신하는 듯하다. IRRLICHT 관련 포럼을 참조.)

그리고 앞으로 나올 쉐이더, 픽셀라이트닝, 터레인 관련 예제들을 보면, 이번 예제의 위치가 어떤 것인지 알 수 있다.


이번 예제에서 애매한 것이, /**/ 설명글 이외에 각 문단별 // 주석이 많다는 것이다. 쉬운 영어이기에 대부분 그냥 넘어갔다.


8. SpecialFX

Posted 2007/09/15 19:24 by 수달

--------------------------------------------------------------------------

튜토리얼 8 : 특수 효과

--------------------

이번 튜토리얼은 특수 효과에 대해 보인다. 스텐실버퍼 쉐도우 사용법, 파티클 시스템, 빌보드, 다이나믹 라이트 그리고 물 표면 씬 노드를 이야기 한다.

이전의 예제들과 마찬가지 시작을 할 것이나, createDevice()에 'shadows' 플래그를 true로 놔야한다. 이렇게 해야 애니메이션 하는 캐릭터에 다이나믹 쉐도우가 먹을 것이다. 만약 예제 실행이 느리다면, 이 플래그를 false로 해야한다. IRRLICHT 엔진은 해당 하드웨어가 스텐실버퍼를 지원하는지 안 하는지 확인해 쉐도우를 끌 것이다. 그러나 예제에서 원한다면 느리더라도 돌려 볼 수는 있다. (역자 주 : 다이나믹 라이트나 쉐도우는 광원의 위치에 따른 빛과 그림자 변화를 적용할지 안 할지를 결정하겠다는 말이다. '실시간 적용'이란 의미가 강하다.)


#include <irrlicht.h>
#include <iostream>

using namespace irr;

#pragma comment(lib, "Irrlicht.lib")

int main()
{
// ask if user would like shadows

char i;
printf("Please press 'y' if you want to use realtime shadows.\n");

std::cin >> i;
bool shadows = (i == 'y');

// ask user for driver

video::E_DRIVER_TYPE driverType;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.2\n"\
" (d) Software Renderer\n (e) NullDevice\n (otherKey) exit\n\n");

std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECTX9; break;
case 'b': driverType = video::EDT_DIRECTX8; break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_NULL; break;
default: return 1;
}

// create device and exit if creation failed

IrrlichtDevice *device =
createDevice(driverType, core::dimension2d<s32>(640, 480),
16, false, shadows);

if (device == 0)
return 1; // could not create selected driver.

video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();


예제는 *.3ds 파일을 읽었다. 그것은 Anim8Or(역자 주 : 3D 애니메이션 모델링 프로그램, 써 보진 않음. 확인 할 예정.)로 만든 작은 방으로 *.3ds로 익스포트 한 것이다. (이 예제를 쓸 당시 아직 IRRLICHT 엔진은 *.an8을 지원하지 않았음) 전문 아티스트가 아니기에 텍스처 맵핑이나 모델링이 엉망일 것이다. 허나 프로그램으로 이것을 보기 좋게 극복했다. 만약 최초의 모델을 보고 싶다면 아래 라인을 주석처리해 보아라. 최초의 모델과 프로그램 기법으로 처리한 것에 차이를 느낄 것이다. 또한 Anim8Or에서 재질에 설정한 방출광(역자 주 : emissive : 조명과 관계없이 Ambient, Diffuse, Specular에 영향을 미치는 광원 요소)을 끌 것이다. 이는 예제를 쓴 저자가 싫어하기 때문이다.


scene::IAnimatedMesh* mesh =

smgr->getMesh("../../media/room.3ds");

smgr->getMeshManipulator()->makePlanarTextureMapping(
mesh->getMesh(0), 0.004f);

scene::ISceneNode* node = 0;

node = smgr->addAnimatedMeshSceneNode(mesh);
node->setMaterialTexture(0, driver->getTexture("../../media/wall.jpg"));
node->getMaterial(0).EmissiveColor.set(0,0,0,0);

첫번 째 특수 효과인 움직이는 물 효과를 만들어 보자. WaterSurfaceSceneNode는 매시를 받아 들인 후 그 표면을 물결치듯 만드는 씬 노드다. 만약 이 씬 노드를 EMT_REFLECTION_2_LAYER 같은 멋진 재질에 적용하면 그럴 듯한 모양이 나온다. 이것을 단지 몇 줄의 코딩으로 해낼 수 있다. 물 표면 씬 노드에 입력하는 메시로 여기서는 hill plane mesh를 사용하는데, 어떤 것이든 상관 없다. 원한다면 여기서 사용되는 room.3ds를 써도 된다. (단 매우 이상하게 보일 것이다.)


// add animated water

mesh = smgr->addHillPlaneMesh("myHill",
core::dimension2d<f32>(20,20),
core::dimension2d<s32>(40,40), 0, 0,
core::dimension2d<f32>(0,0),
core::dimension2d<f32>(10,10));

node =

smgr->addWaterSurfaceSceneNode(mesh->getMesh(0),

3.0f, 300.0f, 30.0f);
node->setPosition(core::vector3df(0,7,0));

node->setMaterialTexture(0, driver->getTexture("../../media/water.jpg"));
node->setMaterialTexture(1, driver->getTexture("../../media/stones.jpg"));

node->setMaterialType(video::EMT_REFLECTION_2_LAYER);


두번째 효과는 단순한 것으로 이미 다른 IRRLICHT 엔진 데모에서 보인 바 있다. 그것은 다이나믹 라이트와 연결된 움직이는 빌보드 이다. 쉽게 광원 씬 노드를 만든 후, 주변을 둥글게 돌게 하자. (fly around) 그 씬 노드를 빌보드 씬 노드에 붙이면 된다.


// create light

node = smgr->addLightSceneNode(0, core::vector3df(0,0,0),
video::SColorf(1.0f, 0.6f, 0.7f, 1.0f), 600.0f);
scene::ISceneNodeAnimator* anim = 0;
anim = smgr->createFlyCircleAnimator (core::vector3df(0,150,0),250.0f);
node->addAnimator(anim);
anim->drop();

// attach billboard to light

node =

smgr->addBillboardSceneNode(node, core::dimension2d<f32>(50, 50));
node->setMaterialFlag(video::EMF_LIGHTING, false);
node->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
node->setMaterialTexture(0,

driver->getTexture("../../media/particlewhite.bmp"));


다음의 특수 효과는 매우 흥미로운 것으로 파티클 시스템이다. IRRLICHT에서 파티클 시스템은 모듈화 되어 있고 확장성 있지만 아직 사용하기엔 쉽지 않다. 엔진에는 파티클 씨앗(particle emitter : 역자 주 : 파티클 생성을 위한 기본 설정으로 '파티클 발현자', '파티클 생성자'라고 할 수 있을 것 같으나, 추상적으로 '파티클 씨앗'이란 단어를 썼다.)을 담아 무(無)에서 파티클을 만들어내는 파티클 시스템 씬 노드(scene::IParticleSystemSceneNode)가 있다. 이 씨앗은 매우 융통성 있고 만들고자 하는 파티클의 색상, 크기, 방향 등 여러 인자를 갖고 있다. 여러 종의 파티클 씨앗 있는데, 예를 들어 고정된 한 지점에서 여러 파티클이 솟아나는 것도 있다. 엔진에서 제공하는 파티클 씨앗이 충분하지 않다면, 개발자는 IParticleEmitter로 부터 상속된 자신만의 클래스를 쉽게 만들어 낼 수 있을 것이다. 이 클래스는 단지 setEmitter()를 통해 엔진에서 사용할 수 있다.


이번 예제에선 박스형 파티클 씨앗(box particle emitter)를 이용할 것으로, 박스 안에서 무작위의 파티클이 생성되는 형태이다. 각 인자들은 박스를 정의하고, 파티클의 방향, 초당 생성되는 새 파티클의 최대 최소, 색깔, 그리고 각 파티클들의 최대 최소 생명력을 정할 것이다.

단순히 씨앗들만 사용해 파티클을 만드는 것은 조금 지루할 수 있기 때문에, 그것을 보완하는 파티클 변환자(particle affector)를 이용해 파티클을 공중에 날리게 하자. 변환자는 파티클 시스템에 추가될 수 있고, 중력이나 바람과 같은 추가 효과를 비슷하게 낼 수 있다. 예제에서 사용할 파티클 변환자는 파티클의 색상을 바꾸는 변환자이다. 그것은 파티클을 점차 히미하게 사라지게 한다.(fade out) 파티클 씨앗과 마찬가지로 파티클 변환자는 개발자에 의해 더 좋게 만들어 질 수 있다. 즉, IParticleAffector로부터 상속한 class를 addAffector()에 넣어주기만 하면 된다.

지금까지 만든 파티클 시스템에 적당한 재질을 설정하면 그럴싸한 캠프 파이어를 구경 할 수 있을 것이다. 또한 재질, 텍스처, 파티클 씨앗, 파티클 변환자 등 인자를 변환해 보면, 쉽게 연막, 비, 폭발, 눈(雪) 등을 만들 수 있다.


// create a particle system

scene::IParticleSystemSceneNode* ps = 0;
ps = smgr->addParticleSystemSceneNode(false);
ps->setPosition(core::vector3df(-70,60,40));
ps->setScale(core::vector3df(2,2,2));

ps->setParticleSize(core::dimension2d<f32>(20.0f, 20.0f));

scene::IParticleEmitter* em = ps->createBoxEmitter(
core::aabbox3d<f32>(-7,0,-7,7,1,7),
core::vector3df(0.0f,0.06f,0.0f),
80,100,
video::SColor(0,255,255,255), video::SColor(0,255,255,255),
800,2000);

ps->setEmitter(em);
em->drop();

scene::IParticleAffector* paf =
ps->createFadeOutParticleAffector();

ps->addAffector(paf);
paf->drop();

ps->setMaterialFlag(video::EMF_LIGHTING, false);
ps->setMaterialTexture(0, driver->getTexture("../../media/fire.bmp"));
ps->setMaterialType(video::EMT_TRANSPARENT_VERTEX_ALPHA);


마지막으로 살펴볼 특수 효과는 애니메이션 캐릭터에 적용되는 동적 그림자(dynamic shadow)이다. 이를 위해 퀘이크 2의 *.md2 모델을 읽고 월드 안에 넣겠다. 그림자를 만드는 것은 addShadowVolumeSceneNode()를 호출하여 쉽게 할 수 있다. (역자 주 : 그림자를 구현하는 방법은 여러가지가 있는데, 그중 예제에서는 Shadow Volume 기법을 사용한 것이다.) 그림자의 색깔은 ISceneManager::setShadowColor()에 의해 전역적으로 결정되어 있다. (역자 주 : 그림자 색깔은 하나로 정해져 모두 같다.) 자, 보라. 동적 그림자가 (이렇게 쉽게) 만들어 졌다.

사실 예제의 캐릭터는 전체 씬에 대비해 너무 작다. setScale()을 이용해 좀 더 크게 만들자. 그리고 캐릭터는 동적 광원에 의해 빛을 받으므로, 광원의 방향으로 법선들을 표준화(normalize)해야 한다. 이는 동적 광원을 받는 모델의 크기가 (1, 1, 1)이 아닌 경우 반듯시 필요하다. 그렇지 않으면 법선들이 지나치게 커진 바람에, 너무 밝거나 너무 어두울 것이다. (역자 주 : 법선은 각 폴리곤 면에서 앞면쪽의 벡터 방향을 의미한다. 법선은 빛과 재질을 계산하는데 들어가기에 모델 크기의 변환만큼 법선에 영향을 주는 광원과의 관계도 변화를 줘야 한다.)


// add animated character

mesh = smgr->getMesh("../../media/faerie.md2");
scene::IAnimatedMeshSceneNode* anode = 0;

anode = smgr->addAnimatedMeshSceneNode(mesh);
anode->setPosition(core::vector3df(-50,65,-60));
anode->setMD2Animation(scene::EMAT_STAND);
anode->setMaterialTexture(0,

driver->getTexture("../../media/Faerie5.BMP"));

// add shadow
anode->addShadowVolumeSceneNode();
smgr->setShadowColor(video::SColor(150,0,0,0));

// make the model a little bit bigger and normalize its normals
// because of this for correct lighting
anode->setScale(core::vector3df(2,2,2));
anode->setMaterialFlag(video::EMF_NORMALIZE_NORMALS, true);


이제 준비한 것을 그려보자.


scene::ICameraSceneNode* camera = smgr->addCameraSceneNodeFPS();
camera->setPosition(core::vector3df(-50,50,-150));

// disable mouse cursor
device->getCursorControl()->setVisible(false);


int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, 0);

smgr->drawAll();

driver->endScene();

int fps = driver->getFPS();

if (lastFPS != fps)
{
core::stringw str = L"Irrlicht Engine - SpecialFX example [";
str += driver->getName();
str += "] FPS:";
str += fps;

device->setWindowCaption(str.c_str());
lastFPS = fps;
}
}

device->drop();

return 0;
}


----------------------------------------------------------------------------

실제 게임에서는 여기서 언급된 파티클 씨앗을 이용한 특수 효과 툴을 만들어 사용하게 될 것이다.


이번 예제를 해석하면서 여러 용어를 한글화 하는데 이질감을 느꼈다. 실제 normalize는 표준화, 계량화라는 말보다 그냥 '노말라이즈'라고 한다. 또한 emitter등도 아마 그냥 '파티클 이미터'라는 식으로 말할 것 같다. 하지만 억지로 한글화 해 보았다.


그냥 이렇게 해석만 해서는 안 될 것 같다. 사실 아무 의미 없다. 개발자라면 당연히 이 코드들을 이용해 여러 조작, 변경, 프로젝트에 직접 구현 등을 해야할 것이다. 그러나 일단 어떤 기능이 있는지 알아두면 나중에 그것을 찾아보면 될 것이니 일단 계속 하기로 한다. 게다가 예제 작성자의 친절함, 예를 들어 한가지를 설명하더라도 실 개발에 쓰일 여러 방법을 보여준다는데서 감동하며, 그의 재치(?)가 얼마나 되는지 살펴보는 것도 도움이 될 것이다.


7. Collision

Posted 2007/09/15 19:23 by 수달

-------------------------------------------------------------------------

튜토리얼 7 : 충돌

----------------


이번 예제에서는 IRRLICHT엔진의 '충돌 확인' 법을 보일 것이다. 세가지 방법이 있다. 첫번째는 계단과 미끄러짐을 포함한 3D 월드의 이동 관련 충돌 확인이다. 두번째는 삼각 폴리곤 피킹(역자 주 : 마우스 포인트 등을 이용해 눈에 보이는 지점의 해당 폴리곤을 찝어내는 기술)이다. 그리고 세번째로 씬 노드 피킹이다.


-------------------------------------------------------------------------

시작

----

퀘이크 3 레벨 맵을 출력했던 튜토리얼 2를 가져오도록 하자. 이 맵에서 걷고, 목표 폴리곤을 찝어내는 기술을 적용해 보겠다. 추가적으로 씬 노드 피킹을 위해 세개의 움직이는 모델을 배치한다.

다음 코드는 퀘이크 3 레벨을 엔진이 읽고 출력한 부분이다. 이에 대해선 튜토리얼 2에서 이미 말했기에 설명하지 않겠다.


#include <irrlicht.h>
#include <iostream>

using namespace irr;

#pragma comment(lib, "Irrlicht.lib")


int main()
{
// let user select driver type

video::E_DRIVER_TYPE driverType;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.2\n"\
" (d) Software Renderer\n (e) NullDevice\n (otherKey) exit\n\n");


char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECTX9; break;
case 'b': driverType = video::EDT_DIRECTX8; break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_NULL; break;
default: return 0;
}

// create device

IrrlichtDevice *device =
createDevice(driverType, core::dimension2d<s32>(640, 480), 16, false);

if (device == 0)
return 1; // could not create selected driver.

video::IVideoDriver* driver = device->getVideoDriver();
scene::ISceneManager* smgr = device->getSceneManager();


device->getFileSystem()->addZipFileArchive("../../media/map-20kdm2.pk3");


scene::IAnimatedMesh* q3levelmesh = smgr->getMesh("20kdm2.bsp");
scene::ISceneNode* q3node = 0;

if (q3levelmesh)
q3node = smgr->addOctTreeSceneNode(q3levelmesh->getMesh(0));


그럭저럭 퀘이크 3 레벨을 읽었다. 이제 좀 다른 것을 해보겠다. 먼저 삼각 폴리곤 선택자(triangle selector)를 만들겠다. 이것은 씬 노드에서 충돌 확인 등을 위해 삼각 폴리곤을 꺼내는 클래스다. 이러한 폴리곤 선택자는 여러가지가 있을 수 있고, 모두 ISceneManager로 만들어 진다. 이번 예제에서는 OctTreeTriangleSelector를 사용한다. 이것은 거대한 메시 덩어리인 퀘이크 3 레벨을 8분할을 통해 최적화 된 삼각형 추출을 해준다.

삼각형 선택자를 만든 후, 이것을 q3node에 부착시킬 것이다. 이것은 필수적이진 않지만, 이렇게 해두면 사소한 신경을 안 써도 된다. (예를 들어 나중에 필요 없어지더라도 직접 drop 시키는 등의 일을 안해도 된다.)


scene::ITriangleSelector* selector = 0;

if (q3node)
{
q3node->setPosition(core::vector3df(-1370,-130,-1400));

selector =

smgr->createOctTreeTriangleSelector(q3levelmesh->getMesh(0),

q3node, 128);
q3node->setTriangleSelector(selector);
selector->drop();
}


튜토리얼 2에서 처럼 1인칭 슈팅 게임 카메라를 붙여 퀘이크 맵을 돌아다닐 수 있게 하자. 추가적으로 이 카메라에 특별한 애니메이터 관련 설정을 할텐데, 충돌에 반응하는 애니메이터(the collision response animator)가 그것이다. 이렇게 설정하면 씬 노드에 영향을 주어 벽을 뚫지 못하게 되고 중력의 영향도 받게 된다. 설정하는데 있어서 애니메이터에게 말해줄 것은 씬 노드의 크기, 월드의 모양, 중력 적용 등 뿐이다. 충돌이 적용된 애니메이터를 카메라에 붙이게 되면, 더 이상 충돌 확인 같은 것을 코드에 넣을 필요가 없다. 모든 것은 자동으로 이뤄지며 모든 충돌 확인 관련 코드들에 피킹을 적용할 수 있게 된다. 이 애니메이터는 카메라 뿐만이 아니고 다른 모든 씬 노드에도 적용이된다. 이 방식으로 IRRLICHT 엔진은 정말 쉽게 충돌 문제를 해결한다. (역자 주 : FPS에서 카메라는 게이머의 플레이어를 말한다. 만약 몬스터에 이를 붙이면, 이 몬스터 역시 쉽게 충돌이 적용될 것이다.)


그럼 createCollisionResponseAnimator()의 여러 인자들을 상세히 살펴 보자. 첫번째 인자는 TriangleSelector로써 월드에서 어떤 방식으로 충돌 확인을 할지 정한 것이다. 두번째는 충돌 확인이 적용될 씬 노드다. 예제의 경우엔 카메라다. 세번째는 얼마나 큰 영역으로 충돌 확인을 할지 결정하는 것으로 원의 반지름을 말한다. 이 수치를 작게하면 카메라는 벽에 좀 더 붙을 수 있을 것이다. 다음 인자는 중력 방향과 속도다. (0, 0, 0)으로 정하면 중력 적용이 안된다. 그리고 마지막 인자는 변환 관련 인자다. 만약 이것이 없다면 카메라는 충돌 확인 타원의 중앙에 위치할 것이다. 그러나 사람의 경우 눈은 전체 몸의 상단에 위치해 있다. 충돌 확인 타원은 중앙이 아닌 상단에 위치해야 한다. 여기서는 중앙에서 윗 방향으로 50정도 높게 적용했다.


scene::ICameraSceneNode* camera =
smgr->addCameraSceneNodeFPS(0, 100.0f, 300.0f, -1, 0, 0, true);
camera->setPosition(core::vector3df(-100,50,-150));

scene::ISceneNodeAnimator* anim =

smgr->createCollisionResponseAnimator(
selector, camera, core::vector3df(30,50,30),
core::vector3df(0,-3,0),
core::vector3df(0,50,0));
camera->addAnimator(anim);
anim->drop();


충돌 확인은 IRRLICHT에선 그리 큰 영역이 아니기에, 이 예제에선 피킹 관련 두가지 방법을 기술하겠다. 이 전에 씬 관련해 약간 준비할게 있다. 우선 피킹을 하면 (그것을 확인해 주는) 동적 광원을 받는 세 개의 애니메이션 캐릭터가 필요하다. 그리고 사용자의 시점을 표현하고 무엇을 피킹할지 결정하는 교차점 빌보드와 마우스 커서 제거를 해야한다. (역자 주 : 교차점이란 FPS의 크로스 헤어처럼 화면 중앙에 위치해 시점과 목표점을 표시해주는 것을 의미. 그리고 빌보드란 카메라가 어디에 있건 동일한 모습, 방향만을 보여주는 스프라이트를 의미한다.)


// disable mouse cursor

device->getCursorControl()->setVisible(false);


// add billboard

scene::IBillboardSceneNode * bill = smgr->addBillboardSceneNode();
bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR );
bill->setMaterialTexture(0, driver->getTexture("../../media/particle.bmp"));
bill->setMaterialFlag(video::EMF_LIGHTING, false);
bill->setMaterialFlag(video::EMF_ZBUFFER, false);
bill->setSize(core::dimension2d<f32>(20.0f, 20.0f));


// add 3 animated faeries.

video::SMaterial material;
material.Texture1 = driver->getTexture("../../media/faerie2.bmp");
material.Lighting = true;

scene::IAnimatedMeshSceneNode* node = 0;
scene::IAnimatedMesh* faerie = smgr->getMesh("../../media/faerie.md2");

if (faerie)
{
node = smgr->addAnimatedMeshSceneNode(faerie);
node->setPosition(core::vector3df(-70,0,-90));
node->setMD2Animation(scene::EMAT_RUN);
node->getMaterial(0) = material;

node = smgr->addAnimatedMeshSceneNode(faerie);
node->setPosition(core::vector3df(-70,0,-30));
node->setMD2Animation(scene::EMAT_SALUTE);
node->getMaterial(0) = material;

node = smgr->addAnimatedMeshSceneNode(faerie);
node->setPosition(core::vector3df(-70,0,-60));
node->setMD2Animation(scene::EMAT_JUMP);
node->getMaterial(0) = material;
}

material.Texture1 = 0;
material.Lighting = false;


// Add a light

smgr->addLightSceneNode(0, core::vector3df(-60,100,400),
video::SColorf(1.0f,1.0f,1.0f,1.0f), 600.0f);


이 작업을 완료하려면 설정한 것들을 드로잉 루프(the drawing loop)에 실제 넣어야 한다. 현재 선택된 것과 직전 마지막으로 선택된 씬 노드를 기억할 변수 포인터 두 개를 잡은 후 메인 루프를 시작한다.


scene::ISceneNode* selectedSceneNode = 0;
scene::ISceneNode* lastSelectedSceneNode = 0;


int lastFPS = -1;

while(device->run())
if (device->isWindowActive())
{
driver->beginScene(true, true, 0);

smgr->drawAll();


smgr->drawAll()로 먼저 기본 씬을 그려준 후 첫번째 피킹을 처리하자. 이는 게이머가 월드의 무엇을 (화면 상에서) 보고 있는지에 알려주는 삼각형 폴리곤 피킹이다. 추가적으로 퀘이트 3 맵에서 그 본 것의 지점을 뽑아내보자. 이를 위해 카메라에서 시작해 보이는 방향 쪽에 다다르는 3차원 선(line)을 만들어야 한다. 그런 후 충돌 확인 매니저(the collision manager)에 이 선이 삼각 선택자에 저장된 월드의 어떤 삼각형과 충돌하는지 알려줄 것을 요청한다. 만약 충돌하고 있으면 그 교차점에 3차원 삼각형을 그리고 교차점 빌보드를 위치시킬 것이다. (역자 주 : 교차점 빌보드를 위치시킨다는게 좀 애매하다. 이에 대해선 bill->setPosition(intersection) 부분을 주석 처리한 후 실험을 해봐야 한다. 주석 처리한 경우와 안 한 경우로 이 프로그램의 첫 시작점에서 마우스를 움직여 월드의 윗 방향 꼭대기를 보라. 맵이 없는 공중 부근에 가면 빌보드가 월드 끝에만 붙고 하늘에는 안 붙는 현상을 보게 될 것이다. 즉 빌보드는 눈에 보이는 화면 정 중앙에 충돌한 월드가 있어야만 그려진다. 또한 먼 거리를 보면 교차점 빌보드도 작아지는데 이는 교차점 빌보드가 충돌 지점 '그 자리'에 위치함을 증명한다. 다양한 응용이 가능한 부분이다.)


core::line3d<f32> line;
line.start = camera->getPosition();
line.end =

line.start + (camera->getTarget() - line.start).normalize() * 1000.0f;

core::vector3df intersection;
core::triangle3df tri;

if (smgr->getSceneCollisionManager()->getCollisionPoint(
line, selector, intersection, tri))
{
bill->setPosition(intersection);

driver->setTransform(video::ETS_WORLD, core::matrix4());
driver->setMaterial(material);
driver->draw3DTriangle(tri, video::SColor(0,255,0,0));
}


또 다른 형태의 IRRLICHT 엔진 지원 피킹은 바운딩 박스(the bounding box)에 기초한 씬 노드 피킹이다. (역자 주 : 바운딩 박스란 충돌 등을 검출하기 위해 임의로 설정한 공간을 말한다. 원형이나 육면체 등이 많이 쓰인다.) 모든 씬 노드는 바운딩 박스를 갖고 있다. 그리고 이 때문에 카메라가 바라보는 씬 노드를 아주 빠르게 잡아낼 수 있다. 다시 이를 충돌 메니저에 요청하겠다. 만약 씬 노드를 구한다면 그것의 재질에 하이라이트를 주겠다. (단 그 씬 노드가 빌보드나 퀘이크 3 맵이면 하이라이트를 안 준다.)


selectedSceneNode =

smgr->

getSceneCollisionManager()->

getSceneNodeFromCameraBB(camera);

if (lastSelectedSceneNode)
lastSelectedSceneNode->setMaterialFlag(video::EMF_LIGHTING, true);

if (selectedSceneNode == q3node || selectedSceneNode == bill)
selectedSceneNode = 0;

if (selectedSceneNode)
selectedSceneNode->setMaterialFlag(video::EMF_LIGHTING, false);

lastSelectedSceneNode = selectedSceneNode;


됐다. 마지막 그리기 처리를 한다.


driver->endScene();

int fps = driver->getFPS();

if (lastFPS != fps)
{
core::stringw str = L"Collision detection example - Irrlicht Engine [";
str += driver->getName();
str += "] FPS:";
str += fps;

device->setWindowCaption(str.c_str());
lastFPS = fps;
}
}

device->drop();

return 0;
}


----------------------------------------------------------------------------

이번 예제를 해석하면서 내용은 쉬우나 많은 개념이 등장해 헤깔리는 면이 많았다. 빌보드가 특정으로 쓰일 때와 일반으로 쓰일 때가 있었으나 제대로 구분이 안 된 듯 하고, select나 selecting, intersection 뭐 이런 단어를 어떻게 해석할지 매번 난감하다. 직접 게임 코드를 돌려보는게 가장 좋은 듯 하다.


예제를 읽다 보니, 씬 노드와 드로잉(drawing)의 윤곽이 명확해 지는 느낌이다. 이는 IRRLICHT 엔진이 추구하는 점이라고도 하겠다. 같은 형, 같은 개념끼리 쭈욱 연결 리스트로 저장해 둔 뒤, 필요에 따라 그것을 사용한다. 자료구조와 알고리즘을 가볍고도 확실히 분리했다. 이 일이 서로 융통성이 있어서 조금의 상상으로도 많은 것을 할 수 있을 듯하다. 또한 이럼에도 불구하고 속도와 용량에서 효율적이라고 하니 대단한 일이다.


아, 참고로 여기 코드는 게시판에 보이기 위한 정렬이므로 실제 엔진 예제 코드로 연습해야 한다.


6. 2D Graphics

Posted 2007/09/15 19:22 by 수달

-------------------------------------------------------------------------

튜토리얼 6. 2D 그래픽

--------------------

이 튜토리얼은 IRRLICHT 엔진에 2D 그래픽 사용법을 보인다. 이미지를 어떻게 그리며(draw), 스프라이트의 키컬러를 적용하고, 반투명 사각형과 다양한 폰트 표시법을 담고 있다. 이것은 IRRLICHT 엔진으로 2D 게임을 만들려고 할때 매우 유용할 것이다. 또한 3D 게임에서라도 멋진 인터페이스를 직접 구현하려면 필요하다. (역자 주 : 보통 상용 게임들은 이런 식으로 직접 인터페이스를 만든다.)


-------------------------------------------------------------------------

시작

----

자, 그럼 헤더 파일, 네임 스페이스, 라이브러리를 걸고 시작해 보자.


#include <irrlicht.h>
#include <iostream>

using namespace irr;

#pragma comment(lib, "Irrlicht.lib")


우선 드라이버 타입을 정하고, 엔진을 시작하며, 캡션 이름을 정한 후 비디오 드라이버 포인터를 저장한다.


int main()
{
// let user select driver type

video::E_DRIVER_TYPE driverType;

printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.2\n"\
" (d) Software Renderer\n (e) NullDevice\n (otherKey) exit\n\n");

char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECTX9; break;
case 'b': driverType = video::EDT_DIRECTX8; break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_NULL; break;
default: return 0;
}

// create device

IrrlichtDevice *device = createDevice(driverType,
core::dimension2d<s32>(512, 384));

if (device == 0)
return 1; // could not create selected driver.

device->setWindowCaption(L"Irrlicht Engine - 2D Graphics Demo");

video::IVideoDriver* driver = device->getVideoDriver();


예제의 모든 2D 그래픽은 '2ddemo.bmp'라는 한장의 텍스처 내에 들어있다. (역자 주 : 이렇게 하면 컬러키 적용 용법을 알 수 있고, 한 텍스처에 다중 이미지를 한번에 로딩한 후 구역별로 잘라 출력하는 법을 알 수 있기 때문이다. 이것만으로도 IRRLICHT 튜토리얼이 얼마나 실전적인지 알 수 있다.) 예제에서는 직접 컬러키의 색상을 말하지 않는다. 다만 엔진에게 '텍스처의 (0, 0)에 있는 색상을 컬러키 값으로 써라.'라고만 한다. 물론 개발자가 직접 driver->makeColorKeyTexture(images, video::SColor(0, 0, 0, 0));을 통해 정할 수도 있다. (역자 주 : SColor(a, r, g, b)는 빼야 할 컬러키) 만약 알파 채널을 적용하려고 하면 makeColorKeyTexture()를 사용해야 한다.


video::ITexture* images = driver->getTexture("../../media/2ddemo.bmp");
driver->makeColorKeyTexture(images, core::position2d<s32>(0,0));


한 화면에 서로 다른 두 폰트로 텍스트를 그리기 위해선, 일단 그 둘을 모두 읽어야 한다. 기본적으로 기본(default) 폰트는 엔진 생성과 함께 읽혀진 상태다. (역자 주 : 그래서 font는 걍 device에서 읽었고, font2는 외부에서 폰트를 읽었다.)


gui::IGUIFont* font = device->getGUIEnvironment()->getBuiltInFont();
gui::IGUIFont* font2 =

device->getGUIEnvironment()->getFont("../../media/fonthaettenschweiler.bmp");


다음으로 화면에 출력할 빨간 임프(Imp : 몬스터)의 사각 위치를 정한다. 이 위치는 우리가 위에서 읽은 스프라이트 상의 어느 부분을 읽어 출력할 것인가를 나타낸다. 두개를 읽는 이유는 스프라이트 애니메이션(매 프레임마다 스프라이트를 번갈아 찍는 2D 애니메이션)을 위해서다.


core::rect<s32> imp1(349,15,385,78);
core::rect<s32> imp2(387,15,423,78);


모든 게 준비되었고, beginScene과 endScene 사이에서 준비된 모든 것을 그려주기만 하면 된다. 이 예제에선 2D 그래픽만 그렸으나,3D 그래픽과 섞어 써도 아무 문제가 없다. 한번 해보길 바란다. 씬 매니저를 이용한 어떤 류의 3D 버텍스(점)나 씬이라도 괜찮다.


while(device->run() && driver)
{
if (device->isWindowActive())
{
u32 time = device->getTimer()->getTime();

driver->beginScene(true, true, video::SColor(0,120,102,136));


makeColorKeyTexture를 적용한 세 개의 스프라이트를 그린다. 마지막 인자는 makeColorKeyTexture에서 정한 알파 채널을 사용할 것인지를 지정한 것이다. 그 전 인자는 스프라이트에 적용할 색상으로 (255, 255, 255, 255)가 원래의 색상이다. (역자 주 : 이 색을 바꾸면 출력할 그림에 영향을 주어 다양한 효과를 낼 수가 있다.) 세 번째 스프라이트의 경우 시간에 따라 적용 색상을 바꿔 보았다.

// draw fire & dragons background world
driver->draw2DImage(images, core::position2d<s32>(50,50),
core::rect<s32>(0,0,342,224), 0,
video::SColor(255,255,255,255), true);

// draw flying imp
driver->draw2DImage(images, core::position2d<s32>(164,125),
(time/500 % 2) ? imp1 : imp2, 0,
video::SColor(255,255,255,255), true);

// draw second flying imp with colorcylce
driver->draw2DImage(images, core::position2d<s32>(270,105),
(time/500 % 2) ? imp1 : imp2, 0,
video::SColor(255,(time) % 255,255,255), true);


텍스처 그리는게 가장 간단하다. 코드 자체만으로도 해석이 될 것이다.


// draw some text
if (font)
font->draw(L"This demo shows that Irrlicht is also capable of drawing 2D graphics.",
core::rect<s32>(130,10,300,50),
video::SColor(255,255,255,255));

// draw some other text
if (font2)
font2->draw(L"Also mixing with 3d graphics is possible.",
core::rect<s32>(130,20,300,60),
video::SColor(255,time % 255,time % 255,255));


마지막으로 IRRLICHT 엔진 로고를 색상 변환이나 알파값 없이 그리고, 마우스를 따라다니는 투명 사각형을 표현하자.


// draw logo
driver->draw2DImage(images, core::position2d<s32>(10,10),
core::rect<s32>(354,87,442,118));

// draw transparent rect under cursor
core::position2d<s32> m = device->getCursorControl()->getPosition();
driver->draw2DRectangle(video::SColor(100,255,255,255),
core::rect<s32>(m.X-20, m.Y-20, m.X+20, m.Y+20));

driver->endScene();
}
}


참 쉽지 않은가?


device->drop();

return 0;
}

-------------------------------------------------------------------------

위에서 잠시 언급한대로 튜토리얼들은 실제 게임을 만드는데 무엇이 필요한지를 잘 알고 있는 개발자에 의해 만들어졌다. 2D에 그리니, 3D에 그리니, 2.5D를 쓰니 마니 고민할 필요가 없을 정도로 쉽게 되어있다. 또한 실무에서 어떻게 그림을 떼어 화면에 보일지도 잘 나타나 있다. 앞으론 이것을 이용해야 겠다.


5. User Interface (+06.08.16)

Posted 2007/09/15 19:21 by 수달

-------------------------------------------------------------------------

튜토리얼 5 : 유저 인터페이스

--------------------------

이번 튜토리얼은 IRRLICHT 엔진의 유저 인터페이스를 만드는 방법을 보일 것이다. 게임 내 윈도우, 버튼, 스크롤 바, 텍스트(static text)와 리스트 박스를 어떻게 만드는지 간단히 설명하겠다. 프로그램은 다음과 같은 모습이 될 것이다.


-------------------------------------------------------------------------

시작

----

항상 그렇듯이 헤더 파일과 네임 스페이스를 부르자. iostream은 콘솔에서 사용자의 입력을 받기 위해서다. 또한 IRRLICHT 장치, 윈도의 새로운 위치 값을 위한 참조 변수(counter variable), 그리고 리스트 박스용 포인터를 확보해 두자.


#include <irrlicht.h>

#inlcude <iostream>


using namespace irr;


using namespace core;

using namespace scene;

using namespace video;

using namespace io;

using namespace gui;


#pragma comment(lib, "irrlicht.lib")


IrrlichtDevice * device = 0;

s32 cnt = 0;

IGUIListBox * listbox = 0;


이벤트 리시버(Event Receiver)는 키보드와 마우스 입력을 받는 것 뿐만 아니라, GUI(그래픽 유저 인터페이스)의 이벤트에도 사용된다. 버튼 클릭, 리스트 박스 선택 변화, 한 GUI 개체의 이동 등 거의 모든 발생 이벤트를 관리 한다. 이벤트 리시버는 어떤 이벤트 인지, 호출한 아이디는 뭔지, 현재 GUI가 어떤 상태인지 등을 알 수 있다.


class MyEventReceiver : public IEventReceiver

{

public:

virtual bool OnEvent(SEvent event)

{

+ GUI 이번트인지 확인해야 한다. 이는 9. MeshViewer에서 가져온 코드를 삽입하면된다. 이렇게 안하면 id를 제대로 읽지 못해 비정상 종료가 될 수 있다.

if (event.EventType == EET_GUI_EVENT) //<-- 이것 추가.

{

s32 id = event.GUIEvent.Caller->getID();

IGUIEnvironment * env = device->getGUIEnvironment();


switch(event.GUIEvent.EventType)

{


이 예에서 상단 스크롤바 위치에 따라 모든 GUI 요소들에 색상 변화가 일어난다. 이는 매우 쉬운 일로 각각의 색상을 기억하고 있는 스킨 객체가 있고, 우리는 이벤트(스크롤 변환)에 따라 저장된 스킨과 알파값만 꺼내 적용시켜 주면 된다.


case EGET_SCROLL_BAR_CHANGED:

if(id == 104)

{

s32 pos = ((IGUIScrollBar *) event.GUIEvent.Caller)->getPos();


for(s32 i = 0; i < EGDC_COUNT; ++i)

{

SColor col =

env->getSkin()->getColor((EGUI_DEFAULT_COLOR) i);

col.setAlpha(pos);

env->getSkin()->setColor((EGUI_DEFAULT_COLOR)i, col);

}

}

break;


만약 버튼이 클릭 되면, 예제에 만들어질 세개 버튼 중의 하나일 것이다. 첫번째 버튼은 엔진을 끄는 것이고, 두번째 버튼은 글이 쓰인 작은 창을 만들 것이며, 세번째 버튼은 '파일 열기 다이얼로그'를 생성할 것이다. 이때 두번째 버튼과 세번째 버튼의 경우 이벤트 발생에 대한 로그(log) 문자가 리스트 박스에 남게 될 것이다.


case EGET_BUTTON_CLICKED:

if(id == 101)

{

device->closeDevice();

return;

}

if(id == 102)

{

listbox->addItem(L"Window created");

cnt += 30;

if(cnt > 200) cnt = 0;


IGUIWindow * window =

env->addWindow(rect<s32>(100 + cnt, 100 + cnt,

300 + cnt, 200 + cnt),

false, // modal?

L"Test window");


env->addStaticText(L"Please close me", rect<s32>(35, 35, 140, 50),

true, //border?

false, //wordwrap?

window);

return true;

}

if(id == 103)

{

listbox->addItem(L"File open");

env->addFileOpenDialog(L"Please choose a File.");

return true;

}

break;

} //switch

} //<--

}

return false;

}

};


본격적으로 시작하자. 우선 IRRLICHT 장치를 만든다. 이전의 예처럼 사용자가 어떤 장치를 쓸지 물어 본다.


int main()

{

// ask user for driver


video::E_DRIVER_TYPE driverType;


printf("Please select the driver you want for this example:\n"\
" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.2\n"\
" (d) Software Renderer\n (e) NullDevice\n (otherKey) exit\n\n");


char i;
std::cin >> i;

switch(i)
{
case 'a': driverType = video::EDT_DIRECTX9; break;
case 'b': driverType = video::EDT_DIRECTX8; break;
case 'c': driverType = video::EDT_OPENGL; break;
case 'd': driverType = video::EDT_SOFTWARE; break;
case 'e': driverType = video::EDT_NULL; break;
default: return 1;
}


// create device and exit if creation failed

device = createDevice(driverType, core::dimension2d<s32>(640, 480));

if (device == 0)
return 1; // could not create selected driver.


디바이스 생성이 성공하면, 이벤트 리시버를 설정하고 드라이버와 GUI 관련 포인터를 저장해 두자.


MyEventReceiver receiver;
device->setEventReceiver(&receiver);
device->setWindowCaption(L"Irrlicht Engine - User Inferface Demo");

video::IVideoDriver* driver = device->getVideoDriver();
IGUIEnvironment* env = device->getGUIEnvironment();


버튼 세개를 더한다. 첫번째는 엔진을 종료하는 것이고, 두번째는 새 창 하나를 여는 것이고, 세번째는 파일 열기 다이얼로그를 부르는 것이다. addButton의 세번째 인자는 각 버튼의 ID이며, 이를 통해 쉽게 이벤트 리시버에서 각각을 식별하게 된다.


env->addButton(rect<s32>(10,210,100,240), 0, 101, L"Quit");
env->addButton(rect<s32>(10,250,100,290), 0, 102, L"New Window");
env->addButton(rect<s32>(10,300,100,340), 0, 103, L"File Open");


문자표시박스(static text)와 스크롤 바를 추가한다. 스크롤 바에는 모든 GUI를 변경시키는 기능을 붙인다. 스크롤 바의 최대 값으로 255를 정한다. 255인 이유는 색상 값의 최대치가 255이기 때문이다. 그런 후 또 하나의 문자표시박스와 리스트 박스를 만들자.


env->addStaticText(L"Transparent Control:", rect<s32>(150,20,350,40), true);
IGUIScrollBar* scrollbar =

env->addScrollBar(true, rect<s32>(150, 45, 350, 60), 0, 104);
scrollbar->setMax(255);

env->addStaticText(L"Logging ListBox:", rect<s32>(50,80,250,100), true);
listbox = env->addListBox(rect<s32>(50, 110, 250, 180));


좀 더 좋은 폰트를 만들기 위해 확장 폰트를 읽고 적용해 보자. 마지막으로 좌상단에 IRRLICHT 엔진의 멋진 로고를 붙이자.


IGUISkin* skin = env->getSkin();
IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp");
if (font)
skin->setFont(font);

IGUIImage* img = env->addImage(
driver->getTexture("../../media/irrlichtlogoalpha.tga"),
position2d<int>(10,10));


자, 이제 그리기만 하면 된다.


while(device->run() && driver)

{
if (device->isWindowActive())
{
driver->beginScene(true, true, SColor(0,200,200,200));

env->drawAll();

driver->endScene();
}

}

device->drop();

return 0;
}


-------------------------------------------------------------------------

엔진에 관한 약간의 회의가 있었었다. 단순히 생각해 게임에서 프로그래밍이란 그림 디스플레이하고 조작만 적용시키면 되는 것 아닌가 하는 생각 때문이었다. 그러나 현업에 점점 적응해가고 게이머의 눈높이를 생각해보면, 그렇게 쉽게 날로 먹을 것이 없다는 생각이 들었다. 모든게 다 필요하다.


물론 그것은 항상 깨질 준비가 된 것들이다. 문화 컨텐츠가 그런 것이듯 오늘의 각종 장치와 인터페이스는 '깨질 운명'이라고 봐도 좋을 것이다. 허나 그럼에도 불구하고 개발자는 그 모든 것을 일단은 습득하고 있어야 한다.


이번 예제를 통해 GUI를 만들어 봤다. GUI란 게임 내에 하나의 윈도 시스템을 만드는 것과 같다. 이번 예제의 예는 매우 단순한 형태이며 그 모양도 일반 윈도와 같이 규칙화 되어있다. 이것을 처음에는 그대로 쓰겠지만, 더 멋진, 더 상업적인 인터페이스를 위해선 개선이 필요하다. 특히 예제의 윈도 박스나 버튼 모양, 크기, 기능을 어디서 어떻게 변경하는지가 이후의 숙제다.


4. Movement

Posted 2007/09/15 19:20 by 수달

--------------------------------------------------------------------------

튜토리얼 4 : 이동

----------------

이번 튜토리얼은 씬 노드를 이동하고 매니메이션하는 법을 보인다. 씬 노드 애니메이터의 기본 컨셉은 키보드를 이용해 노드를 수동적으로 움직이는 것이다.

<예제 그림>


--------------------------------------------------------------------------

시작

----

항상 그렇듯이 헤더 파일, 네임 스페이스, 라이브러리를 연결하자.


#include <stdio.h>

#include <wchar.h>

#include <irrlicht.h>


using namespace irr;


#pragma comment(lib, "Irrlicht.lib")


이번 예제의 목표 중 하나는 정해진 키보드 키를 사용해 씬 노드를 움직이는 것이다. 이를 위해 움직여야 할 위치를 저장하는 포인터 하나를 설정하겠다. (역자 주 : node 변수가 그것임) 그리고 발생하는 이벤트를 받아 씬 노드를 다루고 활성된 카메라를 얻기 위한 IRRLICHT 장치(device) 포인터를 하나 설정하겠다.


scene::ISceneNode * node = 0;

IrrlichtDevice * device = 0;


마우스나 키보드, 혹은 "Ok 버튼이 눌렸다."와 같은 GUI 이벤트를 얻기 위해, IEventReciver 객체가 필요하다. 거기에는 OnEvent()라는 재정의 할 함수 하나가 있다. 이 함수는 이벤트가 발생하면 엔진에 의해 불리는 함수다. 여기에선 W와 S 키를 사용해 씬 노드를 이동하게 하는데 사용한다.


class MyEventReceiver : public IEventReceiver

{

public:

virtual bool OnEvent(SEvent event)

{


만약 키 W나 S가 눌려진 것이면 씬 노드의 Y 축 위치를 조금 바꿀 수 있게 하겠다. 만약 사용자가 W를 누르면 노드는 위로 올라갈 것이고, S를 누르면 아래로 내려가게 보일 것이다.


if(node != 0 && event.EventType == irr::EET_KEY_INPUT_EVENT &&

!event.KeyInput.PressedDown)

{

switch(event.KeyInput.Key)

{

case KEY_KEY_W:

case KEY_KEY_S:

{

core::vector3df v = node->getPosition();

v.Y += event.KeyInput.Key == KEY_KEY_W ? 2.0f : -2.0f;

node->setPosition(v);

}

return true;

}

}

return fasle;

}

};


씬 노드를 움직일 이벤트 리시버를 준비했다. 이제 할 일은 움직여 볼 씬 노드와 IRRLICHT 엔진을 만드는 것이다. 추가적으로 IRRLICHT 엔진에서 할 수 있는 이동과 애니메이션을 보여주기 위해 몇 개 노드를 만들겠다.


int main()

{

MyEventReceiver receiver;

device = createDevice(video::EDT_OPENGL,

core::dimension2d<s32>(640, 480), 16,

false, false, false, &receiver);

video::IVideoDriver * driver = device->getVideoDriver();

scene::ISceneManager * smgr = device->getSceneManager();


W와 S 키로 움직이는 노드를 만들자. 테스팅 목적의 직육면체 하나를 만들어 (0,0,30)에 위치시키고, 여기에 볼만한 텍스처까지 적용해 본다.


node = smgr->addTestSceneNode();

node->setPosition(core::vector3df(0, 0, 30));

node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));


자, 이제 씬 노드 애니메이터를 사용할 또 다른 노드를 만들어보자. 씬 노드 애니메이터는 씬 노드를 다양하게 변경시킬 수 있고, 메시 씬 노드, 빌 보드, 광원, 카메라등 다른 씬 노드에 의해 부착 될 수 있다. (역자 주 : 씬 노드 애니메이터의 기능을 사용자가 다른 씬 노드에서도 사용가능하게 한다.) 씬 노드 애니메이터는 위치의 변경 뿐만 아니라, 예를 들어 한 개체의 텍스처를 애니메이션할 때도 사용된다. (역자 주 : 한 (폴리곤, 메시) 개체에 여러 텍스처를 바꿔 입혀 애니메이션을 할 수 있다.) 예제에서는 테스트용으로 처음 만든 씬 노드 주위를 둥글게 날아다니는 'fly circle'을 만들도록 하겠다.


scene::ISceneNode * n = smgr->addTestSceneNode();

n->setMaterialTexture(0, driver->getTexture("../../media/t351sml.jpg"));


scene::ISceneNodeAnimator * anim =

smgr->createFlyCircleAnimator(core::vector3df(0,0,30), 20.0f);

n->addAnimator(anim);

anim->drop();


마지막으로 보여줄 씬 노드 애니메이터는 md2 모델이다. 이 모델로는 앞으로 곧게 가는 'fly straight' 애미네이터를 적용해 보겠다.


scene::IAnimatedMeshSceneNode * anms =

smgr->addAnimatedMeshSceneNode(

smgr->getMesh("../../media/sydney.md2"));

if(n)

{

anim = smgr->createFlyStraightAnimator(core::vector3df(100, 0, 60),

core::vector3df(-100,0,60), 10000, true);

anms->addAnimator(anim);

anim->drop();


모델을 보기 좋게 하기 위해, 광원을 설정하고(안하면 모델이 검게 나온다), 애니메이션 관련 폭과 속도, 텍스처 등등을 정해 보자. 아래 코드에서 애니메이션과 관련해 setFrameLoop()와 setAnimationSpeed() 대신 anms->setMD2Animation(scene::EMAT_RUN)으로 대신 뛰는 애니메이션을 할 수 있다. (MD2모델만 적용 가능함)


anms->setMaterialFlag(video::EMF_LIGHTING, false);

anms->setFrameLoop(320, 360); anms->setAnimationSpeed(30);

anms->setRotation(core::vector3df(0, 180.0f, 0));

anms->setMaterialTexture(0,

driver->getTexture("../../media/sydney.bmp"));

}


배치한 씬을 보기 위해 1인칭 슈팅 게임 스타일의 카메라와 마우스를 정하겠다.


smgr->addCameraSceneNodeFPS(0, 100.0f, 100.0f);

device->getCursorControl()->setVisible(false);


모든 준비가 완료되었다. 그리는 일만 남았다. 예제에서는 초당 프레임과 윈도 캡션에 이름을 추가한다.


int lastFPS = -1;

while(device->run())

{

driver->beginScene(true, true, video::SColor(255, 90, 90, 156));

smgr->drawAll();

driver->endScene();


int fps = driver->getFPS();


if(lastFPS != fps)

{

wchar_t tmp[1024];

swprintf(tmp, 1024,

L"Movement Example - Irrlicht Engine (%s) (fps:%d)",

driver->getName(), fps);

device->setWindowCaption(tmp);

lastFPS = fps;

}

}

device->drop();

return 0;

}


컴파일한 후 프로그램을 실행해 보라.


+ Recent posts