픽셀쉐이더
픽셀쉐이더도 전체 소스코드를 보여드린 뒤, 새로 추가된 내용만 설명 드립니다.

struct PS_INPUT
{
   float2 mUV : TEXCOORD0;
   float3 mDiffuse : TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;
};

sampler2D DiffuseSampler;
sampler2D SpecularSampler;

float3 gLightColor;

float4 ps_main(PS_INPUT Input) : COLOR
{
   float4 albedo = tex2D(DiffuseSampler, Input.mUV);
   float3 diffuse = gLightColor * albedo.rgb * saturate(Input.mDiffuse);
   
   float3 reflection = normalize(Input.mReflection);
   float3 viewDir = normalize(Input.mViewDir); 
   float3 specular = 0;
   if ( diffuse.x > 0 )
   {
      specular = saturate(dot(reflection, -viewDir ));
      specular = pow(specular, 20.0f);
      
      float4 specularIntensity  = tex2D(SpecularSampler, Input.mUV);
      specular *= specularIntensity.rgb * gLightColor;
   }

   float3 ambient = float3(0.1f, 0.1f, 0.1f) * albedo;
   
   return float4(ambient + diffuse + specular, 1);
}


우선 새로 렌더몽키에 추가했던 세 변수를 전역적으로 선언하겠습니다.

sampler2D DiffuseSampler;
sampler2D SpecularSampler;

float3 gLightColor;


그리고 PS_INPUT구조체에 UV좌표를 추가합니다.

  float2 mUV : TEXCOORD0;


이제 픽셀쉐이더 함수의 젤 윗줄에서 디퓨즈맵을 샘플링 해보죠. 

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


이것이 바로 현재 픽셀이 반사하는 색깔입니다. 여기에 난반사광의 양과 빛의 색상을 곱해야 한다고 했죠? 이전에 diffuse변수를 구하던 코드를 다음과 같이 바꿉니다.

   float3 diffuse = gLightColor * albedo.rgb * saturate(Input.mDiffuse);


이제 한번 F5를 눌러서 정점쉐이더와 픽셀쉐이더를 각각 컴파일 한 뒤, 미리 보기 창을 한 번 봐볼까요?

그림 5.3. 디퓨즈맵만 적용한 결과

 
담벽 텍스처가 보이죠? 푸르스름한 빛도 보이네요. 근데 스페큘러맵을 아직 쓰지 않아서인지 정 반사광이 너무 강하네요. 돌 사이의 틈새까지도 말이지요! 그럼 스페큘러맵을 더해보도록 하죠.

팁: 미리 보기 창에서 물체를 회전하는 법
그림 5.3 처럼 틈새에 빛이 들어오게 하려면 물체를 회전하셔야 할 겁니다. 미리 보기 창에서 물체를 회전하시려면 창 안에 왼쪽 마우스 버튼을 누른 채 마우스를 이리저리 움직여보세요. 만약 회전 대신에 이동이나 확대/축소가 된다면 툴바에서 오른쪽 두 번째 아이콘(Overloaded Camera Mode)을 눌러주시면 됩니다.



픽셀쉐이더에서 specular 변수의 거듭제곱을 구하는 코드(pow함수 호출) 바로 밑에서 스페큘러맵을 샘플링 합니다.

      float4 specularIntensity  = tex2D(SpecularSampler, Input.mUV);


이제 이것과 빛의 색상을 specular에 곱해야겠죠? 코드는 다음과 같습니다.

      float4 specularIntensity  = tex2D(SpecularSampler, Input.mUV);
      specular *= specularIntensity.rgb * gLightColor;


위 그림에서 또 다른 문제점 하나는 난 반사광이 사라지는 순간부터 디퓨즈 텍스처의 디테일도 사라진다는 것입니다. 이것은 주변광 값으로 (0.1, 0.1, 0.1)만을 사용했기 때문인데요. 주변광은 그냥 저희가 임의로 정한 빛의 양이므로 여기에도 디퓨즈맵을 곱해주는 것이 맞습니다. ambient변수를 구하는 코드를 찾아 다음과 같이 바꿔주세요.

float3 ambient = float3(0.1f, 0.1f, 0.1f) * albedo;


다시 한번 정점쉐이더와 픽셀쉐이더를 컴파일 한 뒤 미리 보기 창을 볼까요?

그림 5.4. 스페큘러맵과 주변광까지 제대로 적용한 결과

 
확실한 차이를 볼 수 있죠? 정 반사광이 그렇게 강하지도 않고, 틈새도 완벽하네요. 거기다가 어두운 픽셀에서도 디퓨즈맵의 흔적을 찾아 볼 수 있네요.


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

우선 저번 장에서 사용했던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음, 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장합니다. Sphere.x와 SpecularMapping.fx라는 파일이름을 사용하도록 하겠습니다. 또한 렌더몽키에서 사용했었던 텍스처 2개를 복사해다가 프레임워크 폴더에 저장합니다. Fieldstone_DM.tga와 Fieldstone_SM.tga로 이름을 사용하지요.

이제 비주얼 C++ 솔루션 파일을 연 뒤, 소스코드에서 gpLightingShader 변수가 언급된 곳을 모두 찾아gpSpecularMappingShader로 변경합니다.

그 다음, 전역변수 섹션에 가서 두 텍스처를 저장할 포인터들과 빛의 색상을 저장하는 변수를 선언합니다.

// 텍스처
LPDIRECT3DTEXTURE9        gpStoneDM    = NULL;
LPDIRECT3DTEXTURE9        gpStoneSM    = NULL;

// 빛의 색상
D3DXVECTOR4               gLightColor(0.7f, 0.7f, 1.0f, 1.0f);


렌더몽키에서 사용했던 푸르스름한 빛의 값인 (0.7, 0.7, 1.0)을 그대로 사용하는 거 보이시죠? 위에서 새로운 D3D 자원(텍스처)을 둘 선언했으니 이들을 해제하는 코드를 추가하도록 하죠. CleanUp() 함수에 다음의 코드를 추가합니다.

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

    if ( gpStoneSM )
    {
        gpStoneSM->Release();
        gpStoneSM = NULL;
    }


이제 D3D자원들을 로딩할 차례입니다. LoadAssets() 함수에 다음의 코드를 더합니다.

    // 텍스처 로딩
    gpStoneDM = LoadTexture("Fieldstone_DM.tga");
    if ( !gpStoneDM )
    {
        return false;
    }

    gpStoneSM = LoadTexture("Fieldstone_SM.tga");
    if ( !gpStoneSM )
    {
        return false;
    }


쉐이더 파일의 이름을 SpecularMapping.fx로 바꿔주는 것도 잊지 맙시다.

    gpSpecularMappingShader = LoadShader("SpecularMapping.fx");


이제 마지막으로 RenderScene() 함수를 봅시다. 이미 쉐이더가 모든 일을 하고 있으니 간단히 새로운 변수들을 대입해주기만 하면 되겠네요. 예전에 SetMatrix() 함수들을 호출 해주던 곳이 있었죠? 그 아래에 다음의 코드를 추가합시다.

    gpSpecularMappingShader->SetVector("gLightColor", &gLightColor);
    gpSpecularMappingShader->SetTexture("DiffuseMap_Tex", gpStoneDM);
    gpSpecularMappingShader->SetTexture("SpecularMap_Tex", gpStoneSM);


위 코드는 빛의 색상과 두 텍스처맵을 쉐이더에 전달해 줍니다. 텍스처맵을 대입해 줄 때 _Tex 접미사를 붙여줘야 한다는 건 '제3장: 텍스처매핑'에서 설명했었죠?

이제 코드를 컴파일 한 뒤, 프로그램을 실행하면 렌더몽키에서와 동일한 결과를 보실 수 있을 겁니다.

이쯤 되면 느끼셨겠지만 쉐이더를 사용하면 DirectX 프레임워크에서 책임져야 할 그래픽 관련 업무가 현저히 줄어듭니다. D3D 자원을 로딩하는 것과 쉐이더 매개변수, 그리고 렌더상태(render state)를 관리하는 것이 거의 전부입니다.

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.
  • 인간이 물체의 색을 볼 수 있는 이유는 물체마다 흡수 및 반사하는 빛의 스펙트럼이 다르기 때문이다.
  • 3D 그래픽에서는 디퓨즈맵과 스페큘러맵을 사용하여 빛의 흡수 및 반사를 재현한다.
  • 스페큘러맵은 각 픽셀의 정 반사광을 조절하는 것을 주 목적으로 한다.
  • 빛의 색도 물체의 최종 색에 기여한다.
  • 텍스처는 색상정보만을 저장하는 것이 아니다. 스페큘러맵의 경우가 그 예이다.

이 장은 예상보다 매우 쉽게 끝나버렸습니다. 저번 장에서 작성했던 조명쉐이더에 살을 붙였기 때문인데요. 이렇게 기본이 되는 쉐이더를 잘 짜놓으면 매우 쉽게 구현할 수 있는 기법들이 많습니다.

하지만 이번 장에서 배운 내용을 절대 가볍게 보시지 말기 바랍니다. 최근 게임에서 3D 물체를 렌더링 할 때 여기서 배웠던 기법을 법선매핑[각주:1]과 혼합하는 것이 거의 표준입니다.



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


포프 올림
  1. 법선매핑(normal mapping)은 제7장에서 배웁니다. [본문으로]