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

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)[각주:1]을 구해와 그 색을 화면에 출력하는 것이겠군요. 그렇다면 텍스처로 사용할 이미지와 현재 픽셀의 UV 좌표가 필요하겠죠? 텍스처 이미지는 픽셀마다 변하는 값이 아니므로 전역변수로, UV 좌표는 정점쉐이더로부터 보간기를 거쳐 들어온 입력데이터가 되겠네요. 우선 픽셀쉐이더 입력데이터의 구조체부터 만들겠습니다.

struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};


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

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

sampler2D DiffuseSampler;


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

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


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

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도씩 추가하고, 현재 회전 각도에 따라 회전행렬을 만들어서 그것을 월드행렬로 사용합니다. 현재 사용하시는 컴퓨터의 사양에 따라 이 회전 값이 너무 빠르거나 느릴 수도 있습니다. 본인의 컴퓨터에 맞게 적절히 값을 조정하세요.[각주:2]

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

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


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

수고하셨습니다.


p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.


포프 올림


 
  1. 그림(picture)의 최소구성단위가 픽셀(pixel)인 것처럼 텍스처(texture)의 최소구성단위가 텍셀(texel)입니다. [본문으로]
  2. 실제 게임에서는 지난 프레임 이후 경과한 시간에 따라 회전량을 계산하는 게 옳은 방법입니다. 여기서 보여 드리는 코드는 쉐이더 데모를 위한 것이므로 그냥 이 정도로 놔두겠습니다. [본문으로]
저작자 표시 비영리 변경 금지
신고