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와의 차이를 알 수 있을 것이다.


+ Recent posts