RGBM이란?
fat 버퍼(채널당 16비트)를 써서 HDR 렌더타겟을 저장하는게 가장 직관적인 방법입니다. 하지만 이러기엔 속도가 너무 느렸고 메모리도 많이 잡아먹었죠. 그래서 보통 HDR을 8비트/채널 버퍼에 인코딩(패킹)하곤 했었습니다. 다양한 패킹 방법들이 존재하는데 아마 그중에서 가장 널리 사용했던건 RGBM 이었을겁니다. (이 링크에는 LogLUV 패킹 방법에 대해서도 잘 나와있으니 잘 모르시는 분은 읽어보세요...무.. 물론 영어 -_-)
RGBM의 단점
근데 문제가 있었습니다.. 아티스트분들이 엄청 좋아하는거 하나를 할 수 없거든요.. 넵. .알파 블렌딩 입니다.. -_-
RGBM버퍼를 사용하면 알파블렌딩을 할 수 없는게 현실입니다. 달리 말하면 투명한 물체들을 못쓴다는거지요... 왜냐구요? RGBM을 사용할때 블렌딩을 하려면 다음과 같은 공식을 돌려야 합니다.
( DestColor * DestMultiplier * InvSrcAlpha + SrcColor * SrcAlpha ) / SrcMultiplier
여기서 몇몇 매개변수들을 좀 살펴보지요.
- DestMultiplier: DestAlpha 채널에 저장되어있습니다. 블렌딩 유닛이 쉽게 사용할수 있지요
- SrcMultiplier: 셰이더에서 알파채널로 출력(output)합니다
- SrcAlpha: 투명값입니다. 근데.. 이걸 어디다 저장하죠? 보통 같으면 알파채널이죠.. 근데 RGBM을 사용하면 .... 이미 multiplier를 알파채널에 출력하니... 바.. 방법이..... 아.. 맞다.. "premultiplied alpha." 라는게 있었죠? 셰이더 안에서 SrcColor에 알파값을 미리 곱해주면 됩니다. 대충 해결했군요.
- InvSrcAlpha: 하지만 바로 여기가 모든게 개판나는 곳입니다. 투명값을 더 이상 출력하지 않으니 InvSrcAlpha를 블렌딩 유닛에서도 쓸수 없고, 이걸 셰이더안에서 DestColor에 미리 곱해줄 방법도 없지요. 넵.. 망했습니다 -_-;
기존의 해결방법
그래서 이 문제를 해결하기 위해 먼 짓을 했냐구요?.... 아무짓도 안했습니다... -_- 그냥 렌더타겟을 하나 더 만들어서 문제를 피해갔지요. 모든 투명한 물체들을 HDR 인코딩 없이 이 새로운 버퍼에 그렸습니다. 그리고 나중에 그 결과를 신(scene) 버퍼에 합쳤지요.
간단히 말해 불투명(opaque)한 물체들은 HDR에 그리고 투명한 물체들은 LDR에 그리는 겁니다. 하지만 이러면 전체화면(fullscreen) 패스를 한 번 더 돌려서 버퍼들을 합쳐줘야 하므로 속도저하의 문제가 있었습니다. 그래서 속도저하를 좀 줄여보고자 투명버퍼의 크기를 절반 또는 1/4로 줄여주는 꼼수를 쓰기도 했지요.
뭐, 이래저래 여전히 RGBM 버퍼만 하나 쓰는것보단 메모리를 더 먹었습니다.
혼합가능한(Blendable) RGBM
자, 그럼 제가 최근에 고안해낸 꼼수를 알려드리겠습니다. 혼합가능한(Blendable) RGBM이라 이름을 붙였구요. 일단 간단히 요약부터 해보죠.
- 투명한 물체들을 수학적으로 올바른 방법으로 혼합합니다
- 따라서 별도의 메모리를 필요로 하지 않습니다.
- 투명한 픽셀들은 이전에 HDR 버퍼에 그려졌던 불투명 픽셀의 multiplier를 그대로 사용합니다.
- 하지만 정밀도(precision)의 문제가 생길수 있는데요. 특히 불투명 픽셀과 투명 픽셀의 값이 크게 차이가 날 경우 그렇습니다.
- 투명 물체가 픽셀값을 어둡게 만들경우 벤딩(bending) 효과가 나타날 수 있습니다.
- 또, 투명 물체가 픽셀을 어느정도 이상으로 밝게 만들수도 없습니다. (불투명 픽셀의 multiplier가 허용하는 것 이상으로 밝게 만드는게 불가능합니다)
자..그럼 이제 좀 차근차근 대충대충 설명해보죠..
일단 수학공식에 맞추기
RGBM 혼합공식을 다시 봅시다:
( DestColor * DestMultiplier * InvSrcAlpha + SrcColor * SrcAlpha ) / SrcMultiplier
InvSrcAlpha 때문에 망했다고 전에 말씀드렸죠? 이건 셰이더에서 알파채널 값에 SrcMultiplier를 넣기 때문이었습니다. 그럼 이걸 어떻게 해결할까요? 간단 합니다.. 그냥 SrcMultiplier를 버리면 됩니다.. -_-;;;;; 농담이 아닙니다.. -_-;;;; SrcMultiplier와 DestMultiplier를 같게 만들면 됩니다. 즉 DestMultiplier만 쓰면 되죠. 이따위 짓을 하고 나면 공식이 이렇게 변합니다:
( DestColor * DestMultiplier * InvSrcAlpha + SrcColor * SrcAlpha ) / DestMultiplier
이걸 풀면 이렇게 간단히 되죠.
DestColor * InvSrcAlpha + SrcColor * SrcAlpha / DestMultiplier
자, 이제 알파채널을 사용하지 않으니 SrcAlpha를 그냥 알파채널에 써주면 됩니다. 끝~
올바른 블렌딩 렌더스테이트 설정
전 하드웨어 블렌딩 연산을 가지고 노는걸 좋아합니다. 가끔 기발한 짓을 할수 있거든요. 예전에 스크린 스페이스 데칼 만들 때도 그런짓을 했지요. 이번에도 또 사고쳤답니다 -_- ㅋㅋ
위에서 보여드렸던 블렌딩 공식에 껴 맞추려면 다음과 같이 렌더스테이트를 바꿔주면 됩니다:
- SrcBlend: DestAlpha
- DestBlend: InvSrcAlpha
이렇게 하고나면 위의 혼합공식에 매우 가까워졌지요. 하지만 아직 한가지가 빠져있군요. SrcAlpha를 곱해주는 부분입니다. 이미 SrcBlend를 DestAlpha로 해줬으니 SrcAlpha를 블렌딩 하드웨어에 설정해줄 방법은 없군요. 해법이 뭐냐구요? 전에 했던것처럼 그냥 premultiplied alpha를 다시 쓰면 됩니다. ^_^ 셰이더에서 SrcColor에 SrcAlpha를 미리 곱해주세요.
자, 그럼 모두 다 해결된건가요?.... 불행히도 아닙니다.. ( -_-) 위 블렌딩 스테이트 까지 설정해준 다음엔 공식이 이렇게 되거든요.
자, 그럼 모두 다 해결된건가요?.... 불행히도 아닙니다.. ( -_-) 위 블렌딩 스테이트 까지 설정해준 다음엔 공식이 이렇게 되거든요.
DestColour * InvSrcAlpha + SrcColor * SrcAlpha * DestMultiplier
퉤.. -_- DestMultiplier를 나눠줘야 하는데 곱해주고 있군요.... 이걸 고치려면 RGBM 인코딩/디코딩 하는 법을 좀 바꿔주면 됩니다.
RGBM 인코딩/디코딩 방법 바꾸기
RGBM 인코딩/디코딩을 하는 오리지날 방법은 다음과 같습니다.
- M: max(RGB)
- encoding: RGB / M
- decoding: RGB * M
- alpha encoding: M / 6
이걸 제대로 돌게 만드려면 M을 역으로 뒤집어주면 됩니다. 그러면 인코딩시엔 곱하고 디코딩시엔 나눠줄 수 있습니다. M을 뒤집어주면 대충 이렇게 됩니다.
- M: 1 / clamp( length(RGB), 1, 6 )
- encoding: RGB * M
- decoding: RGB / M
- alpha encoding: M
max(RGB)대신에 length(RGB)를 사용한 이유는 나중에 투명한 물체가 픽셀을 더 밝게 만들 수 있도록 좀 마진(margin)을 준것입니다. 여기서 M을 계산하는 방법은 좀 핵(hack)이라 생각하는데요. 저 공식에 있는 1(두번 나옴)을 0.125같이 작은 수로 바꾸면 LDR에서 정밀도(precision)가 좀 더 나을겁니다. 그런데 이걸 1으로 두면 나중에 투명패스에서 사용할 수 있는 색상값의 범위가 최소한 0~1은 되니.. 그걸 보장하기 위해 저렇게 뒀습니다.. 뭐든간에... 이것보다 훨씬 괜찮은 M 계산법을 다른 분들이 찾아낼거라 믿습니다... :)
드디어 새로운 블렌딩 공식!
이제 이리저리 다 뜯어 고쳤으니 드디어 공식이 완성되었습니다:
( DestColor / DestMultiplier * InvSrcAlpha + SrcColor * SrcAlpha ) * DestMultiplier
이걸 전개하면 다음과 같이 됩니다.
DestColour * InvSrcAlpha + SrcColor * SrcAlpha * DestMultiplier
무하하하하 -_- 블렌딩 렌더스테이트에서 나온 계산하고 똑같죠? 드디어 완성되었습니다.. -_-v
Alpha 쓰기 끄기
잠만요.. 근데 투명값을 알파채널로 출력(output)해주면 HDR 버퍼에 이미 적혀있던 multiplier를 덮어 쓰겠네요? 그럼 안되지요. 알파값은 블렌딩 유닛에서만 필요한거니, 이게 렌더타겟에 적히는 걸 막아야합니다.
이것도 역시 렌더스테이트에서 alpha 쓰기를 마스킹해주는 것만으로 간단히 해결됩니다.
이것도 역시 렌더스테이트에서 alpha 쓰기를 마스킹해주는 것만으로 간단히 해결됩니다.
가산혼합(Additive Blending)
제 방법은 가산혼합과도 잘 동작합니다. 블렌딩 스테이트를 다음과 같이 바꿔주세요
- SrcBlend: DestAlpha
- DestBlend: One
주의점
앞서 밝혔다 싶이, 이 방법에는 두가지 단점이 있습니다.
- 투명한 물체가 픽셀을 너무 어둡게 만들면 벤딩효과가 보일겁니다. 정밀도가 모잘라서 인데요. 이건 이미 버퍼에 있던 픽셀들이 HDR 영역에 있었을때만 보이는 현상입니다.
- 투명한 물체가 픽셀값을 어느정도 이상으로 밝게 만들지 못합니다. 여기서 어느정도라 하면 불투명 픽셀들이 내뱉어낸 multiplier에서 허용하는 한도입니다.
혼합가능한 RGBM을 어느상황에나 사용할 수 있는건 아닙니다. 특별한 상황에서만 사용가능한데요. 실제 게임 만들면서 그런 상황을 본적이 있습니다. 뭐든간에 성능 또는 메모리상의 이유로 버퍼를 따로 하나 만들수 없고 비주얼 퀄리티를 약간 희생시킬 수 있다면 사용하세요. 결국 비주얼 퀄리티와 메모리간의 밸런스 문제니까요.
p.s.
새로운 콘솔용 게임을 만드시는 분들에게 HDR 패킹은 더이상 필요없을지도 모르겠습니다. 하지만 아직도 후진(?) 하드웨어 용으로 게임을 만드시는 분들이 계실테니, 그분들께 도움이 되었으면 하는 맘에서 오랜만에 동영상이 아닌 블로그 글을 썼습니다.. ^_^
포프였습니다.
오호.. 좋은 글 잘 봤습니다.
답글삭제저도 틈틈히 자체 렌더링엔진을 만들게되면 셰이더로 이것저것 실험해보고 싶은데
...막상 취직하니 좀처럼 기회가;;
자 떄려치시고.. 그 직장을 저에게..(응?)
삭제