2011년 12월 28일 수요일

Lighting 모델의 성능 비교


최근에 KGC 에서 발표를 한 이후 예전에 영문 블로그에 써놨던 Oren-Nayar관련 포스트들을 좀 살펴봤다. 근데 이 포스트를 한글 블로그에는 올리지 않은 것 같아 재빨리 올려본다.

조명 공식을 바꿀때마다 성능에 신경이 안쓰일 수가 없는데 다행히 쉐이더 코드와 그 성능을 비교해 놓은 표를 찾았다. 특히 diffuse와 specular 조명을 모두 계산하는 Cook-Torrance 모델이 Blinn-Phong 스페큘라와 Oren-Nayar 디퓨즈 모델을 합친 것보다 빠르다는 건 주목할만 하다.

자세한 내용은 여기를 볼 것.

관련 글:


2011년 12월 26일 월요일

[포프의 쉐이더 입문강좌] 04. 기초적인 조명쉐이더 Part 1

이전편 보기


샘플파일받기

제4장 기초적인 조명쉐이더

이 장에서 새로 배우는 HLSL

  • NORMAL: 정점의 법선정보를 불러올 때 사용하는 시맨틱
  • normalize(): 벡터 정규화 함수
  • dot(): 내적 함수
  • saturate(): 0 ~ 1을 넘어서는 값의 범위를 짤라 냄.
  • reflect(): 벡터반사 함수
  • pow(): 거듭제곱 함수


이 장에서 새로 사용하는 수학
  • 내적: 코사인 함수를 재빨리 계산할 때 사용할 수 있음
  • 정규화: 벡터를 단위벡터(길이가 1인 벡터)로 만듬.


빛이 존재하지 않는다면 물체를 볼 수 없습니다. 매우 당연한 이치인데도 이걸 까먹고 지내는 분들이 많은 것 같습니다. (저도 종종 까먹습니다.) 예를 들어, 창문이 하나도 없는 방에 들어가서 문을 닫아버리면 아무것도 볼 수가 없지요? 어디서 새어 들어오는 빛이 있지 않는 한 아무리 어둠 속에서 오래 있어도 아무것도 보이지 않습니다. 이 당연한 사실을 자꾸 까먹는 이유는 실생활에서 완전히 칠흑 같은 어둠을 찾기가 쉽지 않기 때문입니다. 왜일까요? 바로 끝없이 반사하는 빛의 성질 때문입니다. 딱히 눈에 뜨이는 광원이 없더라도 대기중의 미세입자에 반사되어 들어오는 빛까지 있으니까요. 이렇게 다른 물체에 반사돼서 들어오는 빛을 간접광이라 합니다. 반대로 직접광은 광원으로부터 직접 받는 빛입니다. 그림 4.1에 예를 들어보겠습니다.

그림 4.1 직접광과 간접광의 예

직접광과 간접광 중에서 어떤 것을 더 쉽게 계산할 수 있을까요? 위 그림만 봐도 딱 답이 나오죠? 직접광입니다. 간접광은 수없이 반사의 반사를 거치므로 당연히 직접광보다 계산하기 어렵습니다. 간접광을 계산하는 방법 중 하나로 광선추적(ray-tracing)이라는 기법이 있습니다. 아마 3D 그래픽에 관심이 많으신 분들이라면 최근 들어 광선추적에 대해 논하는 많은 자료를 보셨을 겁니다. 하지만, 아직도 실시간 광선추적기법이 게임에서 널리 사용되지 않는 이유는 하드웨어 사양이 따라주지 않기 때문이라죠. (특히 콘솔 하드웨어의 하드웨어 사양이 더 큰 문제입니다.) 그렇기 때문에 아직도 컴퓨터 게임 등을 비롯한 실시간 3D 프로그램에서는 주로 직접광만을 제대로 계산하고 간접광은 흉내내기 정도로 그치는 게 보통입니다. 따라서 이 장에서도 직접광만을 다루도록 하겠습니다. (간접광까지도 다루는 조명모델을 전역조명모델(global illumination model)이라고 합니다. 반대로 직접광만을 다루는 조명모델을 지역조명모델(local illumination model)이라 합니다.)  참고로 이 장에서 배우는 조명 쉐이더는 아직까지도 대부분의 게임에서 표준으로 사용하는 기법이므로 잘 숙지해 두세요.

빛을 구성하는 요소는 크게 난 반사광(diffuse light)과 정 반사광(specular light)이 있습니다. 이 둘을 따로 살펴보도록 하겠습니다.

난 반사광
배경
대부분의 물체는 스스로 빛을 발하지 않습니다. 그럼에도 저희가 이 물체들을 지각할 수 있는 이유는 다른 물체(예, 태양)가 발산하는 빛이 이 물체의 표면에서 반사되기 때문입니다. 이 때, 여러 방향으로 고르게 반사되는 빛이 있는데 이것을 난 반사광(diffuse light)이라고 합니다. (diffuse 광은 아직도 용어정립이  안되고 있습니다따라서  용어를 사용할  때마다 종종 영문 표기를 같이 하도록 하겠습니다다른 용어로는 산란광확산광 등이 있는데  반사광이 가장 적합한  같습니다.) 어느 방향에서 바라봐도 물체의 명암이나 색조가 크게 변하지 않는 이유를 아시나요? 여러 방향으로 고르게 퍼지는 난 반사광 덕분입니다. 만약 빛이 한 방향으로만 반사된다면(이것이 뒤에서 살펴볼 정 반사광입니다.) 그 방향에서만 물체를 지각할 수 있겠지요.

참고로 물체의 표면이 거칠수록 난반사가 심해지는 것이 보통입니다. (표면이 완전히 매끈하더라도 난반사가 완전히 사라지는 경우는 극히 드뭅니다. 표면을 뚫고 들어간 뒤, 물체 내부에서 반사되는 빛도 있기 때문입니다.)

일단 난 반사광을 그림으로 그려 보겠습니다.

그림 4.2. 난 반사광

그림 4.2에서 아직 보여 드리지 않은 것이 조금 후에 배워 볼 정 반사광입니다. 정 반사광이 무엇인지는 나중에 알려 드릴 테니 일단은 입사광 중의 일부는 난 반사광이 되고 다른 일부는 정 반사광이 된다고만 기억해 두세요.

자, 그렇다면 수학적으로 난 반사광을 어떻게 계산할까요? 당연히 수학자마다 다른 주장을 하지만 그 중에서 게임에서 주로 사용하는 람베르트(lambert) 모델을 살펴봅시다. 요한 람베르트라는 수학자가 창시한 람베르트 모델은 표면법선(법선(normal)이란 표면의 방위(orientation)를 나타내는 벡터입니다. 따라서 그림 4.2에서처럼 좌우로 평평한 평면의 법선은 위쪽으로 수직인 선이 됩니다.)과 입사광이 이루는 각의 코사인 값을 구하면 그게 바로 난 반사광의 양이라고 합니다. 그렇다면 일단 코사인 함수의 그래프를 볼까요?


그림 4.3. y = cos(x) 그래프

위 그래프를 보시면 입사광과 표면 법선의 각도가 0일 때, 결과(y축의 값)가 1인 거 보이시죠? 그리고 각도가 늘어날수록 결과가 점점 작아지다가 90도가 되니 0이 돼버립니다. 여기서 더 나아가면 그 후로는 아예 음수 값이 돼버리네요? 그러면 실제 세계에서 빛의 각도에 따라 결과가 어떻게 바뀌는지 살펴 볼까요?

그림 4.4. 입사광과 법선이 이루는 다양한 각도

위의 그림에서 평면이 가장 밝게 빛나는 때가 언제일까요? 당연히 해가 중천에 떠있을 때겠죠? (그림 a) 그리고 해가 저물어감에 따라 점점 표면도 어두워지겠네요. (그림 b) 이제 해가 지평선을 넘어가는 순간, 표면도 깜깜해집니다. (그림 c) 그렇다면 해가 지고 난 뒤엔 어떻게 되죠? 여전히 표면이 깜깜하겠죠? 표면에 전혀 빛이 닿지 않으니까요. 자, 그럼 이 현상을 그래프로 그려보면 어떻게 될까요? 법선과 해가 이루는 각도를 X축으로 두고 표면의 밝기를 Y축으로 하겠습니다. 여기서 Y축이 가지는 값의 범위는 0~1인데0은 표면이 아주 깜깜한 때를(0%), 1은 표면이 최고로 밝은 때(100%)를 나타냅니다.

그림 4.5. 관찰결과를 그려본 그래프

위 그래프에서 -90 ~ 90도사이의 그래프에 물음표를 달아둔 이유는 각도가 줄어듦에 따라 얼마나 빠르게 표면이 어두워지는지를 모르기 때문입니다. 이제 이 그림을 그림 4.3과 비교해 볼까요? 그림 4.3에서 결과가 0 이하인 부분들을 0으로 만들면 지금 만든 그래프와 꽤 비슷하네요? 차이점이라고는 -90 ~ 90도 사이에서 그래프가 떨어지는 속도가 조금 다르다 뿐이군요. 그렇다면 람베르트 아저씨가 표면이 어두워지는 속도를 아주 꼼꼼히 잘 관찰한 뒤에, 위 코사인 공식을 만들었다고 믿어도 될까요? 전 그렇게 믿고 있습니다. -_-

자, 그럼 람베르트 모델을 적용하면 코사인 함수 한 번으로 난 반사광을 쉽게 구할 수 있겠군요! 하지만 코사인 함수는 그다지 값싼 함수가 아니어서 쉐이더에서 매번 호출하는 것이 영 꺼림직합니다. 다른 대안이 없을까요? 수학책을 뒤적여 보니까 내적(dot product)이라는 연산이 코사인을 대신할 수 있다고 나오는 걸요?

θ = A와 B가 이루는 각도
| A |  = 방향벡터 A의 길이
| B |  = 방향벡터 B의 길이


A ∙ B = cosθ | A || B |

즉,

cosθ = (A ∙ B) ÷ (| A |ⅹ| B |);

위의 내적 공식에 따르면 두 벡터가 이루는 각의 코사인 값은 그 둘의 내적을 구한 뒤 두 벡터의 길이를 곱한 결과로 나눈 것과 같습니다. 여기서 두 벡터의 길이를 1로 만들면 공식을 더 간단히 만들 수 있습니다.

cosθ = (A' ∙ B')

두 벡터가 이루는 각의 코사인 값은 두 벡터의 내적과 같다는 군요. 근데 이렇게 저희 맘대로 벡터의 길이를 바꿔도 되는 걸까요? 이 질문을 다르게 표현하면, '난 반사광을 계산할 때 법선의 길이나 입사광 벡터의 길이가 중요한가요?'입니다. 전혀 그렇지 않지요? 두 벡터가 이루는 각이 중요할 뿐 벡터의 길이는 결과에 아무런 영향을 미치지 않습니다. 따라서 이 두 벡터의 길이를 각각 1로 만들어서 공식을 간단하게 만드는 게 훨씬 나아 보이는군요. (이렇게 길이가 1인 벡터를 단위벡터(unit vector)라고 하며, 단위벡터를 만드는 과정을 정규화(normalize)라고 합니다.)

그럼 내적이 코사인 함수보다 값싼 연산인 이유를 살펴볼까요? 벡터 A의 성분을  (a, b, c)로 두고 벡터 B의 성분을 (d, e, f)로 두면 두 벡터의 내적을 이렇게 간단히 구할 수 있습니다.

A ∙ B = (a ⅹ d) + (b ⅹ e) + (c ⅹ f)

코사인 함수보다 훨씬 간단해 보이는 게 맞죠? 당장 코사인 함수를 구하라고 하면 머리부터 긁적이실 걸요? ^^

자, 그럼 이 정도면 난 반사광에 대해 충분히 설명을 드린 것 같으니 지금 배운 내용들을 까먹기 전에 곧바로 쉐이더를 작성해 보겠습니다.




2011년 12월 21일 수요일

게임개발 포에버라는 팀블로그를 열었습니다.

뭐, 이미 아시는 분은 아시겠지만 한 2주전쯤에 게임개발 포에버라는 이름의 팀블로그를 열었습니다. 이미 제가 있는 북미쪽에서는 인기높은 AltDevBlogADay란 팀블로그에서 컨셉을 빌려왔는데요. 이 블로그의 컨셉은 매우 간단합니다. '다른 게임개발자나 게임개발 지망생 한명에게라도 도움이 될만한 내용을 하루 하나씩 올린다.' 입니다. 매일 글이 올라올 수 있게끔 필자님들도 한 20분 모셨고, 게임개발자 모두에게 도움이 될 수 있도록 프로그래머/기획자/아티스트 님들을 모두 필자로 받았습니다.

현재 운영한지 대략 2주인데 벌써 올라온 글의 수가 한 타스는 되고(거의 매일 하나씩 올라왔죠. ^^) 방문자 수도 5천명 돌파... 그리고 읽으시는 분들의 반응도 너무 좋습니다.

게임개발에 관심이 많으신 분들은 매일 아침 출근/등교길에 한번씩  들러서 읽어보시기 바랍니다.


그리고 참여해주실 분들을 더 모집합니다. 필자분들을 최소 100명 정도 모으는게 목적입니다. 특히 아트 및 기획 관련 글을 기고해주실 분들이 더 필요합니다. ^^ 관심있으신 분들은 필자 가이드 페이지를 읽어보시고 제게 연락을 주세요.


2011년 12월 19일 월요일

[포프의 쉐이더 입문강좌] 03. 텍스처매핑 Part 2

이전편 보기



픽셀쉐이더
정점쉐이더에서와 마찬가지로 전체 소스코드부터 보여드립니다.

sampler2D DiffuseSampler;


struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};


float4 ps_main( PS_INPUT Input ) : COLOR
{
   float4 albedo = tex2D(DiffuseSampler, Input.mTexCoord);
   return albedo.rgba;
}

픽셀쉐이더 입력데이터 및 전역변수

이제 픽셀쉐이더를 살펴보기로 하죠. 픽셀쉐이더에서 할 일은 텍스처 이미지에서 텍셀(texel)(그림(picture)의 최소구성단위가 픽셀(pixel)인 것처럼 텍스처(texture)의 최소구성단위가 텍셀(texel)입니다.) 을 구해와 그 색을 화면에 출력하는 것이겠군요. 그렇다면 텍스처로 사용할 이미지와 현재 픽셀의 UV 좌표가 필요하겠죠? 텍스처 이미지는 픽셀마다 변하는 값이 아니므로 전역변수로, UV 좌표는 정점쉐이더로부터 보간기를 거쳐 들어온 입력데이터가 되겠네요. 우선 픽셀쉐이더 입력데이터의 구조체부터 만들겠습니다.

struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};

어라? 별 다른 게 없네요? VS_OUTPUT 구조체를 가져와 mPosition을 지워버린 것 뿐이군요. 사실 픽셀쉐이더의 입력데이터는 정점쉐이더의 출력데이터와 일치할 수밖에 없습니다. 어차피 정점쉐이더에서 반환한 값을 가져오는 거니까요.

다음은 텍스처를 선언할 차례군요. 앞서 렌더몽키 프로젝트를 설정할 때 DiffuseSampler 라는 이름의 텍스처 개체를 만들었던 거 기억하시죠? 바로 이 개체가 텍셀을 구할 때 사용하는 텍스처 샘플러입니다. 따라서 HLSL 코드에서 사용하는 텍스처 샘플러의 이름도 DiffuseSampler 여야 합니다.

sampler2D DiffuseSampler;

sampler2D는 HLSL에서 지원하는 데이터형 중에 하나로 2D 텍스처에서 텍셀 하나를 구해오는데 사용합니다. 이 외에도 sampler1Dsampler3DsamplerCUBE 등의 샘플러가 있습니다.

이제 픽셀쉐이더 함수를 작성해보죠.

픽셀쉐이더 함수
우선 헤더부터 보겠습니다.

float4 ps_main( PS_INPUT Input ) : COLOR
{

이전과 달라진 점이라면 PS_INPUT형의 Input 매개변수를 받는다는 것뿐이군요. 보간기가 계산해준 UV 좌표 값을 받아오기 위해서입니다. 자, 이제 UV 값과 텍스처 샘플러가 있으니 텍셀 값을 구하는 일만 남았군요. tex2D라는 HLSL 내장함수를 사용하시면 매우 쉽게 이런 일을 할 수 있습니다. tex2D는 첫 번째 매개변수로 텍스처 샘플러를,두 번째 매개변수로 UV 좌표를 받습니다.

    float4 albedo = tex2D(DiffuseSampler, Input.mTexCoord);

위 코드는 DiffuseSampler에서 Input.mTexCoord 좌표에 있는 텍셀을 읽어옵니다. 그 값은 albedo라는 변수에 저장되겠네요. 이제 이 값을 가지고 무슨 일을 해야 할까요? 으음.... 텍스처를 그대로 보여주는 게 목적이니까 그냥 반환하면 되겠네요.

   return albedo.rgba;
}

이제 F5키를 눌러 정점쉐이더와 픽셀쉐이더를 각각 컴파일 한 뒤, 미리 보기 창을 보면...... 엉망이군요?!. 왜일까요? 그것은 정점 버퍼에서 올바른 UV  좌표 값을 불러오도록 설정을 하지 않았기 때문입니다. Workspace 패널 아래에서 Stream Mapping을 찾아 마우스를 더블클릭하세요. POSITION이라는 항목만 있는 거 보이시죠? 이제 Add 버튼을 눌러 새 항목을 추가한 뒤 Usage를 TEXCOORD로 바꿉니다. Index가 0이고 Data Type이 FLOAT2로 되어있는지도 확인하세요. Attribute Name은 굳이 손 안대셔도 됩니다. 이제 OK버튼을 누르면 다음 그림과 같이 제대로 된 지구본을 보실 수 있을 것입니다.


그림 3.6. 그럴듯해 보이는 지구본

근데 albedo란 변수를 반환할 때 return albedo;라고 하지 않고 return albedo.rgba;라고 한 것 보이시죠? 사실 return albedo;라고 해도 전혀 상관은 없지만 뭔가 새로운 것을 보여 드리기 위해 일부러 저렇게 썼습니다.

HLSL에서는 벡터형 변수 뒤에 xyzw나 rgba 등의 접미사를 붙이는 방법을 사용하여 벡터의 성분들에 매우 쉽게 접근할 수 있습니다. 예를 들어, float4를 4개의 요소를 가진 float 배열(즉, float[4])라고 본다면 x나 r은 첫 번째 요소를, y나 g는 두 번째 요소를, z나 b는 세 번째 요소를, w나 a는 네 번째 요소를 가리킵니다. 예를 들어서 위의 albedo에서 rgb값만을 가져오고 싶다면

float3 rgb = albedo.rgb;

라고 하시면 됩니다. 하지만 이에 그치지 않습니다. 이들 접미사의 순서를 마음대로 뒤섞어 새로운 벡터를 만들 수도 있습니다.예를 들어 r, g, b채널의 순서를 뒤바꾸고 싶다면

float4 newAlbedo = albedo.bgra;

이라고 하시면 됩니다. 심지어는 다음과 같이 r채널만 세 번 반복할 수도 있습니다.

float4 newAlbedo = albedo.rrra;

매우 멋지지 않나요? 이렇게 rgba나 xyzw를 이용해서 마음대로 순서를 바꿔가면서 벡터의 성분에 접근하는 것을 스위즐(swizzle)이라고 합니다.

자, 그러면 연습도 할 겸 스위즐을 이용해서 방금 만들었던 지구본의 빨강채널과 파랑채널을 뒤바꿔보는 것은 어떨까요? 누워서 떡 먹기죠? ^_^

선택사항: DirectX 프레임워크
이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시고자 하는 분들을 위한 선택적인 절입니다.

우선 '제2장: 진짜 쉬운 빨강쉐이더'에서 사용했었던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음, 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장합니다. Sphere.x와 TextureMapping.fx라는 파일이름을 사용하도록 하겠습니다. 이제 렌더몽키에서 사용했던 earth.jpg라는 텍스처 파일도 복사해옵니다. 이 파일은 렌더몽키의 설치디렉터리를 보시면 \Examples\Media\Textures 폴더 안에 있습니다.

우선 전역변수들을 살펴봅시다. '제2장: 진짜 쉬운 빨강쉐이더' 사용했던 쉐이더 변수의 이름이 gpColorShader였었군요. 이것을 gpTextureMappingShader로 바꿉시다.

// 쉐이더
LPD3DXEFFECT            gpTextureMappingShader = NULL;

지구 텍스처를 메모리에 저장할 때 사용할 텍스처 포인터도 하나 선언합니다.

// 텍스처
LPDIRECT3DTEXTURE9      gpEarthDM              = NULL;

이제 CleanUp() 함수로 가서 여기서 선언했던 D3D자원들을 해제하는 코드도 추가해야겠군요. 이래야 훌륭한 프로그래머이신 거 아시죠? gpColourShader의 이름을 변경하는 것도 잊지 맙시다.



     // 쉐이더를 release 한다.
     if ( gpTextureMappingShader )
     {
         gpTextureMappingShader->Release();
         gpTextureMappingShader = NULL;
     }


     // 텍스처를 release 한다.
     if ( gpEarthDM )
     {
         gpEarthDM->Release();
         gpEarthDM = NULL;
     }

이제 텍스처와 쉐이더를 로딩해 보겠습니다. 당연히 LoadAssets() 함수에서 로딩을 해야죠.

일단 쉐이더 변수의 이름과 쉐이더 파일의 이름을 각각 gpTextureMappingShader와 TextureMapping.fx로 변경합니다.

     // 쉐이더 로딩
     gpTextureMappingShader = LoadShader("TextureMapping.fx");
     if ( !gpTextureMappingShader )
     {
         return false;
     }

그리고 이전에 만들어 두었던 LoadTexture() 함수를 이용해서 earth.jpg 파일을 로딩합니다.

     // 텍스처 로딩
     gpEarthDM = LoadTexture("Earth.jpg");
     if ( !gpEarthDM )
     {
        return false;
     }

이제 렌더링을 담당하는 RenderScene() 함수를 살펴보도록 하죠. 일단 gpColorShader 변수명이 쓰이는 곳이 많네요. 이것을 모두 찾아 gpTextureMappingShader로 변경합시다.

텍스처매핑 쉐이더에서 새로 추가한 전역변수가 하나 있었죠? 바로 텍스처 샘플러입니다. 그런데 D3D 프레임워크에서 쉐이더에 텍스처를 대입해줄 때, 곧바로 샘플러에 대입해주는 게 아니라 텍스처 변수에 대입해줘야 합니다. 렌더몽키에서 DiffuseSampler말고 DiffuseMap이라고 불리던 텍스처가 있었죠? 이게 바로 텍스처 변수입니다. 그럼 DiffuseMap이란 이름의 쉐이더변수에 텍스처를 대입해주면 될 것 같죠? 사실 그래야 정상인데 렌더몽키가 자기 멋대로 텍스처 변수의 이름을 바꾸더군요. -_-;; TextureMapping.fx 파일을 메모장에서 열어보시면 아실 겁니다. 코드를 쭉 보다 보면 sampler2D DiffuseSampler바로 위에 texture 데이터형으로 선언된 변수가 하나 보일 겁니다? 자기 맘대로 _Tex 접미사를 붙여놨군요. 나쁜 원숭이 같으니라고...

texture DiffuseMap_Tex

뭐 불평해봐야 뭐 달라질게 있겠습니까? 그냥 이 변수명을 사용해서 텍스처를 대입해주도록 합시다. 쉐이더에 텍스처를 대입할 때는 SetTexture()함수를 사용합니다. 이 함수는 SetMatrix함수와 마찬가지로 쉐이더 내부의 변수명을 첫 번째 매개변수로 받습니다.

     gpTextureMappingShader->SetTexture("DiffuseMap_Tex", gpEarthDM);

자, 이제 프로그램을 컴파일 한 뒤 실행시켜 보세요. 렌더몽키에서 봤던 것과 동일한 결과를 보실 수 있죠? 근데, 이 지구가 천천히 회전하면 더 괜찮겠는걸요? 그럼 지구를 빙그르르 돌리는 코드를 추가해보죠.

일단, 현재 회전 값을 기억할 전역변수를 하나 추가합니다.

// 회전값
float gRotationY = 0.0f;

물체의 회전과 위치 등의 정보는 월드행렬의 일부가 됩니다. 따라서 RenderScene()함수로 다시 돌아와 월드행렬을 만드는 코드를 이렇게 바꾸겠습니다.

    // 프레임마다 0.4도씩 회전을 시킨다.
    gRotationY += 0.4f * PI / 180.0f;
    if ( gRotationY > 2 * PI )
    {
        gRotationY -= 2 * PI;
    }


     // 월드행렬을 만든다.
     D3DXMATRIXA16 matWorld;
     D3DXMatrixRotationY(&matWorld, gRotationY);

이 코드가 하는 일은 프레임마다 회전 각도를 0.02도씩 추가하고, 현재 회전 각도에 따라 회전행렬을 만들어서 그것을 월드행렬로 사용합니다. 현재 사용하시는 컴퓨터의 사양에 따라 이 회전 값이 너무 빠르거나 느릴 수도 있습니다. 본인의 컴퓨터에 맞게 적절히 값을 조정하세요. (실제 게임에서는 지난 프레임 이후 경과한 시간에 따라 회전량을 계산하는 게 옳은 방법입니다. 여기서 보여 드리는 코드는 쉐이더 데모를 위한 것이므로 그냥 이 정도로 놔두겠습니다.)

자, 이제 다시 코드를 실행해보면 자전을 하는 지구의 모습을 볼 수가 있죠?

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 텍스처매핑을 하려면 UV 좌표가 필요하다.
  • UV 좌표는 각 정점 상에 정의된 가변 값이다.
  • 픽셀쉐이더가 정점데이터를 이용하려면 정점쉐이더의 도움이 필요하다.
  • 정점쉐이더가 출력하는 값은 모두 보간기를 거친다.
  • tex2D함수를 이용하면 쉽게 텍스처를 샘플링할 수 있다.



고급쉐이더 기법 중에 텍스처매핑을 사용하지 않는 기법은 없다고 해도 과언이 아닐 정도로 텍스처매핑은 쉐이더 프로그래밍에 없어서는 안 될 존재입니다. 다행히도 HLSL에서 텍스처매핑이 그리 어렵진 않으니 잘 익혀두시기 바랍니다.

수고하셨습니다.


2011년 12월 16일 금요일

게임 출시전 개발자가 갖춰야할 마음가짐

아시는 분들은 아시겠지만 제가 2011년 9월에 스페이스 마린이란 게임을 출시했습니다. 다음은 이 게임을 마무리하는 도중 느꼈던 점을 아주 간단히 길게 정리해 놓은 글입니다. 게임을 마무리 지을 때 가져야할 프로그래머의 마음가짐, 아주 당연한 거라고 생각해왔는데 모르는 분들도 좀 계시더군요.

게임 출시전에 가져야 할 마음가짐이란 간단히 말해 조홀라~ 조심하는 겁니다. 아무래도 코드를 수정 할 때마다 새로운 버그를 만들 확률이 높아지거든요. '뭐~ 버그 좀 만드는 거야 어떻다고... 나중에 고치면 되지~' 하시는 분들도 있을텐데요. 이런 마음가짐은 게임 마무리 단계에서는 먹히지 않습니다. 게임 출시 직전에 모든 것을 테스트할 시간이 굉장히 부족하거든요. 특히 몇 년 동안 제대로 작동하던 기능들을 전부 다 테스트할 수 있는 여력은 없지요.

저는 주로 Xbox 360 및 PS3용 콘솔 게임을 개발합니다. 콘솔게임이 시중에 나오려면 반드시 콘솔 제작자(마이크로소프트 및 소니)가 정해 놓은 기술 테스트를 통과해야 하죠. 이 테스트를 신청한 시기부터 그 결과를 들을 때까지 걸리는 기간이 대략 1달입니다. 근데 만약 부주의하게 만든 버그 때문에 이 테스트에 실패하면 어떻게 될까요? 뭐, 버그 고쳐서 다시 테스트 신청을 합니다. 또 1달 기다리죠. 여러 번 버그를 만들면 어쩌죠? 뭐, 출시일이 지연되겠죠. 마케팅 하시는 분들 이미 돈 다 퍼다 부었는데....... 근데 이게 전부일까요? 아뇨... 테스트를 신청할 때마다 돈도 내야합니다. 얼마냐고요? 별로 안비쌉니다.  한 번에 몇 천만원 정도... -_- 왠만한 프로그래머 연봉 한 번에 날라가는 건 쉽죠. 자, 이제 문제점을 아셨나요? 출시날짜를 놓쳐서 돈 날리고 테스트 신청 하느라 또 돈 몇 번 날립니다. 이래도 별 문제가 아니라고 생각하신다면 여러분이 만든 버그때문에 테스트 실패할 때마다 봉급에서 몇 천 만원씩 까면......이제, '뭐~ 어떻다고?'하고 생각하시는 분은 없겠죠? -_-


그럼 제가 이번에 느꼈던 경험을 바탕으로 게임출시 전에 반드시 해야할 일과 하면 안되는 짓거리(?)를 간단히 설명드리겠습니다.

  • 고장난 것만 고친다: 위에서도 말씀드렸듯이 코드를 수정할 때마다 새로운 버그가 생길 확률이 높아집니다. 게임 출시직전에 제대로 작동하고 있는 코드를 괜히 쓸데없이 만져서 프로젝트 전체를 망칠 위험을 감수하는 건 바보같은 짓입니다.
  • 타인의 코딩 스타일을 자기 기호에 맞게 바꾸지 않는다: 코드를 이리 저리 옮기는 것, 빈 칸 4개를 탭 하나로 바꾸는 것, 한 줄로 길게 쓰인 코드를 보기 좋게 여러 줄로 나누는 것 등... 뭐 다 좋은 일인데... 이런 짓을 하다가 버그를 만드시는 분들이 꽤 됩니다. 아무리 훌륭한 프로그래머도 인간인지라 실수는 하기 마련입니다. (본인을 절대 실수 안한다고 생각하시는 분들 계시나요? 당신은 무지할 뿐입니다... -_-) 각 프로그래머가 1달에 한 번만 실수해서 버그 만들어도 프로그래머 30명을 가진 팀에서는 하루에 하나씩 버그가 나옵니다. 남의 코딩 스타일, 이딴 게 맘에 걸리시더라도 그걸 굳이 게임 출시전에 고칠 필요는 없습니다. 그냥 노트에 적어놨다가 다음 게임을 만들 때 고치세요. 또 한가지 말씀 드리고 싶은 것은 개인적으로 정말 맘에 안드는 코딩 스타일이 있는데 사내 코드베이스에 그런 스타일이 만연해 있으면 그건 사내 프로그래머들이 동의한 코딩 스탠다드일 가능성이 높습니다. 이거 맘에 안든다고 자기 맘대로 바꾸기 전에 본인이 사회부적응자는 아닌지 한 번 진지하게 고민해주는 센스... ㅇㅇ?
  • 게이머(또는 테스터)를 만족시킬 수 있는 것만 고친다. 프로그래머의 자기만족은 무시한다: 수학적으로 옳지 않은 게 보인다고요? 최종 사용자(게이머)가 신경 쓸만한 것이이 아니면 고치지 마세요. 그 흔히 쓰는 포토샵의 레이어 블렌딩조차 수학적으론 틀리다는 거 아시나요? 하지만 포토샵을 사용하는 아티스트들은 별로 신경도 안쓰죠. 마찬가지로 게이머들도 수학공식에는 크게 신경 않씁니다. (오히려 수학공식 매우 싫어할껄요? -_-) 수학적으로 옳아보겠다고 프로그래머 맘대로 뭔가를 수정하면 이로 인해 피해를 보는건 동료 개발자들 뿐입니다. 다음과 같은 상황을 생각해 보죠. "아티스트 아찌들~ 울 게임에서 수학적으로 틀린 게 있었어요. 그래서 제가 이렇게 올바르게 고쳤거든요.. 무핫핫~ 그래서 최종 조명 결과가 좀 달라보일테니... 아트들을 다 고쳐주세요! 지난 2년 동안 만들어 왔던 아트들 다 고칠 시간 있죠?..... 뭐 마감이라서 없다구요?!? 하... 하지만 이게 수학적으로 맞는건데... 좀 해욧!" 이딴 식의 주장을 하는 본인을 발견하신다면... 우선 남들의 업무를 존중하는 법부터 배우세요. 본인 만족을 위해서 수학공식 파는 것도 좋고 제가 상관하고픈 바도 아닌데... 남들에게 불합리한 피해를 끼친다면 그냥 퇴사하고 절에 들어가서 홀로 게임 만드시라고 권해 드리겠습니다.
  • 그래도 반드시 고쳐야할 것이 있다면 그로 인해 조금이나마 영향을 받을만한 다른 개발자들 모두의 허락을 받는다: 본인이 고쳐야한다고 생각하는 것과 동료 개발자들이 고쳐야 한다고 생각하는 것에는 차이가 있을 수도 있습니다. 그들이 고쳐야한다고 생각하는 것이 더 중요할 수도 있지요. 만약 그렇다면 다른 개발자들의 업무부담을 늘리는 버그수정은 차라리 안하고 넘어가는게 납니다.
이 위에 적은 이야기들... 사실 게임을 하나라도 출시해 본 프로그래머라면 다들 알고 있을 법한 상식이라 생각했습니다. 특히 콘솔게임을 출시해봤다면. 그런데 스페이스 마린을 마무리 하는 도중 이런 믿음이 깨진 일이 있었죠. 자칭 경력많은 콘솔 게임 프로그래머라고 하는 작자가 하루가 멀다하고 무수한 버그를 만들어 냈고, 제가 그걸 디버깅할 특권(?)을 부여받았더라죠. 근데...... 이 모든 버그들이 발생한 이유가 바로 이 몰상식한 놈이 게임 출시전에 갖춰야할 마음가짐을 몰랐기 때문이라지요..... 써글... -_-

제발 프로그래머 아찌들 부탁인데... 게임을 출시할 때만큼은 좀 책임감있게 코딩합시다... 네?




2011년 12월 13일 화요일

쉐이더 강의 지속여부 설문 결과

얼마전에 '포프의 쉐이더 입문강좌'를 출판해주겠다던 출판사가 있어서 블로그 글을 읽어주시는 분들께 설문을 했었습니다. 과연 블로그 연재를 중단하고 책으로 내는게 독자분들에게 좋은 일인지 아니면 블로그 연재를 마무리하는게 좋은지...

일단 결론부터 말씀드리면 블로그에 끝까지 연재합니다. 만약 출판사에서 그게 문제가 되어 출판을 못해주겠다면 출판은 포기할 생각입니다. (이 블로그 글을 올리고 나서 출판사에 이멜을 쓸 계획...) 나중에 제 나름대로 전자책을 작성해서 올리거나 연재 마친뒤에 출판사를 다시 찾아보던가 해야겠죠.

그럼 자세한 설문결과 입니다.



총 답변해주신 분이 65분이셨고 그 중에서 80%가 블로그 연재를 희망해주셨습니다. 종이책 출판을 선호해주신 17% 분들께는 죄송하지만 대다수가 원하는 대로 블로그 연재를 마무리 짓도록 하겠습니다. (다수결 원칙을 광신하는 편은 아니지만 많은 분들이 언제나 이 강좌를 볼 수 있도록 블로그에 공개한다고 생각해 주십시요.)

책 출판을 선호해주셨던 17% 분들을 위해서도 가능한 출판사에 이야기를 잘해서 책 출판이 가능하도록 노력해보겠습니다.


감사합니다.
포프..

----

p.s. 다음은 설문에 답해주신 분들이 달아주신 댓글들 입니다.

  • 포프선생님 최고!
  • 기초를 다진다는 개념으로 감사히 잘 읽고 있습니다. 꾸벅.
  • 전자책이면 어디서나 볼수 있어서 좋을것 같습니다. 블로그 링크도 하면 더 많은분들이 오실것 같고요.
  • 블로그 연재 와 전자책 출판을 동시에 하는건 어떨까요? 학생이나 저같은 그지는 블로그로 보고, 성질급하거나 바로 쉐이더를 익힐 필요가 있는 사람은 돈주고 사고...
  • 블로그 연재하고 전자책까지 내놨어도 강력하게 책으로 내고 싶다고 하는 출판사가 있다면 그 때 주시는건 어떠실지 싶습니다.
  • 블로그에 연재 후 전자책 출판이 좋을 것 같습니다. 제가 생각에는 애초에 책으로 내실 것을 그냥 블로그에 공개하신것은 무언가의 욕심(돈, 책출판)이라기 보다는 학생 또는 개발자들이 좀 더 쉽게 공부했으면 하는 의도로 생각합니다. 책으로 내면 가난한 학생들에게는 부담이 될 것이라고 생각되고요.. 블로그에 연재하고 전자책으로 출판해서 포프님 글로 도움받은 사람들이 여유되면 free donate 처럼 책을 구매하는 게 좋지 않을까 생각합니다. 물론 저는 전자책으로 내신다면 살 생각입니다. 블로그 글 잘 보고 있고요. ^^ 감사합니다.
  • 종이책과 전자책 동시에 출판 하시면 더 좋지 않을까요 ㅋ 전자책이 편한 사람도있겠지만 저는 개인적으로는 종이책이 편해요 ㅋ
  • 갠적으로 저능 한국에 전자책이 활성화 되기를 희망해요 :D 전자책 넘 편한데 ㅠ_ㅠ 종이책 소장에는 딱히 의미를 두지 않아서, 내용 보존에만 의미를 두는 편이여서 가지고 있는 책들도 e북으로 나온다면, 다 버리고 e북으로 갈아 타고 싶어영
  • 잘 보고 있습니다. 좋은 강좌 감사드립니다 :)
  • 이강좌가 끝나신뒤 실무자를위한 고급강좌도 있으면 좋겠습니다
  • 혹시 종이책으로 출판하는 쪽으로 의견이 모아지더라도 현재와 같은 문체가 좋아요. ^^
  • 가급적 문체의 수정이 적었으면 좋겠습니다.
  • 전 아이패드가 있어서 교제를 스캔해서 봅니다 매번 곤욕입니다 책을 사서 스캔을 떠서.... 비용이 두배 듭니다.. 대형 서점의 기득권 때문에 출판사들이 전자책 출판에 선듯 나서지 못하고 있다고 봅니다 ps. 수업 잘듣고 있습니다 :)
  • 예제가 좀 더 있었으면 합니다. 심화 예제 포함)
  • 전자책을 먼저 내시고 블로그에는 Q/A나 추가 설명 위주로 기록하는건 어떠세요? 공짜로 보기에는 아까운 컨텐츠라고 생각합니다
  • 개인적으로는 책에 돈 쓰는건 안 아까운데 입문자들도 알기 쉽게 잘 쓰셨기 때문에 블로그에 공개버전을 놔두셔서 더 많은 사람들이 보는게 어떨가 싶습니다. 근데 책을 안 보는 사람은 당연히 블로그 글도 안 볼까요? -_-;
  • 블로그 연재 후 전자책 출판을 선택한 것은 우선, 포프님의 친근한 어투를 그대로 볼 수 있는 것도 있구요. 내용을 업데이트 하신다거나 오탈자 교정 같은 것도 쉽게 할 수 있을 것 같아서요. 그리고 전자책 출판은 혹시나 블로그 데이터가 날아가더라도 볼 수 있게요.
  • 많은 사람들이 볼 수 있게 블로그로 연재 하는게 좋다고 생각 됍니다. 만약 출판을 한다면 전차책으로 출판하는것도 많은 사람들이 볼 수 있지 않을까 합니다.
  • 책이나온다면 당장이라도 사러 서점으로 달려갑니다!!!
  • 개인적으로 책을 보느게 더 기억에 오래남고 배우는것도 많지만 책만 봐도 잠이 슬슬오는 제 입장에서는 블로그가 좋네요 -0-; 웹툰 마냥 기다리는 재미도 있구요. 일하다 급 생각나면 찾아보기도 편하고 강좌보다가 다른 블로그 내용 보면서 "아~ 이런사람도 있구나~ 이 사람은 이런 생각하면서 사는구나~"  그런게 때로는 더 가치있더라구요 ^^
  • 글 잘 읽었습니다. 세이더가 어떤 것이고 어떻게 사용하는지 이해가 잘 되네요. 좋은 강의 입니다. 종이책, 블로그 연재 둘다 했으면 좋겠는데 그렇게 안되겠죠? 내용 및 설명이 훌륭하니 블로그 연재해도 전 책을 살 것 같습니다. 좋은 자료 감사합니다.
  • 블로그 연재 후 종이책 출판
  • 1.종이책보다 전자책이 보관도 편하고 보기도 편한거 같습니다. 2.블로그 연재후 출판하시면 아무래도 오자, 누락등의 피드백을 받기 좋으실거 같구요. 3.블로그 연재후 종이,전자 책으로 둘 다 출판되면 가장 좋을거 같습니다.
  • 종이 책으로 출판하면 향후 이북으로도 출판 될 수 있고, 저작권 종료시(아마도 절판) 개인적으로 블로그에 연재할 수 도 있으니 종이책으로 출판하시는 것이 좋지 않을까요?
  • 개인적으로는 종이책이 좋을거 같습니다. 블로그로 보는것도 좋지만 종이책이 확실히 눈에 잘들어오는 것 같네요. 그리고 쉐이더 강의 잘 보고 있습니다. 감사합니다
  • 종이책 이던 전자책이던 전 큰 상관이 없습니다. 다만, 출간을 목정으로 분량을 채워야 된다면은, 여타 나온 책들과 별차이 없는 내용 채우기와 어려운 내용. 그리고 개인 사이버공간(블로그)에서만 표현 할 수 있는 뭔가를 놓치지 않을까 싶습니다.
  • 블로그 연재를 계속 해주셨으면 합니다......


2011년 12월 12일 월요일

[포프의 쉐이더 입문강좌] 03. 텍스처매핑 Part 1

이전편 보기

샘플파일 받기

제3장 텍스처매핑

이 장에서 새로 배우는 HLSL

  • sampler2D - 텍스처에서 텍셀을 구해올 때 사용하는 샘플러 데이터형
  • tex2D() - 텍스처 샘플링에 사용하는 HLSL 함수
  • 스위즐(swizzle) - 벡터 성분의 순서를 마음대로 뒤섞을 수 있는 방법


저번 장에서 배운 내용 어떠셨나요? 너무 쉬었다고요? 실제 게임에서 별 쓸모가 없어 보인다고요? 네, 사실 그렇습니다. 저번 장의 주 목적은 실습을 통해 HLSL의 기초 문법을 배우는 것이었습니다. 보통 프로그래밍 책에서 헬로월드(hello world) 예제를 처음에 드는 것과 마찬가지 이치죠. 그럼 이번 장에서는 그보다 조금 더 쓸모가 있는 내용을 배워볼까요? 물체를 단색으로 출력하는 대신에 표면에 텍스처(texture)(3D 그래픽에서는 이미지를 사용하여 표면의 색감 및 질감(texture)을 표현합니다. 여기서 사용하는 이미지들을 텍스처라고 부릅니다.) 를 입혀보는 게 어떨까요? 이걸 보통 텍스처매핑(texture mapping)이라고 부른다는 것쯤은 다 아시죠?

텍스처매핑과 UV 좌표
3D 물체를 이루는 구성요소는 삼각형이라고 이전에 말씀드렸습니다. 정점 3개로 삼각형을 만들 수 있다는 것도요. 그렇다면 삼각형 위에 이미지를 입히려면 어떻게 해야 할까요? '이 삼각형의 왼쪽 꼭짓점에 저 이미지의 오른쪽 귀퉁이 픽셀을 출력할 것'과 같은 지시를 내릴 수 있어야겠죠? (이렇게 다른 두 점을 서로 대응시키는 것을 영어로 매핑(mapping)이라고 합니다.)삼각형은 이미 정점 3개로 이루어져 있으니 각 정점을 텍스처 위에 있는 한 픽셀에 대응시켜 주면 되겠군요. 그럼 텍스처 위에서 한 픽셀을 어떻게 가리킬까요? 텍스처란 결국 이미지 파일이니까 'x = 30, y = 101에 있는 픽셀'이라는 식으로 정의하면 될까요? 만약 이렇게 정의를 해버리면 나중에 이미지 파일의 크기를 2배로 늘리면 이것을 다시 x = 60, y = 202로 바꿔야겠네요. 별로 바람직하지 않죠?

저번 장에서 배운 내용을 떠올려보니 색상을 표현할 때도 비슷한 이야길 했던 것 같군요. 그 때, 채널의 비트 수에 상관없이 통일적으로 색상을 표현하려면 어떻게 해야 한다고 했죠? 모든 값을 백분율(0~1)로 표현한다고 했죠? 여기서도 똑같은 방법을 사용합니다.  x = 0이 텍스처의 제일 왼쪽 열을, x = 1은 제일 오른쪽 열을 나타낸다고 하면 되겠죠? 마찬가지로 y = 0은 텍스처의 제일 처음 행을, y = 1은 마지막 행을 나타냅니다. 참고로 텍스처매핑을 사용할 때는 XY대신에 UV를 사용하는 게 보통입니다. 특별한 이유는 없고 그냥 위치를 표현할 때 흔히 xy를 사용하니 그와 혼돈을 피하기 위해서 입니다. 이것을 그림으로 표현하면 다음과 같습니다.

그림 3.1. 텍스처의 UV 좌표


이제 다양한 UV 좌표를 대입하면 어떻게 결과가 달라지는지는 몇 가지 예를 들어보도록 하죠. 역시 그림으로 보면 이해가 쉽겠죠?

그림 3.2 다양한 텍스처매핑의 예


(a) 아직 텍스처를 입히지 않은 두 삼각형입니다. 정점 v0, v1, v2와 v0, v2, v3가 각각 삼각형을 하나씩 이루고 있군요.
(b) UV 좌표의 범위가 (0,0) ~ (1,1)입니다. 텍스처를 전부 다 보여줍니다.
(c) UV 좌표의 범위가 (0,0) ~ (0.5, 1)입니다. 따라서 텍스처의 왼쪽 절반만을 모여줍니다. 0.5가 백분율로는 50%니까 딱 중간인 거 맞죠?
(d) UV 좌표의 범위가 (0,0) ~ (0.5, 0.5) 이군요. 따라서 이미지의 왼쪽 절반과 위쪽 절반만을 보여줍니다.
(e) UV 좌표의 범위가 (0,0) ~ (1,2)니까 텍스처를 위아래로 두 번 반복을 해줘야겠네요. (UV 좌표가 0~1 범위 밖에 있을 때 이것을 처리하는 방법에는 여러 가지가 있습니다. 위에서 든 예는 랩(wrap, 반복) 모드를 사용할 때만 올바릅니다. 이 외에도 미러(mirror, 거울)라던가 클램프(clamp, 비반복) 모드도 있습니다.)
(f) UV 좌표의 범위가 (0,0) ~ (2,2)니까 텍스처를 위아래로 두 번, 그리고 좌우로 두 번 반복해줍니다. (역시 마찬가지로 랩모드에서만 올바른 예입니다.)

이 외에도 UV 좌표의 범위를 (1,0) ~ (0,1)로 하면 텍스처의 좌우를 뒤집는 등의 효과도 줄 수 있습니다. 이 정도면 어떻게 UV 좌표를 지정해야 원하는 결과를 얻을 수 있는 지 대충 아시겠죠? 이 정도면 실제로 텍스처매핑 쉐이더를 작성할 준비가 된 것 같군요.


기초설정

  1. '제2장: 진짜 쉬운 빨강쉐이더'에서 했던 것과 마찬가지로 렌더몽키 안에서 새로운 DirectX 이펙트를 만든 뒤, 정점쉐이더와 픽셀쉐이더 코드를 삭제합니다.
  2. 이제 쉐이더의 이름을 TextureMapping으로 바꿉니다.
  3. 정점의 위치를 변환할 때 사용할 gWorldMatrix, gViewMatrix, gProjectionMatrix를 추가하는 것도 잊지 맙시다. 변수 시맨틱을 이용해서 실제 데이터를 전달해 주는 방법도 기억하시죠?
  4. 이제 텍스처로 사용할 이미지를 추가할 차례입니다. TextureMapping 쉐이더에 오른쪽 마우스 버튼을 누른 뒤, Add Texture > Add 2D Texture > 렌더몽키 설치폴더\examples\media\textures\earth.jpg 파일을 선택합니다. Earth라는 이름의 텍스처가 추가되었을 겁니다.
  5. 이 텍스처의 이름을 DiffuseMap으로 변경합니다.
  6. 이제 Pass 0위에 마우스 오른쪽 버튼을 누른 뒤, Add Texture Object > DiffuseMap을 선택합니다. Texture0 이란 이름의 텍스처 개체가 추가되었죠?
  7. 이제 Texture0의 이름을 DiffuseSampler로 바꿉니다.
  8. 이 모든 설정을 마치셨다면 Workspace 패널이 다음 그림처럼 보일 겁니다.


그림 3.3. 기초설정을 마친 렌더몽키 프로젝트


정점쉐이더
일단 전체 소스코드부터 보여드린 뒤, 한 줄씩 차근차근 설명해드리겠습니다.

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float2 mTexCoord : TEXCOORD0;
};

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float2 mTexCoord : TEXCOORD0;
};

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


VS_OUTPUT vs_main(VS_INPUT Input)
{
   VS_OUTPUT Output;
 
   Output.mPosition = mul(Input.mPosition, gWorldMatrix);
   Output.mPosition = mul(Output.mPosition, gViewMatrix);
   Output.mPosition = mul(Output.mPosition, gProjectionMatrix);
 
   Output.mTexCoord = Input.mTexCoord;
 
   return Output;
}

정점쉐이더를 살펴보기 전에 텍스처매핑을 하려면 어떤 데이터가 새로 필요한지 생각해봅시다. 일단 당연히 텍스처로 사용할 이미지 하나가 필요하겠죠? 그렇다면 텍스처를 입히는 작업을 어디에서 해야 할까요? 정점쉐이더일까요? 아니면 픽셀쉐이더일까요? 각 쉐이더가 실행되는 시점을 생각해보면 이에 대한 대답을 쉽게 구할 수 있습니다. 정점쉐이더는 각 정점마다 실행이 된다고 말씀드렸었죠? 근데 텍스처는 어디에 입히죠? 정점에 입히는 게 아니라 표면을 구성하는 모든 픽셀에 입혀야 하죠? 따라서 정점쉐이더에서 하기엔 뭔가 부족할 듯 싶군요. 정점쉐이더와는 달리 픽셀쉐이더는 각 픽셀마다 호출이 되니까 당연히 픽셀쉐이더에서 텍스처매핑을 해야겠군요. 자, 그럼 이미지는 텍스처로 사용할 테니 정점쉐이더에서 선언해줄 필요가 없네요.

그럼 이 외에 다른 정보가 필요할까요? 바로 위에서 말씀드렸었는데 말이죠. 그렇습니다. UV 좌표가 필요하지요. UV 좌표를 어디에 지정했었죠? 각 정점마다였죠? 따라서 UV 좌표는 전역변수가 아니라 정점데이터의 일부로 전달됩니다. 자~ 그럼 이 점을 염두에 두고 정점쉐이더의 입출력 데이터를 살펴보도록 합시다.

정점쉐이더 입력데이터
'제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 입력데이터의 구조체를 일단 가져와보도록 하지요.

struct VS_INPUT
{
    float4 mPosition : POSITION;
};


자, 이제 여기에 UV 좌표를 추가해야겠죠? UV 좌표는 u하고 v로 나뉘니까 데이터형은 float2가 되겠네요. 그렇다면 어떤 시맨틱을 사용해야 할까요? 위치정보가 POSITION이라는 시맨틱을 가졌듯이 UV 좌표도 자신만의 시맨틱을 가지겠죠? TEXCOORD(텍스처좌표(texture coordinate)의 줄임말입니다.)란 시맨틱이 바로 그것입니다. UV 좌표 데이터를 삽입한 뒤의 정점쉐이더 입력데이터는 아래와 같습니다.

struct VS_INPUT
{
    float4 mPosition : POSITION;
    float2 mTexCoord : TEXCOORD0;
};

TEXCOORD뒤에 0을 붙인 이유는 HLSL에서 지원하는 TEXCOORD 수가 여럿이기 때문입니다. 쉐이더에서 여러 개의 텍스처를 동시에 사용할 때, 둘 이상의 UV 좌표를 사용할 경우가 있는데 그럴 때에는 TEXCOORD0, TEXCOORD1등으로 시맨틱을 사용하시면 됩니다.

정점쉐이더 출력데이터
우선 '제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 정점쉐이더 출력데이터를 가져와 봅시다.

struct VS_OUTPUT
{
    float4 mPosition : POSITION;
};

여기에 다른 정보를 추가해야 할까요? '제2장: 진짜 쉬운 빨강쉐이더'에서 설명해 드리지 않았던 내용 중 하나가 정점쉐이더는 위치정보 외에도 다른 정보들을 반환할 수 있다는 것입니다. 정점쉐이더가 위치정보를 반환하는 이유는 래스터라이저가 픽셀들을 찾아낼 수 있도록 하기 위해서였습니다. 하지만, 위치 이외의 다른 정보를 반환하는 이유는 래스터라이저를 위해서가 아닙니다. 이는 오히려 픽셀쉐이더를 위해서입니다. 텍스처매핑에 필요한 UV 좌표가 그 좋은 예입니다.

픽셀쉐이더는 정점 버퍼 데이터에 직접적으로 접근을 못 합니다. 따라서, 픽셀쉐이더에서 사용해야 할 정점데이터가 있다면(예, UV 좌표), 그 데이터는 정점쉐이더를 거쳐 픽셀쉐이더에 전달돼야 합니다. 좀 쓸데없는 제약 같다고요? 다음의 그림을 보시면 왜 이런 제약이 붙어 있는지를 알 수 있으실 것입니다.

그림 3.4. 과연 저 픽셀의 UV 좌표 값은 무엇일까?

UV 좌표가 정의된 장소는 각 정점입니다. 하지만 위 그림에서 볼 수 있듯이 픽셀의 UV 좌표는 정점의 UV 좌표와도 다른 것이 대부분입니다. (픽셀의 위치가 정점과 일치하는 경우에는 UV 좌표가 같습니다.) 따라서 이 픽셀의 올바른 UV 값을 구하는 방법은 현재 위치에서 세 정점까지의 거리를 구한 뒤 그 거리의 비율에 따라 세 UV 값을 혼합하는 것이겠지요. 하지만 이런 혼합을 직접해줄 필요는 없습니다. 정점쉐이더에서 출력한 위치 정보를 래스터라이저가 알아서 처리해줬듯이 정점 이외의 기타 정보는 보간기(interpolator)라는 장치가 알아서 혼합해줍니다. 그럼 '제1장: 쉐이더란 무엇이죠?'에서 보여드렸던 GPU 파이프라인에 보간기를 추가해보죠. 그림 3.5가 되겠습니다.

그림 3.5. 보간기까지 추가했지만 여전히 너무 간략한 3D 파이프라인


참고로 보간기가 보간(보간(interpolate)이란 단어가 이해 되시는 분들은 그냥 위에서 설명해 드렸다시피  '인접한 정점까지의 거리에 비례하여 값을 혼합하는 '이라고 이해하세요.)을 하는 것은 UV 좌표만이 아닙니다. 정점쉐이더가 반환하는 어떤 값이든 보간기는 보간을 해서 픽셀쉐이더에 전달해줍니다.

자, 그럼 이제 정점쉐이더에서 UV 좌표값도 반환해야 한다는 사실, 이해하시겠죠? 추가합시다.

struct VS_OUTPUT
{
    float4 mPosition : POSITION;
    float2 mTexCoord : TEXCOORD0;
};


전역변수
'제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 것 이외에 별도로 필요한 전역변수는 없습니다. 따라서 별다른 설명 없이 코드만 보여 드리겠습니다.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;

정점쉐이더 함수
누누이 말씀드리지만 정점쉐이더의 가장 중요한 임무는 정점의 위치를 투영공간으로 변환시키는 것입니다. 이 코드는 '제2장: 진짜 쉬운 빨강쉐이더'의 쉐이더에서 사용했던 것과 똑같습니다.

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldMatrix );
   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );

이제 UV 좌표를 전달해 줄 차례군요. Output 구조체에 UV 좌표를 대입하기 전에 공간변환을 적용해야 할까요? 그렇지 않습니다. UV 좌표는 여태까지 다뤘던 3차원 공간에 존재하는 게 아니라 삼각형의 표면상에 존재하기 때문입니다. 따라서 아무 변환 없이 UV 좌표를 전달해 줍니다.

   Output.mTexCoord = Input.mTexCoord;

더는 처리할 데이터가 떠오르지 않는군요. 이제 Output을 반환하면서 이 함수를 마무리 짓겠습니다.

   return Output;
}



2011년 12월 8일 목요일

특화된 H/W의 장단점. Xbox360의 EDRAM과 PS3의 ZCULL 메모리

콘솔게임 개발의 장점은 한 하드웨어에 특화된 최적화를 할 수 있다는 겁니다. 마이크로소프트나 소니 둘 다 자기네 하드웨어가 더 뛰어나다는 걸 보여주기 위해 조금씩 다른 기능들을 추가하곤 하는데요. 아무래도 그래픽스 프로그래머인 저는 그래픽 관련 쪽의 하드웨어를 주로 다루게 됩니다. 여기서 간단히 설명 드릴 건 Xbox 360의 EDRAM과 PS3의 ZCull 메모리에 대해... 요즘 Darksiders 2게임 최적화 해주다보니 나름 요놈들을 주물러 줄 일이 있어서......

Xbox 360의 EDRAM
일단 Xbox 360의 EDRAM은 렌더타겟 위에 픽셀을 뿌려주는 속도를 빠르게 하기 위한 놈입니다. 보통 렌더타겟 설정하고 메쉬들을 그리면 최종 결과가 렌더타겟하고 연계된 텍스처의 메모리로 들어가잖아요? 근데 bandwidth가 충분치 않아서 여기서 bottleneck이 걸리는 경우가 많습니다. Xbox 360는 이 문제를 해결하려고 EDRAM이란 것을 렌더링 파이프라인 젤 마지막에 설치해서 텍스처 메모리가 아닌 이 하드웨어 메모리에 모든 픽셀들을 그립니다. 즉 렌더타겟 아무리 설정하고 픽셀을 수만개를 그려봐야 최종 텍스처의 메모리에는 아무것도 안들어갑니다. 모든 건 고스란이 EDRAM안에 있죠. 이러면 아무래도 EDRAM 하드웨어 상에서 블렌딩이며 overdraw며 다 처리해서 확실히 빠릅니다. 이렇게 그릴거 다 그린 뒤에 이 EDRAM 안에 있는 최종결과를 텍스처로 옮겨갈 때 쓰는 명령어가 Resolve()입니다.  XNA 개발해보신 분들은 3.0버전인가부터 Resolve()를 직접 호출해줘야 하죠? 바로 Xbox 360하고 PC에서 공통으로 실행되는 API를 제공하기 위해서 그런 겁니다.

근데 문제는 Xbox 360에서는 언제나 EDRAM을 이용해야만 한다는 거죠. 물론 EDRAM을 사용해서 성능상 손해될 것은 없습니다. EDRAM을 사용하지 않는 것보다 성능이 나오지 않는 경우는 없으니까요. 정말 문제는 EDRAM의 크기가 10MB라는 건데요. 총 합이 10MB 를 넘는 렌더타겟들을 한 번에 사용하려면 그대로 뻗습니다......이게 바로 스페이스마린 Xbox 360의 해상도가 진정한 720P가 아닌 이유입니다. 진정한 720P는 1280 X 720 픽셀을 지원해야 하는데 이럴경우 저희 조명패스가 10MB 제한을 넘었거든요.

조명패스에서 사용하는 렌더타겟:

  • 렌더타겟
    • 포맷: A16R16G16B16
    • 바이트 / 픽셀: 8바이트
  • 깊이/스텐실 버퍼
    • D24S8
    • 바이트 / 픽셀: 4바이트
이걸 1280 X 720으로 메모리 사용량을 계산하면
  • 렌더타겟: 1280 X 720 X 8 = 7,372,800 바이트
  • 깊이/스텐실 버퍼: 1280 X 720 X 4 = 3,686,400 바이트
  • 총합: 11,059,200 바이트 = 10.54 MB (버럭! 0.54메가가 넘다니!)
물론 이런 문제를 해결하기 위해 Predicated Tiling이라는 방법을 사용할 수 있습니다. 하지만 이건 성능 상의 문제도 있고 PS3나 PC 등의 다른 플랫폼도 동시에 지원하는 게임에서 한쪽 플랫폼에만 특화된 코드를 개발하는 일에 인력을 투입하는 것도 좀 돈낭비란 생각이 들어서.... 그냥 저희 멋대로 좌우 40픽셀씩만 잘라냈습니다. 그래서 스페이스마린 Xbox 360 버전의 해상도는 1200 X 720입니다.... -_-..... 이렇게 해서 다시 메모리 사용량을 계산하면 10MB가 조금 안됩니다.
  • 렌더타겟: 1200 X 720 X 8 = 6,912,000바이트
  • 깊이/스텐실 버퍼: 1200 X 720 X 4 = 3,456,000 바이트
  • 총합: 10,368,000 바이트 = 9.89 MB
하지만 최종 TV에 등장하는 해상도는 여전히 1280 x 720입니다. 내부적으로 모든 렌더링을 1200 X 720으로 한 뒤 마지막 단계에서 그냥 1280 x 720으로 업샘플(upsample) 해줍니다... Xbox 360 하드웨어 자체에서 업샘플링을 지원해 주는걸로 기억합니다...품질도 괜찮은 편이고요.

PS3의 ZCULL 메모리
PS3에는 EDRAM같은 건 없습니다. 따라서 Xbox 360에 비해 렌더타겟에 픽셀들을 뿌려주는게 확실히 느립니다. 그 대신 PS3에는 Xbox 360에는 없는 ZCull이라는게 있는데요. ZCull은 계층적 깊이버퍼(Hierarchical Depth Buffer)와 하는 일이 비슷합니다. 보통 깊이 테스트를 통한 픽셀 rejection은 픽셀쉐이더가 실행된 뒤에 하거든요. 그래서 깊이 테스트에 실패하던 말던 픽셀쉐이더를 돌리느라 GPU 사이클을 낭비하곤 하지요.  '깊이테스트에 실패할거면 차라리 픽셀쉐이더조차 돌리지 말자'라는 개념으로 ATI가 추가한게 Hi-Z(계층적깊이)구요. 아마 이게 특허가 걸려있어서 NVidia에서는 그대신 ZCull을 추가하지 않았나 싶습니다. 참고로 Xbox 360에 들어가는 GPU가 ATI, PS3에 들어가는 GPU가 NVidia입니다.

사실 ZCull과 EDRAM은 하는 짓 자체가 전혀 틀린데 그래도 공통점 하나가 있습니다. ZCull도 하드웨어 자체에 붙어있는 메모리가 있죠. 물론 당연 그에 따라 제약도 있을거고.... 물론 EDRAM처럼 렌더타겟의 비트수만큼의 MB를 잡아먹진 않습니다. 그대신 자체적으로 깊이만 판단하면 되니까 그냥 어느정도 해상도까지만 지원해주죠. 이 어느정도 해상도라는게 대략 2048 x 1536입니다. 이정도면 사실 왠만해선 충분한 해상도지만 Cascade Shadow Map 기법에서 cascade를 렌더타겟 하나에 뭉쳐놓으려고 할 때 문제가 생기곤 하죠. 각 cascade가 1024 x 1024고 총 4개의 cascade를 사용한다면 렌더타겟의 크기가 2048 x 2048이 될테니까요.

물론 ZCull이 지원하는 해상도를 넘는 렌더타겟을 사용해도 최종결과는 동일합니다. 즉 뻗지 않습니다.... -_-..... Xbox 360의 EDRAM과는 달리 다 제대로 돌고 결과도 제대론데 문제는 성능이 개떡이 된다는 거죠. PS3의 픽셀쉐이더 속도가 Xbox 360에 비해 느린 것이 보통이라 이 ZCull이 제대로 작동되냐 마냐에 따라 프레임수가 확 차이가 납니다. 소니측에서도 성능향상 방법 1번으로 꼽는게 'ZCull 잘 주물러주기'입니다. 물론 단지 해상도뿐만이 아니라 다른 조건들이 맞아야만 ZCull이 활성화되서 좀 더 다루기 까다로운 것도 있지만..... 역시 PS3는 참 까탈스럽습니다.. ㅎ

어쨌든 이 해상도 꾸겨 맞출려고, 한 때는 cascade의 크기를 768 x 768로 제한했었고... (이러면 카스케이드 4개를 렌더타겟 하나에 뭉쳐도 1536 x 1536이니까요.) 나중에는 차라리 Deferred Shadow라는 기법을 이용해서 이 제한을 피해갔습니다. (물론 디퍼드 샤도우를 사용한 이유는 인접 카스케이드 맵의 혼합을 통한 그림자 품질개선이 주 목적이었습니다.)


대충 제 생각/소망
ZCull이나 EDRAM이나 모두 훌륭한 아이디어임에는 분명합니다. 이거 없었으면 저희 게임에서 이 정도로 성능뽑아줄 수도 없었고요.

다만 EDRAM이 ZCull처럼 해상도에 대해 좀 너그러웠으면 하는 바램이 있습니다. 해상도 지원안되는거면 그냥 성능을 줄여도 좋으니 화면에는 보이는 결과는 옳게? 저희처럼 수백만 달라 들어가는 게임이 아니라면 그 정도 성능이 필요할리가 없거든요. (아님 EDRAM사이즈를 엄청 크게 줘서 콘솔 제조원가를 올린뒤, 그걸 게이머들이 내게 하거나?.... 뭔가 이 옵션은.... 안될거 같죠?  -_- )

그리고 PS3쪽에 바라는 건 ZCull의 까탈을 좀 줄여주거나.... 까탈을 부리고 싶으면 제대로 알려주기라도 하라는... 소니에서 제공하는 프로파일러에서 캡춰하지 않는한 ZCull이 도는지 아닌지 알수가 없어요... 잘 돌던게 다른 프로그래머 실수로 갑자기 고장나도 몇 달 지나서야 발견하고.... (아님 이 해상도 제한을 높이고 콘솔기기를 비싼 값에 팔던가.....? 역시 돈이 문제입니다... -_-  )

2011년 12월 6일 화요일

[포프의 쉐이더 입문강좌] 02. 진짜 쉬운 빨강쉐이더 Part 2

이전편 보기

픽셀쉐이더
자, 이제 픽셀쉐이더를 작성해 볼 차례입니다. 정점쉐이더에서 했던 것과 마찬가지로 렌더몽키의 Workspace에서 Pixel Shader를 찾아 더블클릭합니다. 그리고 그 안에 있는 코드를 모두 지웁니다. 실제로 코드를 한 줄씩 쳐보셔야 실력이 늡니다. ^^ 꼭 코드를 다 지우세요.

이제 정점쉐이더에서 그랬던 거와 마찬가지로 전체코드를 보여드린 뒤, 한 줄씩 살펴보기로 하죠.

float4 ps_main() : COLOR
{   
   return float4( 1.0f, 0.0f, 0.0f, 1.0f );
}


픽셀쉐이더의 가장 중요한 임무는 픽셀의 색을 반환하는 것입니다. 현재 저희가 만드는 쉐이더기 빨강쉐이더니까 그냥 빨간색을 반환하면 되겠죠? 그렇다면 빨간색을 RGB값으로 어떻게 표현할까요? RGB(255, 0, 0)이 제일 먼저 떠오르시나요? 흠... 그렇다면 픽셀쉐이더 코드를 작성하기 전에 다음 절을 먼저 보셔야겠습니다.

색의 표현방법
빨간색을 RGB로 표현하라고 하면 (255, 0, 0)을 먼저 떠올리시는 이유는 RGB의 각 채널을 8비트로 저장하는 경우가 대부분이기 때문입니다. 8비트를 정수로 표현하면 총 256개의 값(2∧8 = 256)을 표현할 수 있습니다. 이 값을 0부터 시작하면 0 ~ 255가 되므로 각 채널의 최대값이 255이 되는 거지요. 근데 8비트가 아니라 5비트로 각 채널을 표현하면 어떻게 될까요? 2∧5 = 32이므로 31이 최대 값이 되지요. 따라서 8비트 이미지에서 빨간색은 (255, 0, 0) 이지만 5비트 이미지에서의 빨간색은 (31, 0, 0)이라는 찹찹한 결과가 생기고 마네요?

그러면 비트 수에 상관없이 통일적으로 색을 표현할 방법은 없을까요? 아마 포토샵에서 HDR 이미지를 다뤄보신 분들이라면 이미 그 답을 알고 계실 듯 하네요. 바로 백분율(%)을 사용하면 되지요. 백분율을 사용하면 비트 수에 상관없이 빨간색의 RGB값이 언제나 (100%, 0%, 0%)가 됩니다. 이게 바로 쉐이더에서 색상을 표현할 때 사용하는 방법입니다. 백분율을 그냥 유리수로 나타내면 0.0 ~ 1.0이 되니까 쉐이더에서 빨간색의 RGB값은 (1.0, 0.0, 0.0)이 됩니다.

픽셀쉐이더 함수
자, 그럼 이젠 어떤 RGB 값을 반환해야 할지도 알아봤으니 픽셀쉐이더 함수를 작성할 일만 남았군요. 픽셀쉐이더 함수의 헤더부터 살펴봅시다.

float4 ps_main() : COLOR
{

이 헤더가 의미하는 바는 다음과 같습니다.

  • 이 함수의 이름은 ps_main이다.
  • 이 함수는 매개변수를 받지 않는다.
  • 이 함수의 반환형은 float4이다.
  • 이 함수의 반환 값을 백 버퍼의 색상(COLOR)값으로 처리할 것.


여기서 딱히 추가로 설명해 드릴 부분은 반환 값의 데이터형으로 float3가 아니라 float4를 쓴다는 정도입니다. 4번째 값은 알파 채널로 보통 투명효과를 나타내는 용도로 쓰이곤 합니다. (이 값이 1이면 완전 불투명, 0이면 완전 투명입니다.)

자, 그럼 이 함수 안에선 무슨 일을 해야 했었죠? 그렇죠. 빨간색을 반환해야죠. 이렇게 코드를 짜면 됩니다.

   return float4( 1.0f, 0.0f, 0.0f, 1.0f );
}

여기서 특별히 설명드릴 것은 float4(r, g, b, a)라는 형태로 float4 벡터를 새로 생성한다는 것과 알파 채널의 값이 1.0(100%)이므로 픽셀이 완전히 불투명 하다는 정도 입니다. 이제 쉐이더 편집기 안에서 F5키를 눌러 정점쉐이더와 픽셀쉐이더를 각각 컴파일 하면 미리 보기 창에서 다음과 같은 빨간색 공을 보실 수 있을 겁니다.

팁: 렌더몽키에서 쉐이더를 컴파일 하는 법
렌더몽키에서는 정점쉐이더와 픽셀쉐이더를 별도로 컴파일 해줘야 합니다. 편집기에서 각 쉐이더를 불러온 뒤 F5를 눌러주세요. 미리 보기 창이 열릴 때도 두 쉐이더가 모두 컴파일 됩니다.

그림 2.7. 처음으로 만들어본 빨강쉐이더!


정말 간단한 쉐이더였죠? 여기서 빨간색 대신 파란색을 보여주려면 어째야 할까요? float4(0.0, 0.0, 1.0, 1.0)을 반환하면 되겠죠? 노란색은요? 노란색은 연두색과 빨간색을 섞은 거니까.... 음.... 제가 굳이 답을 알려드리지 않아도 아시죠?

이제 이 렌더몽키 프로젝트를 잘 저장해 두세요. 각 장이 끝날 때마다 렌더몽키 프로젝트를 저장해 두시기 바랍니다. 나중에 다른 장에서 다시 이용할 거거든요.

선택사항: DirectX 프레임워크
이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시고자 하는 분들을 위한 선택적인 절입니다.

우선 '제1장: 쉐이더란 무엇이죠?'에서 만들었던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 각 장마다 프레임워크를 따로 저장하는 이유는 다른 장에서 이 프레임워크를 가져다가 코드를 추가할 예정이기 때문입니다.

다음은 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장할 차례입니다.


  1. Workspace 패널에서 ColorShader를 찾아 오른쪽 마우스 버튼을 누릅니다.
  2. 팝업메뉴에서 Export > FX Exporter를 선택합니다.
  3. 위에서 DirectX 프레임워크를 저장했던 폴더를 찾아 그 안에 ColorShader.fx란 이름으로 파일을 저장합니다.
  4. 이제 Workspace 패널에서 Model을 찾아 오른쪽 마우스 버튼을 누릅니다.
  5. 팝업메뉴에서 Save > Geometry Saver를 선택합니다.
  6. 역시 DirectX 프레임워크가 있는 폴더 안에 Sphere.x란 이름으로 파일을 저장합니다.


이제 비주얼 C++ 에서 프레임워크의 솔루션 파일을 연 뒤, 다음의 코드들을 차례대로 추가해보도록 하죠. ShaderFramework.cpp 파일을 열겠습니다.

우선, 투영행렬을 만들 때 필요한 상수들을 #define으로 정의하겠습니다.

#define PI           3.14159265f
#define FOV          (PI/4.0f)                     // 시야각
#define ASPECT_RATIO (WIN_WIDTH/(float)WIN_HEIGHT) // 화면의 종횡비
#define NEAR_PLANE   1                             // 근접 평면
#define FAR_PLANE    10000                         // 원거리 평면

이제 Sphere.x하고 ColorShader.fx 파일을 로딩해서 메모리에 저장해둘 때 사용할 포인터 2개를 선언합니다.

// 모델
LPD3DXMESH        gpSphere        = NULL;


// 쉐이더
LPD3DXEFFECT      gpColorShader   = NULL;

이제 모델과 쉐이더 파일을 로딩해야겠죠? '제1장: 쉐이더란 무엇이죠?'에서 속을 비워두었던 LoadAssets()함수 안에 다음의 코드를 추가할 때로군요.

    // 쉐이더 로딩
    gpColorShader = LoadShader("ColorShader.fx");
    if ( !gpColorShader )
    {
        return false;
    }


    // 모델 로딩
    gpSphere = LoadModel("sphere.x");
    if ( !gpSphere )
    {
        return false;
    }

위 코드는 '제1장: 쉐이더란 무엇이죠?'에서 미리 구현해 두었던 LoadShader() 함수와 LoadModel() 함수를 호출해서 파일들을 로딩한 뒤, 그 중에 하나라도 NULL 포인터이면 로딩에 실패했다는 의미로 false를 반환합니다. 이렇게 로딩에 실패한 경우 비주얼 C++의 출력 창에 에러메시지가 있을 테니 살펴보시기 바랍니다.

새로운 D3D 자원을 로딩할 때 마다 이를 해제하는 코드를 추가하는 습관을 기르도록 합시다. GPU 상의 메모리 누수를 막기 위해서입니다. CleanUp() 함수에서 D3D를 해제하기 바로 전에 다음의 코드를 삽입하겠습니다.

    // 모델을 release 한다.
    if ( gpSphere )
    {
        gpSphere->Release();
        gpSphere = NULL;
    }


    // 쉐이더를 release 한다.
    if ( gpColorShader )
    {
        gpColorShader->Release();
        gpColorShader = NULL;
    }


이제 사전작업은 모두 끝났으니 마지막으로 쉐이더를 이용해서 물체를 그리기만 하면 됩니다. 3D 물체를 그리는 코드는 RenderScene()에 넣기로 했었죠? RenderScene() 함수로 갑니다.

// 3D 물체 등을 그린다.
void RenderScene()
{

쉐이더 안에서 전역변수들을 사용했던 것 기억하시나요? 렌더몽키에서는 변수 시맨틱을 통해 이 값들을 대입해줬지만 여기서는 직접 이 값들을 만들어서 쉐이더에 전달해 줘야 합니다. 우선 뷰행렬부터 만들어 볼까요?

    // 뷰 행렬을 만든다.
    D3DXMATRIXA16 matView;
    D3DXVECTOR3 vEyePt(    0.0f, 0.0f, -200.0f ); 
    D3DXVECTOR3 vLookatPt( 0.0f, 0.0f,  0.0f );
    D3DXVECTOR3 vUpVec(    0.0f, 1.0f,  0.0f );
    D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec );

위에서 볼 수 있듯이 카메라의 현재 위치와 카메라가 바라보는 곳의 위치, 그리고 카메라의 위쪽을 가리키는 벡터만 있으면 D3DXMatrixLookAtLH() 함수를 호출하여 뷰행렬을 만들 수 있습니다. 여기서는 카메라가 현재 (0, 0, -200)에 위치해 있고 (0, 0, 0)을 바라보고 있다고 가정합니다. 실제 게임에서는 카메라 클래스로부터 이 정보를 가져와서 뷰행렬을 만드는 것이 정석입니다.

다음은 투영행렬을 만들 차례입니다. 투영행렬은 원근투시법(perspective projection)을 사용하느냐 직교투시법(orthogonal projection)을 사용하느냐에 따라 사용할 함수와 매개변수들이 달라집니다. 여기서는 원근투시법을 사용하므로 D3DXMatrixPerspectiveFOVLH() 함수를 사용하겠습니다. (직교투시법을 사용할 때는 D3DXMatrixOrthoLH를 사용하세요.)

    // 투영행렬을 만든다.
    D3DXMATRIXA16 matProjection;
    D3DXMatrixPerspectiveFovLH( &matProjection, FOV, ASPECT_RATIO, NEAR_PLANE,
        FAR_PLANE );

이제 월드행렬을 만들어 보겠습니다. 사실 월드행렬은 한 물체의 위치와 방위, 그리고 확장/축소 변환을 합친 것입니다. 따라서 뷰행렬 및 투영행렬과 달리 각 물체마다 월드행렬을 만들어줘야 합니다. 본 예제에서는 월드의 원점(0, 0, 0)에 물체를 놓아둔다고 가정하므로 월드행렬을 그냥 단위행렬(identity matrix)로 놔두겠습니다.

    // 월드행렬을 만든다.
    D3DXMATRIXA16 matWorld;
    D3DXMatrixIdentity(&matWorld);

쉐이더에서 사용할 전역변수 3개를 전부 다 만들었으니 이제 이 값들을 쉐이더에 전달해줘야 겠군요. 이 때 쉐이더의 SetMatrix함수를 이용하면 이런 일을 쉽게 할 수 있습니다. SetMatrix의 첫 번째 인수는 쉐이더 안에서 사용하는 변수의 이름이고, 두 번째 변수는 위에서 정의한 D3DXMATRIXA16형의 변수입니다.

    // 쉐이더 전역변수들을 설정
    gpColorShader->SetMatrix("gWorldMatrix", &matWorld);
    gpColorShader->SetMatrix("gViewMatrix",  &matView);
    gpColorShader->SetMatrix("gProjectionMatrix",  &matProjection);

쉐이더에 필요한 변수들의 값을 모두 전달해줬다면 이제 GPU에게 명령을 내릴 차례입니다. '앞으로 그릴 모든 물체들에 이 쉐이더들을 적용할 것'이라는 명령을 말입니다. 이런 명령은 쉐이더의 Begin() / BeginPass()와 EndPass() / End() 함수호출로 내립니다. BeginPass()와 EndPass()가 구성하는 블럭 안에 물체를 그리는 함수를 넣으면 물체가 그려질 때 이 쉐이더가 사용되죠. 우선 아래의 코드를 보시죠.


    // 쉐이더를 시작한다.
    UINT numPasses = 0;
    gpColorShader->Begin(&numPasses, NULL);
    {
        for (UINT i = 0; i < numPasses; ++i )
        {
            gpColorShader->BeginPass(i);
            {
                // 구체를 그린다.
                gpSphere->DrawSubset(0);
            }
            gpColorShader->EndPass();
        }
    }
    gpColorShader->End();
}


DrawSubset() 호출이 BeginPass() / EndPass() 안에 있고, 이는 다시 Begin() / End() 호출 안에 있는 거 보이시죠? 이렇게 하면 GPU가 gpColorShader 쉐이더를 이용해서 gpSphere 물체를 그릴 것입니다.

위의 코드를 보시면 쉐이더에서 Begin() 함수를 호출 하고 난 뒤에 다시 BeginPass()를 호출하는 거 보이시죠? 가끔 패스(pass)를 보고 '아니, 쉐이더는 알겠는데 그 안에 들어있는 패스는 또 뭐여?'라고 혼돈스러워하는 학생들을 본 적이 있는데 크게 신경 쓰지 않으셔도 됩니다. 패스는 다양한 쉐이더를 이용하여 동일한 물체를 여러 번 그릴 때 유용하지만 실무에서 둘 이상의 패스를 쓰는 경우가 별로 없으니 그냥 무시하세요. 그냥 Begin() 함수를 호출할 때, numPasses 변수의 주소를 전달하여 쉐이더 안에 들어있는 패스의 수(대부분의 경우 1)를 구해온다는 정도만 아시면 됩니다. 만약 2개 이상의 패스가 존재한다면 정점/픽셀쉐이더 쌍도 둘 이상이 존재한다는 거니까 그 수만큼 BeginPass()/EndPass()를 호출하면서 여러 번 물체를 그려주면 되는 거죠.

이제 코드를 컴파일 한 뒤 프로그램을 실행하면 아까 렌더몽키에서 봤던 것과 동일한 결과를 보실 수 있습니다.

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 각 정점마다 변하는 값은 정점데이터의 멤버변수로 받는다.
  • 모든 정점에 공통적으로 사용되는 값은 전역변수로 받는다.
  • HLSL은 벡터연산에 간편히 사용할 수 있는 float4, float4x4 등의 데이터형을 제공한다.
  • 정점의 공간을 변환할 때는 행렬 곱을 사용한다. HLSL에서 제공하는 내장함수 mul()을 사용하면 손쉽게 행렬과 벡터를 곱할 수 있다.
  • HLSL에서 색상을 표현할 때는 0 ~1 사이로 정규화한 값을 사용한다.



이 장에서 배운 내용은 정말 기초 중의 기초입니다. 이렇게 간단한 쉐이더를 혼자서도 뚝딱 작성하실 정도로 쉐이더 문법의 기본이 되어야 나중에 다른 복잡한 쉐이더도 쉽게 작성하실 수 있습니다. 제가 강의를 할 때, 이 빨갱이 쉐이더가 너무 쉽다고 눈으로만 대충 훑어보고 넘어간 일부 학생들이 나중에 다른 쉐이더에서 고생하는 경우를 종종 봤습니다. 쉐이더 자체가 어려워서가 아니라 아주 기초적인 HLSL 문법조차도 제대로 숙지하지 않았기 때문이었습니다. 다음 장으로 가시기 전에 반드시 빨강쉐이더 정도는 직접 작성하실 수 있을 정도로 한두 번 연습을 해두시기 바랍니다.


[포프의 쉐이더 입문강좌] 02. 진짜 쉬운 빨강쉐이더 Part 1

이전편 보기

샘플파일 받기

제2장 진짜 쉬운 빨강쉐이더

이 장에서 새로 배우는 HLSL

  • float4: 4개의 성분을 가지는 벡터 데이터형
  • float4x4: 4 X 4 행렬 데이터형
  • mul(): 곱하기 함수. 거의 모든 데이터형을 변수로 받음.
  • POSITION: 정점위치 시맨틱. 정점데이터 중 위치정보만을 불러옴.


이 장에서 새로 사용하는 수학

  • 3D 공간변환 - 행렬 곱을 이용함.



'제1장: 쉐이더란 무엇이죠?'에서 쉐이더란 픽셀의 위치와 색을 계산하는 함수라고 말씀드렸습니다. 그렇다면 이번 장에서는 실제로 픽셀의 위치와 색을 계산하는 쉐이더를 만들어봐야겠죠? 처음 쉐이더를 짜보시는 분들도 쉽게 이해하실 수 있게끔 매우 간단한 쉐이더 프로그램을 만들어 보겠습니다. 우선 렌더몽키에서 빨간색 공을 그리는 쉐이더를 작성해보면서 HLSL 문법을 처음으로 접해보는 게 좋겠군요! (이렇게 단색을 출력하는 쉐이더는 디버깅을 할 때도 유용하게 쓰입니다.) 렌더몽키에서 쉐이더를 작성하면 그 결과를 .fx 파일로 익스포트(export)해서 이걸 DirectX 프레임워크에 그대로 가져다 쓸 수도 있습니다.

기초설정
다음의 단계를 따라서 기초적인 설정을 마무리합시다.


  1. 렌더몽키를 시작합니다. 무서운(?) 원숭이 얼굴이 잠시 스쳐 지나간 뒤에 빈 작업공간(workspace)가 등장할 것입니다. 
  2. Workspace 패널 안에서 Effect Workspace위에 마우스 오른쪽 버튼을 누릅니다. 팝업 메뉴가 등장할 겁니다.
  3. 팝업메뉴에서 Add Default Effect > DirectX > DirectX를 선택합니다. 이제 미리 보기(preview) 창에 빨간색 공 하나가 보이죠?
  4. Workspace패널에 Deafult_DirectX_Effect라는 새로운 쉐이더도 추가되었을 것입니다. 쉐이더의 이름을 ColorShader로 바꿉니다.
  5. 이제 화면이 아래와 같을 것입니다.


그림 2.1. 기초설정을 마친 렌더몽키 프로젝트


정점쉐이더
이제 ColorShader옆에 있는 더하기(+) 표시를 누릅니다. 제일 아래쪽에 Pass 0이 보이시죠? 그 옆에 있는 더하기 표시를 다시 누르세요. 이제 Vertex Shader를 더블클릭하시면 오른쪽 쉐이더 편집기 안에 Vertex Shader코드가 등장할 겁니다. 사실 여기에 들어있는 코드가 이미 빨간 공을 그리고 있지만 저희는 한 줄씩 연습을 해봐야 하니 이 속에 있는 코드를 모두 지우겠습니다.

코드를 다 지우셨나요? 그렇다면 이제 본격적으로 시작해보죠! 우선 한 눈에 보실 수 있게끔 정점쉐이더 코드를 전부 보여드린 뒤 한 줄씩 설명해 나가도록 하겠습니다.

struct VS_INPUT 
{
   float4 mPosition : POSITION;
};

struct VS_OUTPUT 
{
   float4 mPosition : POSITION;
};

float4x4 gWorldMatrix;         
float4x4 gViewMatrix;          
float4x4 gProjectionMatrix;    


VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldMatrix );
   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );
   
   return Output;
}



전역변수 vs 정점데이터
쉐이더에서 사용할 수 있는 입력 값으로는 전역변수와 정점데이터가 있습니다. 이 둘을 구분 짓는 기준은 한 물체를 구성하는 모든 정점이 동일한 값을 사용하느냐의 여부입니다. 만약 동일한 값을 사용한다면 이것은 전역변수가 될 수 있지만 각 정점마다 다른 값을 사용한다면 당연히 전역변수는 안되겠지요. 그 대신 정점 버퍼, 즉 정점 데이터의 일부로 이 값을 받아들여야 합니다.

전역변수의 예로는 월드행렬, 카메라의 위치 등이 있고, 정점데이터 변수의 예로는 정점의 위치, UV좌표 등이 있습니다.

정점쉐이더 입력데이터
우선 정점쉐이더에서 입력으로 받을 데이터들을 VS_INPUT이라는 구조체로 선언해보겠습니다.

struct VS_INPUT
{
    float4 mPosition : POSITION;
};

'제1장: 쉐이더란 무엇이죠?'에서 정점쉐이더의 가장 중요한 임무는 각 정점의 위치를 공간 변환하는 것이라고 했던 거 기억하시나요? 그러기 위해서는 정점의 위치를 입력으로 받아야 하죠? 그게 바로 위 구조체가 멤버변수 mPosition을 통해 정점의 위치를 얻어오는 이유입니다. 이 변수가 DirectX의 정점버퍼(버텍스 버퍼(vertex buffer)라고도 합니다.)로부터 위치정보를 구해올 수 있는 이유는 POSITION이라는 시맨틱(semantic, 태그(tag) 정도로 이해하시는 게 편할 겁니다.)  때문입니다. 정점버퍼에는 정점의 위치, UV좌표, 법선 등을 비롯한 다양한 정보가 담겨 있을 수 있는데 이 중에서 필요한 정보만을 쏙쏙 빼오는 것을 시맨틱이라고 해두죠.

따라서 float4 mPosition : POSITION; 이라는 코드는 '정점데이터에서 위치(POSITION) 정보를 가져와서 mPosition에 대입해라!'라는 명령입니다.

아차! 그렇다면 float4는 뭘까요? 이건 변수의 데이터형입니다. float4는 HLSL자체에서 지원하는 데이터형의 하나로 4개의 성분(x ,y, z, w)을 가지는 벡터입니다. 각 성분은 부동소수점(floating-point)형 입니다. HLSL은 float4외에도 float, float2, float3 등의 데이터형을 지원합니다. (참고로 GPU는 부동소수점 벡터를 처리하는데 최적화된 장치입니다. 따라서 쉐이더에서 사용하는 기본적인 데이터형은 정수가 아닌 부동소수점입니다. 정수는 오히려 쉐이더의 성능을 저하시키는 요인입니다.)

정점쉐이더 출력데이터
정점쉐이더의 입력데이터를 선언해봤으니 이제 출력데이터를 살펴봐야겠죠? '제1장: 쉐이더란 무엇이죠?'에서 보여드렸던 초 간단 GPU 파이프라인의 그림을 기억하시나요? 각 픽셀의 위치를 찾아내려면 정점쉐이더가 위치변환 결과를 래스터라이저에 전달해줘야만 했습니다. 따라서 정점쉐이더는 반드시 위치변환 결과를 반환해야 합니다. 자, 그렇다면 정점쉐이더 출력데이터 구조체를 VS_OUTPUT이란 이름으로 선언해보지요!

struct VS_OUTPUT 
{
    float4 mPosition : POSITION;
};

float4형으로 위치데이터를 반환하면서 '이것은 위치(POSITION)요!'라는 시맨틱을 붙여준 거 보이시죠?

전역변수
정점쉐이더에서 공간 변환을 할 때, 사용해야 할 전역변수들이 몇 있는데 그 전에 공간 변환이 무언지부터 설명해 드려야 할 듯 싶군요.

3D 공간변환
3D물체를 모니터에 그리려면 정점들의 위치를 공간 변환해야 한다고 말씀드렸습니다. 그렇다면 과연 어떤 공간들을 거쳐야 3D물체를 모니터에 보여줄 수 있을까요? 사과를 예로 들어보죠.

물체공간
자, 일단 사과를 손에 쥐어봅시다. 사과의 중앙을 원점으로 삼고 그 점을 시작으로 오른쪽(+x), 위쪽(+y), 앞쪽(+z)으로 3개의 축을 만들어 볼까요? 이제 원점으로부터 사과의 표면까지의 거리를 이리저리 재보면 각 점들을 (x, y, z) 좌표로 표현할 수 있겠죠? 그리고 이 정점들을 3개씩 묶어 삼각형들을 만들면 폴리곤으로 사과모델을 만들 수 있겠네요.

이제 사과를 손에 쥔 채 팔을 이리저리 움직여봅시다. 사과를 어디로 움직이던 간에 원점으로부터 각 정점까지의 거리는 변하지 않죠? 이것이 바로 물체공간(object space) 또는 지역공간(local space)입니다. 물체공간에서는 각 물체(3D 모델)가 자신만의 좌표계를 가지므로 다수의 물체를 통일적으로 처리하기 어렵습니다.

그림 2.2. 물체공간의 예



월드공간
이제 사과를 모니터 옆에 놓아볼까요? 모니터도 물체니까 자신만의 물체공간을 가지고 있겠군요. 이 둘을 통일적으로 처리하고 싶은데 그러려면 어떻게 해야 할까요? 이 두 물체를 같은 공간으로 옮겨오면 될 거 같은데요? 그러면 새로운 공간을 하나 만들어야겠군요. 현재 계신 방의 입구를 원점으로 삼고 오른쪽, 위쪽, 앞쪽으로 +x, +y, +z인 3개의 축을 만들어보죠. 이제 그 원점에서부터 모니터를 구성하는 정점들까지의 거리를 재면 새로운 (x, y, z) 좌표로 정점들을 표현할 수 있겠죠? 사과도 똑같은 방법으로 표현할 수 있겠네요. 이 새로운 공간을 월드공간(world space) 또는 세계공간이라고 합니다.

그림 2.3. 월드공간의 예



뷰공간
자, 그렇다면 이제 카메라를 가져다가 사진을 좀 찍어볼까요? 일단 위 두 물체들이 모두 사진 속에 들어오도록 사진을 찍고, 다음에는 이들이 전혀 보이지 않도록 전혀 엉뚱한 곳을 찍어봅시다. 이 두 사진은 확연히 다르죠? 처음 사진에서는 두 물체를 볼 수 있는데, 다른 사진에서는 흔적도 찾아볼 수 없군요. 그렇다면 이 두 사진 간에 뭔가 위치 변화가 있어야 한단 이야긴데 월드공간에서는 그 두 물체들의 위치가 전혀 변하지 않았는걸요? 아하! 그렇다면 이 카메라가 다른 공간을 사용하는 거로군요! 이렇게 카메라가 사용하는 공간을 뷰공간(view space)이라고 부릅니다. 뷰공간의 원점은 카메라 렌즈의 정 중앙이고 역시 그로부터 오른쪽, 위쪽, 앞쪽으로 3개의 축을 만들 수 있습니다.

그림 2.4 뷰공간의 예. 물체들이 카메라 안에 있음.


그림 2.5 뷰공간의 예. 물체들이 카메라 밖에 있음.


투영공간
일반 카메라로 사진을 찍으면 인간의 눈을 통해 보는 것과 마찬가지로 멀리 있는 물체는 조그맣게 보입니다. 근데 왜 우리 눈이 이렇게 작동하는지 아세요? 이건 인간의 시야가 좌우로 각각 100도 정도, 상하로 각각 75도 정도 되어서 그렇습니다. 따라서 멀리를 바라볼 수록 눈에 들어오는 범위가 넓어지는데 이 늘어난 범위를 일정한 크기의 망막에 담으려다 보니 멀리 있는 물체가 작게 보이는 거지요. 일반 카메라도 사람의 눈을 흉내 내는데 이와는 달리 직교카메라란 것도 있습니다. 직교카메라는 상하좌우로 퍼지는 시야를 가지지 않습니다. 무조건 앞쪽만 바라보지요. 따라서 직교카메라를 사용하면 거리에 상관없이 물체의 크기가 변하지 않습니다.

그러면 결국 카메라로 사진을 찍는 과정을 두 단계로 나눌 수 있는 것 같네요. 첫째는 월드공간에 있는 물체들을 카메라 공간으로 이동, 회전, 확대/축소시키는 단계고요, 둘째는 이렇게 새로운 공간에 위치된 물체들을 2D 이미지 위에 투영하는 것입니다. 이러면 첫 번째 단계를 뷰공간, 두 번째 단계를 투영공간이라고 확실히 구분할 수 있겠죠? 이제 직각투시법(orthogonal projection)을 사용하던 원근투시법을 사용하던 간에 뷰공간은 아무 영향을 받지 않겠네요. 그 대신 투영공간에서 이 투시법을 적용하겠죠.

이렇게 투영까지 마친 결과가 바로 화면에 보여지는 최종 이미지입니다.

정리
3D 그래픽에서 정점위치의 공간을 변환할 때 흔히 사용하는 방법이 정점의 위치벡터에 공간행렬을 곱하는 것입니다. 물체를 지역공간에서 화면공간까지 옮겨올 때 거치는 공간이 총 셋(월드공간, 뷰공간, 투영공간)이므로 행렬도 3개를 구해야 합니다. 참고로 각 공간의 원점과 세 축을 알면 그 공간을 나타내는 행렬을 쉽게 만들 수 있습니다.  (이 행렬을 직접 만드는 방법은 3D 수학책을 참조하시기 바랍니다. 이 책에서는 Direct3D에서 제공하는 함수를 사용해서 이 행렬들을 구성합니다.)

자, 그럼 여태까지 논한 모든 공간변환들을 정리해서 보여드리면 다음과 같습니다.

물체공간 ----------> 월드공간 --------> 뷰공간 ---------> 투영공간
              ⅹ월드행렬                   ⅹ뷰행렬                ⅹ투영행렬

위의 모든 행렬들은 각 정점마다 값이 변하지 않으니 전역변수로 선언하기에 적합하군요.

전역변수 선언
그럼, 이제 어떤 전역변수들이 필요한지 아시겠죠? 그렇습니다. 공간변환을 할 때 사용할 월드행렬, 뷰행렬, 투영행렬이 필요합니다. 정점쉐이더 코드에 다음의 세 라인을 삽입합시다.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;

float4x4라는 새로운 데이터형이 나왔군요? 이것도 역시 HLSL에서 지원하는 데이터형 중에 하나입니다. 4 X 4 행렬이라는 거 쉽게 아시겠죠? 이 외에도 float2x2, float3x3 등의 데이터형이 있습니다.

자, 그럼 이제 행렬들도 선언했는데 과연 누가 이 변수들에 값을 전달해줄까요? 보통 게임에서는 그래픽 엔진에서 전역변수들의 값을 대입해주는 코드가 있습니다. 렌더몽키에서는 변수시맨틱(variable semantic)을 통해 변수 값을 대입해줍니다. 그럼 변수시맨틱을 사용해보죠.


  1. Workspace 패널 안에서 ColorShader를 찾아 마우스 오른쪽 버튼을 누릅니다.
  2. 팝업메뉴에서 Add Variable > Matrix > Float(4x4)를 선택합니다. f4x4Matrix란 이름의 새로운 변수가 추가될 겁니다.
  3. gWorldMatrix로 변수의 이름을 변경합니다.
  4. 이제 gWorldMatrix 위에 마우스 오른쪽 버튼을 눌러, Variable Semantic > World를 선택합니다. 이게 바로 렌더몽키에서 변수 시맨틱을 통해 변수 값을 전달하는 방법입니다.
  5. 이제 위의 과정을 반복하여 뷰 행렬과 투영행렬을 만듭니다. 변수 명을 각각 gViewMatrix와 gProjectionMatrix를 만들고 View와 Projection 변수시맨틱을 대입합니다.
  6. 마지막으로 matViewProjection이란 변수를 지웁니다. 처음 이펙트를 만들 때 같이 딸려온 변수인데 저희는 이 대신 gViewMatrix와 gProjectionMatrix를 씁니다.


이 과정을 마치셨다면 Workspace 패널이 아래 그림처럼 보일 겁니다.

그림 2.6. 변수시맨틱을 대입한 뒤의 Workspace 패널


정점쉐이더 함수
이제 모든 준비작업이 끝났습니다. 드디어 정점쉐이더 함수를 작성할 때가 왔군요. 우선 함수헤더부터 볼까요?

VS_OUTPUT vs_main( VS_INPUT Input )
{

이 함수헤더가 의미하는 바는 이와 같습니다.

  • 이 함수의 이름은 vs_main이다.
  • 이 함수의 인수는 VS_INPUT 데이터형의 Input이다.
  • 이 함수의 반환값은 VS_OUPUT 데이터형이다.


C에서 함수를 정의하는 것과 별 차이가 없죠? HLSL은 C와 비슷한 문법을 사용한다고 전에 말씀드렸습니다. 자, 그럼 다음 라인을 보죠.

   VS_OUTPUT Output;

이건 그냥 함수의 끝에서 반환할 구조체를 선언한 것 뿐입니다. 함수헤더에서 선언했다시피 데이터형이 VS_OUTPUT인 거 보이시죠? VS_OUTPUT의 멤버로는 무엇이 있었죠? 투영공간으로 변환된 mPosition이 있었죠? 그럼 이제 공간변환을 해볼 차례군요! 우선, Input.mPosition에 담긴 모델공간 위치를 월드공간으로 변환합시다. 공간변환을 어떻게 한다고 했었죠? 정점위치에 행렬을 곱하는 거였네요. 그러면 float4형의 위치벡터와 float4x4 행렬을 곱해야겠네요? 행렬과 벡터를 곱하는 법을 찾기 위해 수학책을 뒤지실 필요는 없습니다. HLSL은 이미 여러 데이터형 간의 곱셈을 처리해주는 내장함수 mul()을 가지고 있습니다. 이 함수를 사용하면 공간변환이 이렇게 간단해집니다.

   Output.mPosition = mul( Input.mPosition, gWorldMatrix );

위의 코드는 모델공간에 존재하는 정점위치(Input.mPosition)에 월드행렬(gWorldMatrix)를 곱해서 그 결과(월드공간에서의 위치)를 Output.mPosition에 대입합니다. 이제 똑같은 방식으로 뷰공간과 투영공간으로 변환하면 됩니다.

   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );

복잡한 거 하나도 없죠? 자, 이제 무슨 일이 남았을까요? 정점쉐이더의 가장 중요한 임무는 모델공간에 있는 정점의 위치를 투영공간까지 변환하는 것이었으니까... 음.... 그 중요한 임무를 방금 막 마친 듯 한데요? 자, 그럼 이 결과를 반환하는 걸로 정점쉐이더를 마치겠습니다.

   return Output;
}

이제 F5를 눌러서 정점쉐이더를 한 번 컴파일 해보면 여전히 빨간 공이 보이죠? 그럼 일단 정점쉐이더는 잘 마무리가 된 듯 하네요. 혹시라도 컴파일 에러가 보이면 뭔가 잘못했단 이야기니 한번 코드를 다시 검토해보세요.

팁: 쉐이더 컴파일에 실패한 경우
오타나 문법적 오류 때문에 쉐이더 컴파일에 실패한 경우, 미리 보기 창에 컴파일에 실패했다는 오류메시지가 등장할 것입니다. 이 때, 정확히 어떤 코드에 문제가 있는지 알고 싶으시다면 렌더몽키의 젤 아래쪽에 위치한 출력(output)창을 보세요. 자세한 오류메시지와 더불어 문제가 있는 코드의 행과 열 번호까지도 보여줍니다.