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에서 텍스처매핑이 그리 어렵진 않으니 잘 익혀두시기 바랍니다.

수고하셨습니다.


댓글 19개:

  1. 우왕 머리에 쏙쏙 들어와요~

    답글삭제
    답글
    1. 어디서 많이 뵌 이름이어서 눌러보니 아시는 분이 맞네요..

      삭제
  2. @최재규: 원래 잘알고 계셨으면서 -_-;

    답글삭제
  3. 잘 봤습니다. ^_^

    이해는 아직 많이 부족하지만, 결과물은 출력이 되었습니다.~

    답글삭제
  4. 포프님 잘 보고 갑니다~ :D

    답글삭제
  5. 일단 여기까지도 출력은 무사히 끝냈습니다. ^^
    이해도는 아직 100%는 무리지만 반복하고 있습니다.~

    답글삭제
  6. @익명 @김경진:

    혹시라도 제가 설명을 시원하게 잘 하지 못한 부분 있음 알려주세요. 이해가 잘 안되시는 부분이 아마 제가 설명을 자세히 하지 못한 부분일듯~

    블로그에 연재하면 좋은게 언제라도 내용을 보강할 수 있단 거지요 :)

    답글삭제
  7. Pope 님 네 알겠습니다.
    제가 이해도가 많이 떨어졌어, 열심히 반복학습 중입니다. ^^
    의문점이 있으면 꼭 댓글 남기겠습니다.

    답글삭제
  8. 질문이 있습니다. 쉐이더 내에서 사용하는 UV좌표는 어떻게 설정된 것입니까? 코드 내에서 uv좌표에 대한 것은 못 찾겠어요...!(제가 말을 잘 못해서 질문이 되게 엉성하네요...)

    답글삭제
  9. UV 좌표는 아티스트들이 vertex마다 하나씩 설정해줍니다. (아티스트 아찌들에게 물어보시길~) Vertex에 들어가는 정보중에 아티스트들이 넣어주는 게 위치 외에도 UV좌표, 법선(normal) 등등이 있지요.

    답글삭제
  10. 잘 보고 있습니다.
    그런데 swizzle 응용을 어떤 식으로 하는지 예를 들어주실 수 있을까요?
    rgba같은 경우 순서가 정해져 있는 일종의 규칙인데 굳이 바꿔 쓰면 헷갈릴 것 같기도 하고.. 바꿔야 하는 일이 있을지 아직 잘 모르겠어서요^^;

    답글삭제
    답글
    1. rgba에 반드시 색만 있으란 법은 없지요.. 예를들어서 CPU쪽에서 float4 timeSinCos; 함수로 다음과 데이터를 전달해 준다고 생각해보세요. x = 시간, y = sin(시간), z = cos(시간).... 그리고 픽셀쉐이더에서 이런 일을 하는 코드를 작성한다고 생각해보죠.

      float a = 3.0f * sin(t);
      float b = 2.0f * cos(t);
      float c = 3.0f * cos(t);
      float d = 2.0f * sin(t);

      그럼 이렇게 4번 곱하기를 하는 대신에 스위즐을 이용해서 곱하기 한번으로 가능합니다.

      float2 temp = float2(3.0f, 2.0f);
      float4 result = temp.xyxy * timeSinCos.yzzy;

      억지로 만든 예지만... 대충 감이 오실듯 ^_^

      삭제
  11. 픽셀 쉐이더에서 POSITION 시멘틱 읽기는 안되나요?

    안된다면 왜 안되는지 설명 해주시면 감사하겠습니다 ^^

    답글삭제
    답글
    1. 안됩니다.. 이유는 HLSL에서 지원을 안해서라죠. 딱히 다른 이유가 있는건 아닙니다. -0- 쉐이더 버전3부터는 VPOS라고 시맨틱을 이용하면 화면좌표에서의 픽셀위치를 읽어올순 있긴 합니다.

      삭제
  12. hlsl 관련 문법 질문 있습니다.

    float4 PS_Main(VSOUTPUT IN) : COLOR0
    {
    ....

    float ret;
    return ret;
    }

    이렇게 서로 다른 타입 ( float <-> float4 ) 을 리턴하여도

    에러가 발생 안하고 심지어 이렇게 사용 하는 코드를 여럿 보았습니다.

    자료를 찾아 보아도 명확한 답이 못 찾겠어 이렇게 질문합니다.

    감사합니다 ^^

    답글삭제
    답글
    1. float4 a;
      float b;

      가 있을때..

      a = b; 를 대입하면 알아서 이렇게 해줍니다. a.xyzw = b.xxxx;

      곱셈등의 연산을 할때도 이런게 허영되어야 매우 편하니까요.. 예를 들면

      float4 vector = float4(1,0,0,0); 을 3배로 늘리려 하면 간단히
      vector = vector * 3;

      이게 안되면 정말 귀찮을거 같아요.. 실수할 여지도 크게 안보이고..


      하지만 그 반대로 b = a; 를 대입하면 컴파일러 에러가 나는걸로 알고 있습니다... (아니라면 말해주세요) 이건 실수를 유발할 여지도 많으니까요... a.y를 대입하고 싶은때 깜박잊고 a; 만 썼다던가 등등...

      삭제
  13. 중요한 건 아닌데요, 자전 방향이 반대입니다ㅎㅎㅎㅎㅎ

    답글삭제
  14. 작성자가 댓글을 삭제했습니다.

    답글삭제