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화면에 나타나게 되는데 이때를 픽셀이라 한다. 그래픽 카드마다 이들을 어떻게 처리하는지 조금씩 다르다.


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


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


+ Recent posts