2012년 1월 16일 월요일

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

이전편 보기


샘플파일 받기


제5장 물체에 색을 입히는 디퓨즈/스페큘러 매핑

실 세계에서 물체들이 다른 색을 갖는 이유는 물체마다 흡수/반사하는 빛의 스펙트럼(spectrum)이 다르기 때문입니다. 예를 들면 검정색 표면은 모든 스펙트럼을 흡수해서 검정색이고, 하얀색 표면은 모든 스펙트럼을 반사해서 하얀색입니다. 빨간색 표면 같은 경우는 빨강색의 스펙트럼을 반사하고 그 외의 스펙트럼을 흡수하니 빨간색으로 보이는 것이죠.

그렇다면 표면이 빛을 흡수하는 성질을 쉐이더에서 어떻게 표현할까요? 표면 전체가 한가지 색으로 칠해져 있다면 그냥 간단히 전역변수를 사용하면 되겠죠? 하지만 대부분의 경우, 물체의 표면은 다소 복잡한 패턴을 가지고 있습니다. 따라서 각 픽셀마다 색상을 정해줘야겠지요. 그럼 표면에서 반사할 색을 이미지로 그린 뒤, 픽셀쉐이더에서 이 텍스처를 읽어와 조명계산의 결과에 곱하면 되겠죠?

그런데 '제4장: 기초적인 조명쉐이더'에서 빛을 계산할 때, 난 반사광(diffuse light)와 정 반사광(specular light)을 따로 구했었죠? 그렇다면 난 반사광과 정 반사광을 합친 결과에 이 텍스처를 곱해야 할까요? 아니면 따로 해야 할까요? 전에도 말씀드렸듯이 인간이 물체를 지각할 수 있는 이유는 대부분 난 반사광 덕분입니다. (정 반사광은 타이트한 하이라이트를 추가해줄 뿐이지요.) 따라서 위 텍스처를 난 반사광의 결과에만 적용하는 것으로 충분합니다. 이렇게 난 반사광에 적용하는 텍스처를 디퓨즈맵(diffuse map)이라 부릅니다.

그렇다면 정 반사광은 어떻게 할까요? 물론 디퓨즈맵을 정 반사광에 그대로 사용할 수도 있습니다만 다음과 같은 두 가지 이유 때문에 정 반사광용으로 스페큘러맵(specular map)을 따로 만드는 경우가 허다합니다. 첫째, 난 반사광이 반사하는 빛과 정 반사광이 반사하는 빛의 스펙트럼이 다른 경우가 있습니다. 둘째, 각 픽셀이 반사하는 정 반사광의 정도를 조절하는 용도로 스페큘러맵을 사용할 수도 있습니다. 예를 들어, 사람의 얼굴에 정 반사광을 때린다고 생각해 보죠. '제4장: 기초적인 조명쉐이더'에서 봤던 것처럼 얼굴의 전체에서 고르게 정 반사광이 보일까요? 거울을 보시면 알겠지만 이마나 코가 더 반짝거리죠? 그리고 이마나 코에서도 정 반사광이 좀 듬성듬성 보일 겁니다. (이건 모공이나 털 때문에 피부가 매끄럽지 않기 때문입니다.) 스페큘러맵을 잘 칠하면 이런 효과를 낼 수 있습니다. 따라서 이 장에서는 디퓨즈맵과 스페큘러맵을 따로 사용하도록 하겠습니다.

이 외에도 물체의 색에 영향을 미치는 다른 요소가 있습니다. 바로 조명의 색입니다. 흰색 물체에 빨간색 빛을 비추면 물체가 불그스름하게 되죠? 조명의 색은 전역변수로 쉽게 지정할 수 있습니다.

여태까지 말씀 드린 것을 보기 쉽게 설명하면 다음과 같습니다.

난 반사광 = 빛의 색상 X 난 반사광의 양 X 디퓨즈맵의 값정 반사광 = 빛의 색상 X 난 반사광의 양 X 스페큘러맵의 값

그러면 위 내용들을 염두에 두고 디퓨즈/스페큘러매핑 쉐이더를 작성해 볼까요?

기초설정
일단 저번 장에서 사용했던 렌더몽키 프로젝트의 사본을 만들어 새로운 폴더에 저장합니다. 혹시 렌더몽키 프로젝트를 저장해 놓지 않으신 분들은 부록 CD에서 samples\04_lighting\lighting.rfx 파일을 복사해 오시면 됩니다.

이 파일을 렌더몽키에서 연 다음, 쉐이더의 이름을 SpecularMapping으로 바꾸겠습니다. 이름까지 바꾸셨다면 이제 디퓨즈맵과 스페큘러맵으로 사용할 이미지들을 추가해야겠군요. 쉐이더 이름에 마우스 오른쪽 버튼을 누르면 나오는 팝업메뉴에서 Add Texture > Add 2D Texture > Fieldstone.tga파일을 선택합니다. 그러면 Fieldstone이라는 이름의 텍스처가 보이시죠? 이 이름을 DiffuseMap으로 바꾸도록 합시다.

이제 Pass 0에 마우스 오른쪽 버튼을 눌러 Add Texture Object > DiffuseMap을 선택합니다. Texture0이라는 텍스처 개체가 생겼을 것입니다. 이 이름을 DiffuseSampler로 변경합니다.

이제 스페큘러맵을 추가해야 하는데 아무리 렌더몽키 폴더를 뒤져봐도 마땅한 놈이 안 보이는군요. 그래서 제 손으로 직접 스페큘러맵을 만들어서 부록 CD에 넣어놨습니다. 일단 이 스페큘러맵이 어떻게 생겼는지 한번 볼까요? 디퓨즈맵과 같이 비교해서 보면 좋겠군요

그림 5.1. 디퓨즈맵(왼쪽)과 스페큘러맵(오른쪽)

스페큘러맵에서 돌판 사이의 틈새를 검정색으로 칠해 놓은 거 보이시죠? 따라서 이 틈새는 전혀 정 반사광을 반사하지 않을 겁니다. (하지만 난 반사광은 여전히 존재하지요.) 여기서 한가지 기억하실 점은 텍스처가 언제나 색상정보를 가지지는 않는다는 것입니다. 스페큘러맵이 그 좋은 예죠. 스페큘러맵에 저장된 정보는 최종이미지의 색상 값이라기 보다는 각 픽셀이 반사하는 정 반사광의 양입니다. 이와 마찬가지로 픽셀 수준에서 제어하고 싶은 변수가 있다면 이렇게 텍스처를 사용하는 것이 보통입니다. 후에 법선매핑을 다룰 때, 이렇게 텍스처를 이용하는 예를 보실 수 있을 것입니다.

팁: 픽셀수준에서 제어하고 싶은 변수가 있다면 텍스처를 이용합니다.

자, 그럼 부록CD에서 Samples\05_DiffuseSpecularMapping\Fieldstone_SM을 찾아서 렌더몽키 프로젝트에 추가합니다. 추가하는 방법은 간단히 파일을 끌어다 이펙트 이름 위에 놓아주면 됩니다. 텍스처 이름은 SpecularMap으로, 텍스처 개체의 이름은 SpecularSampler로 하겠습니다.

다음은 빛의 색을 추가하도록 하지요. 쉐이더 이름에 오른쪽 버튼을 누른 뒤에 Add Variable > Float > Float3를 선택합니다. 변수명을 gLightColor로 하지요. 이제 이 변수를 더블클릭하면 값을 대입할 수 있습니다. 약간 푸르스름한 빛을 사용한다는 의미로 (0.7, 0.7, 1.0)을 대입하겠습니다.

마지막으로 Stream Mapping을 설정할 차례입니다. '제4장: 기초적인 조명쉐이더'와는 달리 여기서는 텍스처를 사용하니 정점데이터에서 UV 좌표를 읽어와야 합니다. Stream Mapping에 더블클릭을 해서 TEXCOORD0을 추가합니다. 당연히 데이터형은 float2입니다.

여기까지 설정을 마치셨다면 렌더몽키 작업공간이 아래와 같을 겁니다.

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

정점쉐이더
이제 정점쉐이더를 보도록 하지요. 우선 전체 소스코드부터 보여드린 뒤, 새로 추가된 코드만 설명 드리겠습니다.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;

float4 gWorldLightPosition;
float4 gWorldCameraPosition;

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float3 mNormal: NORMAL;
   float2 mUV: TEXCOORD0;
};

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float2 mUV: TEXCOORD0;
   float3 mDiffuse : TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;
};

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

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

   float3 lightDir = Output.mPosition.xyz - gWorldLightPosition.xyz;
   lightDir = normalize(lightDir);
 
   float3 viewDir = normalize(Output.mPosition.xyz - gWorldCameraPosition.xyz);
   Output.mViewDir = viewDir;
 
   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );
 
   float3 worldNormal = mul( Input.mNormal, (float3x3)gWorldMatrix );
   worldNormal = normalize(worldNormal);

   Output.mDiffuse = dot(-lightDir, worldNormal);
   Output.mReflection = reflect(lightDir, worldNormal);

   Output.mUV = Input.mUV;
 
   return Output;
}



새로 추가해야 할 전역변수가 있던가요? 새로 추가된 변수는 빛의 색상하고 2개의 텍스처 샘플러인데 텍스처 샘플러야 당연히 픽셀쉐이더에서 사용하는 거니까 여기선 선언하지 않아도 되겠네요. 빛의 색상도 픽셀쉐이더에서 그냥 곱하면 되겠는걸요?

그렇다면 정점쉐이더 입출력 구조체는 어떨까요? 새로 추가해야 할 게 하나 생겼죠? 픽셀쉐이더에서 텍스처매핑을 하려면 UV 좌표가 필요하니까요. 정점버퍼에서 UV 좌표를 가져와서 픽셀쉐이더에 전달해 줘야겠네요. 다음의 코드를 정점쉐이더의 입력구조체와 출력구조체 양쪽에 모두 추가합시다.

    float2 mUV: TEXCOORD0;

정점쉐이더 함수에 추가해야 할 코드도 딱 한 줄 뿐입니다. UV좌표를 전달해주는 것이죠.

   Output.mUV = Input.mUV;

정말 간단했죠? 이게 전부랍니다.



댓글 9개:

  1. 정말 간단했죠?? 그리고 ......
    다음편이 보기

    여기까지 무사히(?) 따라왔습니다.
    다음 강좌는 구정에 개시 하겠시네요.

    구정 잘 보내세요 캐나다에 계신면...^^;;;;;;;;

    그리고 .... 강좌 고맙습니다. ^^

    답글삭제
    답글
    1. 경진님 안녕하세요. .언제나 좋은 댓글 달아주셔서 힘내서 글 씁니다... 제가 별로 명절 이런걸 챙기는 편이 아니라 구정이 언젠지도 모르지만.. -_-;;;; 경진님은 구정 잘 보내시구요..

      생각해보니 항상 글 봐주시는데 전 경진님이 어떤 분이신지 조차 잘 모르네요..(좀 죄책감이 ^_^) 담에 답글 남겨주실 땐 본인소개도 부탁드려요 ;)

      p.s. 다음편 보기.... 는 글을 그나마 좀 길게 연재하려는 수작이라죠.. 중간에 출판땜에 좀 공백기간이 있을거니까.. 너무 휑화니 비어두긴... 좀 그래서 ^_^

      삭제
    2. 죄책감이라니요.

      자주 들리는 김윤정님의 블러그에서 링크 타고 들어왔어,
      좋은 강좌를 날름 보고 있는데,
      제가 먼저 알려드렸어야 되지요

      신생 게임회사에서 배경 그래픽을 담당하고, 가끔 맥스 스크립트 제작(초보수준)을 하고 있습니다.
      쉐이더와 프로그램에 아주 관심이 많은 37세(남)의 배경 디자이너입니다.
      이름은 닉네임입니다. ^^

      삭제
  2. 오늘글도 잘 봤습니다.

    그리고 tex2D 해서 나오는 값은 0~ 255 사이값이 나오는게 맞죠?

    답글삭제
    답글
    1. 0~1(0~100프로 라고 읽으시는게 편함)이 나옵니다. 전 강좌 어딘가 써놨던거 같은데 ^_^

      삭제
    2. 여기서 색의 표현방법: http://kblog.popekim.com/2011/12/02-part-2.html

      삭제
  3. 저도 잘 보고 있습니다. ㅎ

    근데 버텍스 쉐이더 코드에 그때그때 주석을 달고 있는데...

    렌더몽키 오류인지는 모르겠으나

    '준비' 라는 단어가 들어가면 컴파일이 안되네요 ㄷㄷ

    신텍스 에러가 나네요

    예를들어 // 정반사광을 픽셀 쉐이더에서 구할 수 있도록 준비

    이렇게 코멘트를 달면 컴파일 에러가 ㅠㅠ

    이 부분 혹시 알고 계시는 부분인가요?

    답글삭제
    답글
    1. 모르는 부분이에요.. 전 사실 한글로 주석을 안달거든요.. '_' 제 생각엔 한글을 아스키 코드로 처리해서 뭔가 잘못된 문자가 들어가는듯.. 행바꿈 문자라던가 ㅎㅎ

      삭제