2012년 1월 9일 월요일

[포프의 쉐이더 입문강좌] 04. 기초적인 조명쉐이더 Part 3

이전편 보기

정 반사광

배경
정 반사광(specular light)(이것을 반사광이라 부르기도 합니다.)은 난 반사광과는 달리 한 방향으로만 반사되는 빛으로 입사각이 출사각과 같은 것이 특징입니다. 따라서 정 반사광의 효과를 보려면 빛이 반사되는 방향에서 물체를 바라봐야만 합니다. 모니터에 빛이 반사 되서 화면을 보기 힘들었던 기억 있으시죠? 그 때 모니터를 조금 돌리면 조금 살만했던 거도요? 그게 바로 정 반사광입니다.

앞에서 보여 드렸던 난 반사광을 그림에 정 반사광도 추가해 보지요.

그림 4.8 난 반사광과 정 반사광

난 반사광과 마찬가지로 정 반사광을 수학적으로 재현해내는 수학공식이 여럿 있습니다. 여기서는 게임업계에서 널리 사용하는 기법인 퐁(phong) 모델을 사용하겠습니다. 퐁 모델은 반사광과 카메라벡터(카메라에서 현재 위치까지 선을 그은 벡터)가 이루는 각도의 코사인 값을 구하고, 그 결과를 여러번 거듭제곱하면 정 반사광을 구할 수 있다고 합니다. 아래의 그림을 보시죠.

그림 4.9. 정반사광의 예

반사광(R)과 카메라벡터(V)가 이루는 각도의 코사인 값을 구하는 것은 난 반사광에서 했던 것과 별반 차이가 없겠군요. 법선벡터와 입사광 벡터 대신에 반사광 벡터와 카메라벡터를 쓰는 것만 빼면요. 근데 왜 이 결과에 다시 거듭제곱을 할까요? 역시 코사인 그래프를 보면 답이 보입니다.

그림 4.10. 거듭제곱수가 늘어남에 따라 빠르게 줄어드는 코사인 그래프

위 그래프에서 보면 거듭제곱수가 늘어남에 따라 코사인 값이 빠르게 줄어드는 거 보이시죠? 실생활에서 정 반사광을 관찰해봅시다. 정반사광의 폭이 얼마나 되나요? 난 반사광에 비해 상당히 타이트하지 않나요? 바로 이런 타이트한 정 반사광을 재현하기 위해 코사인 값에 거듭제곱을 하는 겁니다.

그러면 거듭제곱은 몇 번이나 해야 할까요? 이건 사실 표면의 재질에 따라 다릅니다. 거친 표면일수록 정 반사광이 덜 타이트할 테니까 거듭제곱 수를 줄여줘야겠죠. 보통 한 20번 정도 거듭제곱을 해주면 대충 괜찮은 결과를 얻으실 수 있습니다.

그럼 이제 쉐이더를 작성해 봅시다.

기초설정
바로 조금 전에 작성했었던 난 반사광 쉐이더에 정 반사광 조명 코드를 추가하도록 하죠. 어차피 이 두 광이 합쳐져야 제대로 된 조명효과니까요.

그림 4.9에서 새로 추가된 것이 뭐가 있었죠? 반사광 벡터하고 카메라 벡터죠? 반사광 벡터야 입사광 벡터를 법선에 대해 반사시킨 것이니(입사각과 출사각이 같습니다) 이미 가지고 있는 정보에서 구할 수 있겠네요. 카메라 벡터는요? 입사광의 벡터를 구했던 것과 마찬가지 방법으로 카메라 위치에서 현재 위치까지 선을 쭈욱~ 그으면 되겠죠? 그러려면 카메라 위치를 전역변수로 만들어야 겠네요. 렌더몽키의 Lighting 쉐이더 위에 마우스 오른쪽 버턴을 눌러 새로운 float4 변수를 추가합시다. 이름은 gWorldCameraPosition이 적당하겠네요. 이제 이 변수 위에 마우스 오른쪽 버튼을 눌러 ViewPosition이라는 변수 시맨틱을 대입합니다.

이 외에 별다른 설정은 없는 것 같군요. 이제 정점쉐이더를 살펴봅시다.

정점쉐이더
마찬가지로 정점쉐이더의 전체 소스코드부터 보여드리겠습니다.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


float4 gWorldLightPosition;
float4 gWorldCameraPosition;


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


struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   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);


   return Output;
}


정점쉐이더 입력데이터 및 전역변수
일단 정점쉐이더 입력데이터를 보죠. 새로 필요한 정점정보가 있나요? 아무리 생각해도 별 다른 게 안 떠오르는 거 보니 없는 거 같네요. 난 반사광에 사용했던 입력구조체를 그냥 사용해도 될 거 같습니다.

그렇다면 전역변수는 어떻죠? 방금 전에 추가했던 gWorldCameraPosition을 선언해야겠죠? 다음의 코드를 추가합니다.

float4 gWorldCameraPosition;

정점쉐이더 출력데이터
이제 정점쉐이더 출력데이터를 살펴보도록 하죠. 난 반사광에서 그랬던 것처럼 정점쉐이더에서 정 반사광을 계산한 뒤에 픽셀쉐이더에 전달해 주면 될까요? 불행히도 그렇진 않습니다. 정 반사광을 구하려면 코사인 값에 거듭제곱을 해야 하는데 거듭제곱을 한 뒤 보간(interpolate)을 한 결과와 보간을 한 뒤에 거듭제곱을 한 결과의 차이는 엄청납니다. 따라서 정 반사광 계산은 픽셀 쉐이더에서 해야 하니 이 계산에 필요한 두 방향벡터인 R과  V를 구한 뒤에 픽셀쉐이더에 전달해 주도록 하겠습니다. 다음의 코드를 VS_OUTPUT에 추가합시다.

   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;

정점쉐이더 함수
이제 정 반사광을 계산하는데 필요한 두 방향벡터를 구해보죠. 카메라 벡터는 어떻게 구한다고 했었죠? 그냥 카메라 위치로부터 현재위치까지 선을 그으면 된다고 했죠? 입사광의 방향벡터를 구하는 것과 별 다를 바가 없겠네요. 입사광의 방향벡터를 구하는 코드 바로 아래에 다음의 코드를 추가합니다.

   float3 viewDir = normalize(Output.mPosition.xyz - gWorldCameraPosition.xyz);
   Output.mViewDir = viewDir;

이제 정 반사광의 방향벡터를 구할 차례입니다. 이 때, 빛의 입사각과 출사각이 같다고 말씀드렸었죠? 그럼 반사벡터를 구하는 수학 공식이 필요하겠군요. 근데 이런 공식은 굳이 기억하지 않으셔도 됩니다. (저도 수학책 다시 열어봐야 압니다. -_-) 여태까지 그랬던 것처럼 당연히 이런 것을 척척 처리해주는 HLSL 함수가 있겠죠? reflect()라는 함수입니다. reflect는 첫 번째 인자로 입사광의 방향벡터를 두 번째 인자로 반사 면의 법선을 받습니다. Output을 반환하기 바로 전에 다음의 코드를 입력합니다.

   Output.mReflection = reflect(lightDir, worldNormal);

자, 이제 두 벡터를 다 구해봤으니 정점쉐이더에서 할 일은 끝났습니다.

픽셀쉐이더
마찬가지로 픽셀쉐이더의 전체 코드부터 보여드립니다.

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


float4 ps_main(PS_INPUT Input) : COLOR
{
   float3 diffuse = 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);
   }


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

   return float4(ambient + diffuse + specular, 1);
}


우선 정점쉐이더 출력데이터에서 가져올 두 벡터를 PS_INPUT 구조체에 추가합니다.

   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;

이전에 diffuse를 구했던 코드 바로 밑에 새로운 코드들을 추가하겠습니다. 우선 mReflection과 mViewDir을 다시 한번 정규화시켜 줍니다. 정점쉐이더에서 이미 단위벡터로 만들었던 이 벡터들을 다시 정규화해 주는 이유는 보간기를 거치는 동안 그 값이 흐트러질 수 있기 때문입니다. (보간기가 선형적(linear)으로 보간을 해서 그렇습니다.)

   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);
   }

위에서 난반사광의 양이 0% 이상일 때에만 정 반사광을 계산하는 거 보이시죠? 난 반사광이 존재하지 않는 표면에는 이미 빛이 닿지 않으므로 정 반사광이 존재할 수가 없기 때문입니다. 내적을 구할 때 -viewDir을 사용한 것도 보이시죠? 난 반사광을 구할 때와 마찬가지로 두 벡터의 밑동이 만나야 올바른 내적의 결과를 구할 수 있기 때문입니다.

또한 거듭제곱을 할 때 pow() 함수를 이용한 것도 눈 여겨 봐주시기 바랍니다. 여기서는 20번 거듭제곱을 했는데 각 물체마다 이 값을 다르게 하는 것이 보통입니다. (거듭제곱의 수가 높을 수록 정반사광의 범위가 타이트해집니다. 숫자를 바꿔보면서 실험해보세요.)  따라서 이 값을 float형의 전역변수로 선언해주는 게 보다 나은 방법이 되겠습니다. 이 정도는 독자 분들의 몫으로 남겨두도록 하지요.

이제 결과를 반환할 차례입니다. 일단 정 반사광의 효과만을 보기 위해 specular만을 반환해볼까요? 이전에 있던 return문을 다음과 같이 바꿉니다.

  return float4(specular, 1);

이제 쉐이더를 컴파일한 뒤 실행해보면 다음의 결과를 보실 수 있을 것입니다.

그림 4.11. 난 반사광에 비해 매우 강렬하고 타이트한 하이라이트를 보여주는 정 반사광

이제 정 반사광이 어떤 건지 확실히 보이시죠? 여기에 난 반사광을 더하면 보다 완벽한 조명효과가 되겠네요. return 코드를 다음과 같이 바꿔봅시다.

    return float4(diffuse + specular, 1);

위 코드에서 난 반사광과 정 반사광을 더하면 그 결과가 1이 넘는 경우가 있는데 크게 걱정하지 않으셔도 됩니다. 그런 경우엔 알아서 1이 됩니다. (현재 하드웨어 백버퍼의 포맷이 8비트 이미지이기 때문입니다. 부동소수점 텍스처를 사용하면 1 이상의 값을 저장할 수도 있습니다.)

이제 정점쉐이더와 픽셀쉐이더를 각각 컴파일 하신 뒤 미리 보기 창을 보면 다음과 같은 결과가 보이죠?

그림 4.12. 난 반사광 + 정 반사광

자, 이 정도면 훌륭한 조명효과입니다. 하지만 공의 왼쪽 밑부분이 칠흑같이 어두운 게 좀 망에 안 드는군요. 앞서 말씀 드렸다시피 실제세계에서는 간접광이 저 어두운 부분을 비춰줄 텐데 말이지요. 그럼 아주 간단하게 주변광을 정의해줘서 저 부분을 조금이나마 밝혀볼까요? 주변광을 10%로 선언해서 ambient 변수에 대입해주도록 합시다.

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

그리고 최종 반환 값에 ambient를 추가합니다.

   return float4(ambient + diffuse + specular, 1);


이제 결과가 아래와 같이 바뀔 겁니다.

그림 4.13. 주변광 + 난 반사광 + 정 반사광

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

우선 '제3장: 텍스처매핑'에서 사용했던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음, 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장합니다. Sphere.x와 Lighting.fx라는 파일이름을 사용하도록 하겠습니다.

이제 비주얼 C++에서 솔루션 파일을 엽니다.

자, 그럼 전역변수를 먼저 살펴보겠습니다. 일단 이 장에서는 텍스처를 사용하지 않으니 저번 장에서 선언했던 텍스처 변수, gpEarthDM를 지우겠습니다. 그 다음, 쉐이더 변수의 이름을 gpTextureMappingShader에서 gpLightingShader로 바꿉니다.

이제 새로운 변수들을 선언할 차례입니다. 광원의 위치와 카메라의 위치가 필요했었죠? 이 둘은 모두 월드공간 안에 있었네요. 렌더몽키에서 사용했던 빛의 위치를 다시 사용하겠습니다.

// 빛의 위치
D3DXVECTOR4                gWorldLightPosition(500.0f, 500.0f, -500.0f, 1.0f);

카메라 위치는 예전에 RenderScene() 함수 안에서 사용했던 값을 그대로 가져왔습니다.

// 카메라 위치
D3DXVECTOR4                gWorldCameraPosition( 0.0f, 0.0f, -200.0f, 1.0f );

이제 CleanUp() 함수로 가봅시다. 더 이상 gpEarthDM 텍스처를 사용하지 않으니 이를 해제하는 코드를 지웁니다.

다음은 LoadAssets() 함수 입니다. 우선 gpEarthDM 텍스처를 로딩하는 코드를 삭제합니다. 그리고 쉐이더의 파일명을 Lighting.fx로 바꿉니다. gpTextureMappingShader라는 변수명을gpLightingShader로 바꾸는 것도 잊지 마세요.

    // 텍스처 로딩


    // 쉐이더 로딩
    gpLightingShader = LoadShader("Lighting.fx");
    if ( !gpLightingShader )
    {
        return false;
    }

마지막으로 RenderScene() 함수를 보겠습니다. 일단gpTextureMappingShader 라는 변수명을 모두 찾아gpLightingShader로 바꿉니다. 이제 뷰행렬을 만드는 코드를 보죠. 뷰행렬을 만들 때 사용했던 vEyePt라는 변수가 있었죠? 이 변수의 값이 앞서 정의했던 gWorldCameraPosition의 값과 동일하니 gWolrldCameraPosition의 값을 사용하도록 하지요.

예전에 아래처럼 되어 있던 코드를

    D3DXVECTOR3 vEyePt( 0.0f, 0.0f, -200.0f );

다음과 같이 바꿉니다.

    D3DXVECTOR3 vEyePt( gWorldCameraPosition.x, gWorldCameraPosition.y,
        gWorldCameraPosition.z );

이제gpLightingShader->SetTexture() 코드를 지웁니다. 이 장에서 만든 쉐이더에는 텍스처를 사용하지 않으니 이 코드가 필요 없습니다. 그럼 마지막으로 광원의 위치와 카메라의 위치를 쉐이더에 전달해 줍니다. 이들의 데이터형은 D3DXVECTOR4이므로 쉐이더에서 SetVector()를 호출합니다.

    gpLightingShader->SetVector("gWorldLightPosition", &gWorldLightPosition);
    gpLightingShader->SetVector("gWorldCameraPosition", &gWorldCameraPosition);

이제 코드를 컴파일 한 뒤 실행해보시죠. 아까 렌더몽키에서 보셨던 것과 동일한 결과를 볼 수 있죠?

기타 조명기법
여전히 대부분의 게임이 사용하는 조명기법은 람베르트 + 퐁이지만 최근 들어 다른 조명기법들을 사용하는 게임들이 늘어나고 있습니다. 조명기법을 좀 더 심층적으로 연구하고 싶으신 독자 분들을 위해 몇 가지 기법을 언급하겠습니다.


  • 블린-퐁(Blinn-Phong): 퐁과 거의 비슷한 기법. 현재도 많이 사용함
  • 오렌-네이어(Oren-Nayar): 표면의 거친 정도를 고려한 난 반사광 조명기법
  • 쿡-토런스(Cook-Torrance): 표면의 거친 정도를 고려한 정 반사광 조명기법
  • 구면조화 조명기법(spherical harmonics lighting): 오프라인에서 간접광을 사전 처리한 뒤, 실시간에서 이를 주변광으로 적용할 때 사용할 수 있음


정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 람베르트 모델은 난 반사광을 계산하는 기법으로 코사인 함수를 사용한다.
  • 퐁 모델은 정 반사광을 계산하는 기법으로 코사인 값을 거듭제곱 한다.
  • 벡터의 길이를 1로 바꾸면 내적을 구하는 것만으로도 코사인 함수를 대신할 수 있다.
  • 동일한 계산을 어느 쪽에서도 할 수 있다면 픽셀쉐이더 보다는 정점쉐이더에서 한다.
  • 이 장에서 배운 조명보다 훨씬 사실적이고 복잡한 기법들이 존재한다. 그 중 일부는 이미 몇몇 게임에서 쓰이고 있다.


이제 조명기법까지 마쳤으니 쉐이더의 기초는 다 배운 거나 다름없습니다. 다음 장부터는 여태까지 배웠던 지식들을 잘 혼합하여 보다 실용적인 기법들을 구현해 보겠습니다. 제1~4장 중에 잘 이해가 안 되는 내용이 있었다면 다시 한 번 복습을 하신 뒤에 제5장으로 넘어오시기 바랍니다.

댓글 23개:

  1. 오늘도 멋진 강의 잘 보았습니다.

    실습해야지~ :D

    답글삭제
  2. 멋진 강의 잘 봤습니다.

    아직은 전 강의까지 이해가(강의는 멋진되, 제 능력이 부족하여) 될듯 안될 듯. 몇번씩 반복 코딩과 반복 학습 중입니다.

    다시금 멋진 강의 고맙습니다.

    답글삭제
  3. 항상 잘 보고 있습니다. 감사합니다.

    답글삭제
  4. @JazzEz2dj, @김경진: 감사합니다 ^_^

    @달리나음: 앗 새로운 분이시다! 덥썩~ 반갑습니다~

    답글삭제
  5. 질문 있습니다.
    diffuse엔 saturate를 실행한 상태라서 0~1 사이 값만 들어 있다고 이해하고 있습니다.
    그런데 왜 if (diffuse.x > 0)를 해주어야 되는지 궁금합니다. ^^

    답글삭제
  6. diffuse.x가 0이라면 if (diffuse.x > 0) 가 false가 되거든요 ^_^ diffuse.x >= 0 이 아니니~

    답글삭제
  7. 아 네..제가 코딩을 잘못했을지도 모르겠습니다.
    if 문을 주석 처리 하여도, 셈플 구에서 보이는 것이 같이 나와서 질문 하였습니다. ^^

    삭제했다고 남네요...^^;;(밑에 문장 추가 할려고 삭제했는데..)

    여기까지 이해 40% , 코드 외우기 60%로 잘 따라왔습니다.

    답글삭제
  8. 넵... 삭제된 댓글은 제가 영구삭제를 직접 해야하더라구요. 영구삭제 했습니다.

    if 문 삭제해도 비슷한건 아마 현재 조명에서 구를 비추는 각도자체가 90도 넘는 경우가 없어서 일거에요. gWorldLightPosition을 0,0,500으로 넣어보면 if문을 삭제했을 때 구의 가장자리에 어이없게 스펙조명이 드는걸 보실 수 있을 거에요.

    그냥 빛이 직접 안드는 곳이면 (light와 normal이 90도를 넘는 경우, 내적 <= 0인 경우) 정반사광도 안들어야 한다는 뜻의 if 문이죠.

    40% 이해.. 아주 훌륭하세요!

    답글삭제
  9. 여전히 부담은 있죠.. 가능하면 안쓰는게 좋긴 합니다. 굳이 쉐이더 뿐만 아니라 C++에서도 안쓰는게 좋긴 합니다.. 뭐 근데 써야한다면 또 서야죠~ 흣~

    답글삭제
  10. 안녕하세요. 강좌 정말 감사드립니다.
    근데 궁금한게 있는데요.

    버텍스셰이더에서 난반사광값을 구하고 픽셀셰이더에서 정반사광 값을 구해서
    더하는거 잖아요?

    그런데 제 생각에는 어차피 버텍스셰이더에서 벡터값들 구해서 픽셀셰이더로 넘겨주는데
    그냥 버텍스 셰이더에서만 정반사까지 모두 구현해도 값은 상황이 될거라 생각했는데

    실제로 그렇게 해보니 픽셀셰이더를 사용한 정반사광이 더 부드럽더라구요!

    픽셀셰이더안에서 다시 정점마다 벡터값을 구해서 쓰는것도 아닌데,
    왜차이가 나는거죠?

    아니면제가 뭔가 잘못 한걸 까요?

    답글삭제
    답글
    1. 이미 본문에 설명이 있습니다. (좀 안보이게 짧지만.. -_- 그래서 다시 붙여넣습니다)

      "이제 정점쉐이더 출력데이터를 살펴보도록 하죠. 난 반사광에서 그랬던 것처럼 정점쉐이더에서 정 반사광을 계산한 뒤에 픽셀쉐이더에 전달해 주면 될까요? 불행히도 그렇진 않습니다. 정 반사광을 구하려면 코사인 값에 거듭제곱을 해야 하는데 거듭제곱을 한 뒤 보간(interpolate)을 한 결과와 보간을 한 뒤에 거듭제곱을 한 결과의 차이는 엄청납니다. 따라서 정 반사광 계산은 픽셀 쉐이더에서 해야 하니 이 계산에 필요한 두 방향벡터인 R과 V를 구한 뒤에 픽셀쉐이더에 전달해 주도록 하겠습니다. 다음의 코드를 VS_OUTPUT에 추가합시다."

      삭제
    2. 아하 그렇군요!!! 빠른답변 감사합니다!!
      언제나 좋은강좌, 좋은답변 감사합니다.
      셰이더를 많이 배우고 갑니다!

      삭제
  11. 전역변수 gWorldCameraPosition 에 대해 질문이 있습니다.
    ViewPosition 시맨틱을 지정하라고 설명되어있는데 ViewPosition 이라는 이름의 시맨틱을 찾을 수 없어서요... 혹시 View 시맨틱이 아닌가 짐작해봅니다.
    만약에 View 시맨틱이라면 gViewMatrix 전역변수와 같은 역할이니까 gWorldCameraPosition 변수가 필요 없는건 아닌지요. ^^

    답글삭제
    답글
    1. 자문자답이군요.
      float4 변수로 만들어야 하는걸 float4x4 로 잘못 봤습니다. T_T

      삭제
    2. 덕분에 전 편해졌습니다. 감사합니다 ^_^

      사실 카메라위치를 뷰행렬의 머지막 행에서 구해올수도 있지만...뷰행렬은 vs에서 카메라 위치는 ps에서 주로 쓰던 습관이 있어서 그냥 전 따로 지정해서 씁니다.

      삭제
  12. HLSL 공부하러 왔다가 강좌 잘 보고 갑니다. 이제 막 공부를 시작하긴 했지만 제가 보기엔 정점 쉐이더에서 출력하는 정보들에 문제가 있는 것 같습니다.

    우선 mViewDir은 정점 쉐이더에서 normalize 해주면 안 됩니다. 예를 들면, 카메라 (0,1,0)에서 바라보는 두 정점 (0,0,0)-(1000,0,0) 중간 지점의 mViewDir은 수평에 가까워야지 45도 각도가 나오면 안 됩니다.

    그리고 mReflection은 보간되어서는 안 되는 벡터 입니다. 예컨데 두 정점의 법선이 직각인 경우 두 정점에서의 반사각은 서로 180도를 이루게 됩니다. 즉 모든 보간값이 제로 벡터가 되는 거죠. 사실 이 경우가 아니더라도 반사각을 보간한다는 것은 (정석대로 보간된 법선으로부터 반사각을 구하는 방식과 비교하여) 수학적인 근거가 없는 거 같네요.

    다만 mDiffuse은 정점 쉐이더를 통해 보간이 가능한 것 같습니다. 하지만 TEXCOORD는 경우 선형 보간이 아닌 perspective correction이 들어간 보간입니다. 이것이 의도한 바였는지 우선 확인해볼 필요가 있지요.

    결론적으로는 법선을 보간하여 픽셀 쉐이더에서 조명을 계산하는 것이 올바른 퐁 쉐이딩인 것 같네요. 예제의 경우 정점들이 치밀하여 오차가 눈에 띄지 않지만 terrain같이 coarce vertice의 경우 artifact가 보이겠죠.

    답글삭제
    답글
    1. 어라? 전에 답글을 달았었는데 이상하게 사라졌네요. SJ님 말씀이 맞습니다. 수학적으로는 사실 틀립니다. 보통 픽셀쉐이더 쪽에서 부하가 걸리는 경우가 많다보니 가능한 정점쉐이더에서 많은 계산을 수행해서 속도를 빠르게 해보려는 꼼수입니다. SJ님이 말씀하신 것처럼 육안으로는 문제가 거의 안보일 정도니까요.

      Terrain 쪽은 제가 안해봐서 모르겠습니다만 문제가 보일수도 있겠네요. 그럼 terrain용으로 별도의 쉐이더를 -_-;;;;

      삭제
  13. 포프님의 책 잘보고 있습니다. 한가지 질문이 있는데요
    어떤경우에는 Specular를 구할때 Reflection Vector와 Light Vector와의 내적을 구해서 거듭제곱으로 스펙큘러를 구하는것을 본적이 있습니다. Camera Vector를 썼을 경우에는 if문을 써서 Diffuse > 0 일때만 나타나도록 하는것 같은데요
    만약 Light Vector로 내적을 구한다면, 이런 조건을 달지 않고 바로 스펙큘러를 구할수 있을것 같은데 굳이 Camera Vector를 쓰신것은 어떤 이유가 있으신건가요?
    제가 쉐이더 초짜라 아무것도 몰라서요..^^;; 답변 부탁 드리겠습니다.

    답글삭제
    답글
    1. light 벡터를 reflect하나 camera vector를 reflect하나 결과는 같습니다. 어차피 두 벡터가 이루는 각은 변하지 않으니까요.. 어느경우에도 diffuse > 0을 체크해주는게 옳다고 생각합니다 이유는.. diffuse <= 0 인 경우(light와 법선이 이루는 각이 90도 이상)에는 아예 그 픽셀에 빛이 들지 않는다는 이야기므로 정반사광이 존재할 수가 없거든요..(물론 표면이 거친 경우에는 표면을 타고 흐르는 빛이 세어나올수도 있으나.... 현재 사용하는 조명모델이 거친 표면을 고려하지 않으므로 일단 패스 -_-)

      삭제
  14. 음.. 질문이 있습니다.
    스펙큘러의 연산중 거듭제곱 이전의 연산까지 버텍스 셰이더에서 하고
    스펙큘러의 거듭제곱 연산만 픽셀셰이더에서 하게 해봤는데요
    되게 이상하게 나옵니다. 왜 그럴까요?

    답글삭제
  15. 안녕하세요. 책 사서 공부하다가 질문하려고 찾았는데요.

    프레임워크에서 gpLightingShader->SetVector("gWorldCameraPosition", &gWorldCameraPosition);

    이부분을 빼면 정반사광만 안나오던데 왜그런건가요??

    답글삭제
    답글
    1. 정반사광을 계산할때 카메라 위치를 써야하는데.. 그 위치를 (0, 0, 0)으로 넣어주셔서 결과가 0이 되어버려서 그렇습니다. 위치벡터는 길이가 0이면 안되는데 (0, 0, 0)이면 길이가 0이거든요

      삭제