2012년 1월 23일 월요일

[포프의 쉐이더 입문강좌] 05. 물체에 색을 입히는 디퓨즈/스페큘러 매핑 Part 2


이전편 보기


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

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 물체를 렌더링 할 때 여기서 배웠던 기법을 법선매핑(법선매핑(normal mapping)은 제7장에서 배웁니다)과 혼합하는 것이 거의 표준입니다.


다음편 보기

댓글 14개:

  1. 강좌 고맙습니다.

    빛 색도 바꿔 보고 텍스쳐도 바꿔보고, 이리저리 변화 되는 것 보니 뿌듯합니다. ^^

    p.s. 몇 차례 맥스에서 obj로 넘겨(Texture coordinates 체크크,, 메쉬는 잘 보이는데요. uv 좌표는 읽어 들이지 못하네요..혹시 특별한 세팅법이 필요한것지 궁금합니다.

    답글삭제
    답글
    1. 3DS Max 2010에서 한번 테스트 해봤거든요.. 저는 잘 되는데요? 전 이렇게 했어요.

      1.맥스를 연다
      2.box를 하나 만든다
      3.test.3ds 파일로 export
      4.렌더몽키에서 이걸 불러온다.

      텍스처 잘 나오는데요..?

      삭제
    2. 왜 혼자 할땐 안되고,....
      답변 보고 하니 되네요......아~~~ 답변 고맙습니다. ^^

      삭제
    3. 그래서 세상은 함께 살아가는 거라지요 -_-

      삭제
  2. 이번 강좌도 잘 봤습니다.

    빛을 더하고 곱하는 것에 대해서 개념을 확실히 잡을만한 자료같은게 있을까요??

    이게 약간 헷갈리네요 :D 하하하...

    답글삭제
    답글
    1. 건 왠지 대마왕님이 잘 아실 거 같은데.... 전 그냥 diffuse는 곱하고 specular는 더한다란 개념으로 가고 있어서...

      삭제
  3. 궁금한점이 있습니다. terrain을 tool을 만들고 있는데 지형 올라오는 부분을 Lock을 걸어서 하고 있습니다. 이걸 쉐이더로 작업을 할 수 있을까요? 즉, 버텍스들을 쉐이더로 옮길 수 있는지 궁금해서 물어봅니다.

    그리고 혹시 괜찮으시면 후반부에 움직이는 물체(애니메이션)에 대한 쉐이더 강의 부탁드려요.

    정적인 물체에 대한 쉐이더 책은 많은데 움직이는 물체에 대한 쉐이더가 많이 없네요.
    모션 블러같은것을 접목하려해도 자료구하기가 너무 힘드네요.

    너무 좋은 강의 감사합니다.

    답글삭제
    답글
    1. 제가 해본적은 없지만.... 가능은 하거든요.

      어차피 높이맵(height map)에 따라 정점의 높이를 바꾸는 거잖아요. 그럼 1) float[] 변수로 높이값을 전달해주거나 2) 높이값을 가진 텍스터를 전달해준 뒤 텍스터 룩업을 해서 높이값을 구한뒤에.... 그 높이값을 위치.y 에 추가해주면 되죠...

      그리고 그 다음의 공간변환하면 끝....?


      근데 float[] 로 값 전달해주기엔 constant register가 좀 모자를 듯 하고... 정점쉐이더에 텍스처를 전달해주려면 어차피 텍스처에 lock을 걸어야 하니..... 그닥 의미가 있을까요..^^?


      정범버퍼에서 texture 읽어오는 건 여기에 --> http://gamedevforever.com/61


      일단 후반부 강의도 다 써놓은건데 .. 모션블러나 이런 강의는 없어요. 초보자용은 아니라고 생각해서.. 정 필요하면 나중에 따로 블로그 글을 쓰던가 할 순 있는데.... 렌더몽키에서 설정 안될거 같은데... 데모코드를 작성하기가 꽤 애매할거 같네요...

      그냥 설명만하고 코드는 배쨀까요? -_-;

      삭제
    2. float[] vValue;

      생각해보니 Lock을 해야만 할 것 같네요.~
      그리는것만 쉐이더로 하고 그냥 해야겠네요.
      답변 감사합니다.

      나중에 꼭 부탁해요~(애니메이션된 쉐이더)^^

      삭제
    3. 네.. 저가 생각해도 lock하는게 가장 간단한 방법 같아요 ^^... DX11 쉐이더를 쓰면 tessellation 도 할 수 있긴 할텐데........ 건 잘 모르므로 패스 -_-

      삭제
    4. 제가 받아본 댓글중 가장 감동적이군요. 감사합니다 -_-a

      삭제
  4. 안녕하세요 pope kim님, 어제부로 HLSL 프로그래밍에 막 발을 들인 초보자입니다. 셰이더 프로그래밍 입문책은 반 좀 넘게 읽어가는 참인데요, 다름이 아니라 제가 구현하고싶은 쉐이더가 있는데 어떻게 구현하면 좋을지 갈피가 안잡혀서 이렇게 질문을 올립니다.
    https://courses.cs.washington.edu/courses/cse459/10wi/content/html/projects_459/project1a/index.html
    위 홈페이지에 있는 구체처럼 물체에 발광효과를 주고 싶은데요.
    뭐 굳이 저렇게 일부만 발광하는것 뿐 아니라, 전체를 발광하게 해도 좋습니다.
    emissive랑 관련이 있을까 싶어서 자료를 뒤져봐도 딱히 수확은 없고 점점 미궁속으로 빠져가네요.... 구체적인것까진 아니더라도 길이라도 잡아주십사합니다.

    답글삭제