2011년 11월 24일 목요일

디퍼트 라이팅 엔진에서 Oren-Nayar 조명 쓰기


그래픽스 프로그래밍 관련 블로그를 가끔 보는 편이다. 일단 이게 새로운 기법을 배우는데 도움이 되니까... 그런데 가끔씩 실제 경험보다는 그냥 이론에 치우쳐 사실이 아닌걸 우기는 블로그 포스트들이 있어서 좀 맘이 불편하다.

일례로 다음은 다른 그래픽 프로그래머와 나눈 대화 중 하나:
  • 다른놈: "우리 엔진은 디퍼트 라이팅이라 오렌 네이어(Oren-Nayar) 조명을 쓰는건 매우 힘들어"
  • 나: "먼 개소리? -_- 졸 쉬워~"
  • 다른놈: "아냐아냐 그렇지 않아. 이 블로그에서 힘들다고 했어."
  • 나: "으음... -_- 근데 난 이미 2년전에 울 엔진에서 구현했는걸? 매우 쉬워."
  • 다른놈: "허걱~~" ( 땀을 삐질.. -_-;;;; )
그래서 그놈에게 어떻게 해는지를 설명해 줬다...... 그리고 이놈처럼 잘못된 글 때문에 혼돈스러워 하시는 분들을 위해 여기에도 설명을....

우선 사전 지식.....

Oren-Nayar가 뭔지 모르시는 분들은 여기를 우선 보시길... 텍스처 룩업을 통해 최적화하는 방법도 소개함... 그 외에 울 팀에서 자체적으로 만든 approximation 함수도 있음... 내 맘대로 Oren-Nayar 조명을 간단히 설명하자면, 거칠음(roughness)을 조명 계산공식에 반영하는 diffuse 조명 모델이라고 하겠음....

그리고... 디퍼드 라이팅이 뭔지 모르시는 분들은 여기를 볼 것.

이 정도면 사전지식도 된듯 하니 본격적으로 설명을 하면..... Oren-Nayar조명을 하려면 기타 조명에 비해 다른 정보 하나가 필요하다. 거칠은 정도. 그럼 디퍼드 라이팅 엔진에서 Oren-Nayar를 하려면 어떻게 할까? 거칠은 정도를 G 버퍼에 저장하면 끝.... 물론 G 버퍼에 거칠은 정도를 저장하는 법은 여러가지가 있는데... 이게 사람들이 많이 혼돈스러워 하는 부분일 듯 하다.

대부분의 디퍼드 라이팅 엔진은 법선(normal)의 XY를 G 버퍼의 R16G16에 저장하는 듯 하다. 그러면 이 별도의 정보(거칠은 정도)를 저장하려면 렌더타겟이 하나 더 필요하단 이야기지... 렌더타겟을 하나 더 사용하면 메모리도 더 먹고 속도도 느려지므로 그닥 좋은 방법은 아님.

다른 방법은 법선을 8비트 채널에 저장하는 방법... 이것의 단점은 8비트 자체의 정밀도(precision)가 떨어져서 조명 결과가 부드럽지 않게 나오는 것.... 하지만 Crytek에서 발표한 best-fit normal을 사용하면 R8G8B8 채널에 법선을 제대로 저장할 수 있다. (스페이스마린에서도 한동안 이 방법을 사용했었음.). 일단 이렇게 법선을 저장하고 나면 마지막 alpha채널에 거칠은 정도를 저장할 수 있으니.... 문제해결........ 일까?

그러니 내가 구현한 방법은 이보다는 조금 더 복잡했다. 스페큘라 파워(specular power)도 저장해야 했거든... 곰곰히 생각해보니 스펙 파워에 굳이 8비트를 전부다 사용할 일이 없더라... (정말 127이상의 스펙 파워를 쓰는 경우가 얼마나 있을까? 11 미만의 스펙 파워를 쓰는 경우도 거의 없을듯?) 그래서 스페큘러 파워에 7비트만을 사용하고 거칠은 표면인지를 나타내는 플래그에 1비트를 사용하기로 했다.

on/off로 거칠은 정도만 표현하면 되냐고? 물론 아니다. 조금더 생각해보면 거칠은 정도란 결국 스페큘러 파워의 역함수임을 알 수 있었다. 거칠은 표면은 조명을 더욱 고르게 산란시키므로 스페큘러 파워가 적어야 하고, 매끄러운 표면은 그 반대이므로 스페큘러 파워가 높아야 하고....

이런 관찰 결과를 내 맘대로 대충 함수로 짜보니 이런 꼴이 되더라...

G-버퍼 저장법
  • RGB: 법선
  • A: 거칠음/스펙 파워의 짬뽕
매우 간략화시킨 디퍼드 라이팅 쉐이더 코드

float4 gval = tex2D(Gbuffer, uv);

// Crytek의 기법을 사용하여 법선을 decode
float3 normal = decodeNormal(gval.xyz);  


float specpower = gval.a;
float roughness = 0;
if (specpower > 127)
{
    specpower -= 127;
    roughness = someHackeryCurveFunction(127 - specpower);
}

// 이제 이 변수를 이용하여 올바른 조명을 계산할 것...


이정도면 별로 안어렵지? Xbox 360과 PS3에서도 별로 느리지 않은 방법이었다. 물론 approximation을 이용해 Oren-Nayar를 최적화시키기도 했지만.....

댓글 2개:

  1. 요즘 가장 많은 관심을 가지고 있는 디퍼드 렌더링분야입니다. 좋은 정보 감사합니다.

    답글삭제
    답글
    1. 도움이 되었다니 다행입니다 ^^

      삭제