일부 사진은 클릭하면 영상이 나옵니다.


원문 링크는

http://game.watch.impress.co.jp/docs/series/3dcg/20120314_518785.html

를 참조 하세요


【GDC 2012】니시카와 젠지의 3D 게임 팬을 위한「inFamous 2」강좌
훌륭하지만 아직은 불완전한 SSAO。그 약점을 보완하는 기술에 주목!!



3월5일~ 9일 개최(현지 시간)
장소:San Francisco Moscone Center


3D게임 그래픽스의 커다란 테마로 [음(陰)]과 [영(影)]이 있다. 최근에 [그림자(影)]생성은 Depth Shadow Map 기법이 주류이며, 그림자(影)생성을 위한 섀도우 맵을 여러장 사용하여 그것들을 시점으로부터의 원근에 따라 할당하는 캐스케이드 확장 기법이 주류로 되고 있다. 이러한 어프로치의 그림자(影) 생성은 생성하는 섀도우 맵의 해상도에 의존한 정밀도가 되며, 미세한 그림자(影)를 만들어낼 수 없다.

또 간접광등의 2차광원으로부터의 그림자(影)생성은 현재 쓰이고 있는 방식중에는 절망적이다.
여기서 최근 각광을 받아 온것이 렌더링 결과에 따라 출력되는 Depth(심도)버퍼의 내용을 바탕으로하여
포스트 프로세스 방식의 가상적인 환경광에 대한 그늘(陰)을 생성하는 SSAO(Screen Space Ambient Occlusion)이다

SSAO는 Crytek에 의해 고안되었으며 눈깜빡할 사이에 여러 타이틀에 채택되었지만 그와 동시에 자잘한 문제가 부각되어 왔다. 최근 GDC에서는 이 SSAO의 개량에 관련된 세션이 많이 생겼으며 올해도 예외 는 없었다。「Ambient Occlusion Fields and Decals in Infamous 2」도 그 세션중에 하나 였다. 매우 이해하기 쉬웠고 게다가 구현이 간단해보이니 소개하고 싶다。

【inFamous 2】



■ Screen Space Ambient Occlusion(SSAO)는 완벽하지 않다!


Nathan Reed氏(Rendering Programmer、Sucker Punch Productions)
PS3 전용 타이틀인「inFAMOUS 2(인퍼머스 2)」에서는 새겨넣는 형태의 그림자[陰] 로써 버텍스 단위로 차기차폐 상태를 새겨넣는 고전적인 AO(Ambient Occlusion)와 SSAO 양쪽을 사용하여 다양하고 풍부한 "그림자(陰)" 표현을 하였다.

버텍스 단위의 AO와 SSAO는 각각의 단점과 장점을 서로 감싸는 좋은 보완관계가 성립된다. 버텍스 단위의 AO는 움직이지 않는 정적인 오브젝트 그것도 거대한 건물이 만들어내는 대국적인 그림자( 陰(影)) 생성에는 유효하다

이 그림자(陰(影))의 투영되는 곳의 버텍스를 미리 세세하게 해두지 않으면 안되는 귀찮음이 있지만 SSAO에서는 만들기 어려운 광범위의 그림자((陰(影)))를 만들어 내는데에 적합하다. 이것을 보간하는 존재가 SSAO이다

SSAO는 정적인 오브젝트는 물론, 돌아다니는 동적인 오브젝트에 대해서 국소적인 그림자((陰(影))) 생성을 수행할 수 있다. 구체적인 알고리즘은 아래와 같다.


보고 있는 픽셀이 어느정도 차폐되고 있는 장소의 픽셀인가를 , 그 픽셀에 대응하는 Z값과 그 픽셀 주변에 대응하는 Z값을 읽어들여, 그 값 끼리 비교해서 구한다. 그 비교 연산으로부터 깊이 맵(심도 맵)상의 깊숙히 닫혀진 장소에 있는 픽셀일수록 차폐도가 높다고 하고 "그림자(陰)"색을 입힌다.

간지나게 말하면 깊이맵 상에서 초 로컬한 레이캐스팅을 수행하여 차폐상태를 조사하는 수법이라고
할 수 있다.

하지만 SSAO는 결국은 렌더링 결과에 대해서 수행하는 화면좌표계 포스트 프로세스이기 때문에
몇가지 약점이 있다.

우선 렌더링 결과의 바깥둘레 부근은 (심도버퍼의)정보가 부족하기 때문에 차폐도를 제대로 계산 할 수 없다. 그림자색(陰色)이 엷어지는것 뿐이므로 무시되고있는 케이스가 대부분이다

또 깊이 맵은 시점으로부터 본 깊이 정보밖에 저장되어있지 않으므로 무언가에 차폐되고있는 가에 대한배후(背後)의 차폐도는 계산 할 수 없다.

그것과 씬이나 오브젝트에 대해서 시점까지의 거리가 거의 일정한 게임이라면 눈치채기 어렵지만
씬이나 오브젝트에 대해서 시점을 멀리하거나 가까이 하거나 하면 부자연스러운것을 눈치 채게 된다.

그렇다. SSAO에서 입혀진 그림자색(陰色)의 범위(크기)는 화면좌표계에서의 차폐도의 탐색이 수행되는것뿐이므로 그려지는 그림자색(陰色)의 영역은 화면상의 면적에 대해서 거의 일정하게 되버리는 것이다.

예를 들면, 어떤 막대 모양의 오브젝트의 밑부분에 그려진 그림자"陰"색의 크기에 주목을 하고 보면 그 오브젝트에 찰싹 붙을 정도로 가까이서 봐도 멀리 떨어져서 봐도 화면상에 그려지는 밑부분에 그림자(陰)의 크기는 변하지 않는다.

정확히는 가까이 갔을때는 그 막대의 빝부분이 확대해서 보이기 때문에 그 그림자 색(陰)은 크게 보여야 하며, 멀리 떨어졌을때에는 막대가 작게 보이기 때무문에 그것에 맞춰서 작게 보여야 한다



■ AO Fields 방식


「inFAMOUS 2」에서 AO Fields 방법
「inFAMOUS 2」는 3인칭 시점의 오픈 월드 형태의 액션 게임으로 비교적 카메라가 씬이나 오브젝트에 대해서 가까이 가거나 멀리가거나 할수있는 시스템으로 되어있다.

게다가 초능력을 사용하여 자동차같은 커다란 오브젝트를 집어 던지거나 이동시키거나 할 수 있기 때문에 위에서 말한 SSAO의 약점은 크리티컬하게 보이는 상황이 있다.


그래서「inFAMOUS 2」개발 팀은 SSAO를 독자적으로 연구하여 개량하기로 했다.
그중 하나가「Ambient Occlusion Fields」(AO Fields)이다.

이 방식에서는 각 오브젝트 모델에 짝이되는 AO Fields를 사전 계산을 할 필요가 있다.
AO Fields란 감각적으로 말하자면 [ 그 오브젝트가 모든 방향에서 어떤 다른물체들을 차폐하는 가 ] 라는 정보이다.

이 AO Fields의 크기는 각 오브젝트 모델을 완전히 가리는 직육면체보다도 더욱더 큰 직육면체 로 한다.
이 커다란 직육면체를 xyz 각 축 8~ 16분할하여 복셀로 분할하고 이 각 복셀의 중심으로부터 전방위(6방향)에 대해서 32 x 32텍셀의 큐브맵을 렌더링 한다.

【AO Fields】
AO Fields 이미지(왼쪽)、각 복셀에서의 AO Fields는 각 복셀의 중심으로부터 봤을때의 전방위의 배경을 큐브맵으로 렌더링한 결과로부터 얻는다(오른쪽)

여기까지는 꽤 대담하다. 렌더링 된 큐브맵을 탐색해서 그 중심을 구하고 복셀의 중심으로부터 이 중심으로의 방향을 산출한다. 이것을 차폐 벡터라고 한다. 참고로 여기서 말하는 [ 중심 ] 이라는 것은 큐브맵에 그려져 색칠된 텍셀의 면적으로부터 그 중심을 구하는것 뿐이다. 이것으로 이 복셀의 중심으로부터 본 [차폐 방향≒가장 이 지점이 차폐되고있는 방향]가 구해진다

이 큐브맵에 렌더링된 텍셀의 면적(칠해진 면적)은 이 복셀의 차폐율을 나타내고 있으며 이면적을 바탕으로 [ 이 지점의 차폐율 ]을 구한다.

이렇게 구한 차폐방향(x,y,z)와 차폐율을 텍스처에 저장한다. 차폐방향은 벡터이므로 RGB에 저장하고, 차폐율은 스칼라 이므로 α 채널에 저장한다. 이렇게 계산되었을때 가능한것이 3D 텍스처(3차원의 수치 테이블)로 이것이 그 오브젝트에 짝을 이루는 AO Fields라는 것이 된다.


【AO Fields의 사전계산】

「inFAMOUS 2」의 AO Fields 방식은 Mattias Malmer의 논문 「Fast Precomputed Ambient Occlusion for Proximity Shadows」을 참조하고 있으며, 이 논문을 참고로 AO Fields의 직육면체 사이즈가 결정되었다고 한다

세션에서는 실제「inFAMOUS 2」상의 오브젝트로 AO Fields를 어느정도의 크기로 잘랐는가도 구체적인 값을 알려주었다.

예를들면 자동차에서는 32 x 16 x 8 의 복셀로 구분지어 각 테이블이 32비트 정수였기에 3D텍스처의 용량은 16,384바이트(16KB)로 되어있다. 마찬가지로 공원의 의자는 16x8x8복셀로 4KB, 쓰레기통은 8x8x8 복셀로 2KB. 3D 텍스처라고 해도 꽤 엉성하기에 의외로 사이즈는 작다. 단 사이즈는 작지만 데이터 테이블은 벡터 데이터이며 정밀도가 요구되므로 텍스처 압축(DXT)는 하지 않았다는 것.

【AO Fields의 크기】
「다른 물체의 미치는 차폐상태」를 구하므로 오브젝트 사이즈보다도 필연적으로 큰 영역이라고 생각할 필요가 있다. 그림의 회색 상자가 오브젝트를 감싸는 보정된 사이즈의 직육면체라고 했을때 파란 상자가 AO Fields。「inFAMOUS 2」에서는 참고논문을 본 떠ε=0.25를 선택했다



■ AO Fields를 사용한 렌더링


여기까지를 그대로 구현한 결과. 그림자색(陰色)엉성한 사각형이 되어 나타난다. 이대로는 너무 부자연스럽다
실제로 이 AO Fields를 런타임에서 어떻게 이용할까 인데 모든 오브젝트군의 렌더링이 끝난 후에 각 오브젝트의 AO Fields사이즈의 직육면체를 렌더링 해 나가는 형태이다.

라고는 해도 실제로 직육면체의 실체를 그리는것이 아닌 깊이 테스트를 수행하여 AO Fields 사이즈의 직육면체와 모든 오브젝트의 렌더링이 끝난 씬과의 교차부분 ( AO Fields를 적용할수 있는 부분)을 구하는 것이 주된 목적이다.

이것은 Deferred 렌더링의 라이팅 방식과 꽤 닮은 형태이다. Deferred 렌더링의 라이팅 방식에서는「광원의 적용범위」를 구하지만 AO Fields에서는「차폐의 적용범위」를 구하고있는 것이다

실제로 렌더링을 수행한 픽셀 셰이더에서는 깊이 테스트의 결과로 그 부분이 AO Fields를 적용해야할 장소인가라고 판단가능한 경우는 그 깊이 값을 바탕으로 월드 좌표계의 위치정보를 역산하여 그 값으로부터 AO Fields의 어느 복셀의 영향 하에 있는가 ( 오브젝트의 로컬 좌표 )를 구한다.

AO Fields의 어느 복셀을 읽어내면 좋은가를 구하면, 그 복셀에 대응하는 3D 텍스처로부터 차폐 벡터와 차폐율을 읽어낸다. 다음은 그 장소의 법선벡터를 읽어내어( Defferd 렌더링 엔진이라면 깊이 버퍼와 함께 이전에 G 버퍼로써 렌더링이 끝난 상태) 이 방향벡터와의 사각 관계를 검토하면서 차폐율에 바이어스값을 주고 그림자색(陰色)을 산출하면 된다

여기까지의 내용을 실제로 실행한것이 오른쪽에 있는 스크린샷으로 아니나 다를까 엉성한 복셀단위로 관리되고있는 AO Fields의 내용이 사각형처럼 렌더링 결과에 여실히 나타나 버렸다。

【차폐도의 산출식】
해당 장소의 차폐도 산출식. Strength는 차폐강도를 나타내며 이것은 아티스트가 오브젝트마다 결정할 수 있다. 즉 AO Fields에 따라 그림자색陰色의 농담을 Strength로 컨트롤 할 수 있다(왼쪽)、해당 장소에 법선벡터가 차폐 벡터의 방향에 가까우면 가까울 수록 그림자색(陰色)은 짙어진다 라는 조정 항목이다(오른쪽)



■ AO Fields의 개량


가장 외곽부분 경계에 있는 복셀을향해 차폐율이 0으로 되도록 AO Fields를 조정. 이 슬라이드에서 alpha는 α채널에 저장되어있는 차폐율을 말한다
그래서 각 오브젝트의 AO Fields에 대해, 가장 외곽부분 경계에 있는 복셀의 차폐율을 강제적으로 0으로 하여 AO Fields의 차폐율을 가장 큰 값으로부터 외곽 복셀의 차폐율을 점점 0으로 선형적 감쇠형태의 조정을 한다. 이 조정을 수행하는 것이 이 스크린 샷으로 사각형처럼 보이는 아티펙트는 거의 알수 없게 된다.



【AO Fields 조정】
왼쪽이 조정 전 오른쪽이 조정 후. 사각형같이 보이는 아티펙트는 거의 보이지 않고 있다

하지만 아티펙트는 또하나 있다. 그것은 잘못된 그림자색(陰色)이 붙는다라는 아티펙트이다.
알기 쉬운 예로써 자동차의 본네트나 지붕, 사이드 스텝 부근에 불필요한 그림자색(陰色)이 생긴다.

이것은 AO Fields가 꽤 엉성한 복셀 단위로 계산되고있기 때문이며, 오브젝트의 경계면 부근에서는 차폐율이 급변하기 때문에 이 부근에서의 차폐율의 계산을 진행할때 많은 오차가 생겨 버리는 것이다

각 복셀로 차폐 항목을 계산할때에 이 복셀이 경계면에 있을때는 자동으로 차폐율을 조정하도록 하는AO Fields 계산법을 넣어보는 것도 생각해 봤지만 복잡한 형상이 있는 부분에서는 그것도 판단이 어려웠다.

그래서 어쩔수 없이 그림자색(陰色)을 입히는 픽셀 부분에 법선벡터를 참조하여 그 방향에 따라 AO Fields의 참조를 반 복셀 옆으로 땡겨서 조정(오프셋)을 더하는 방식을 채택했다

약간 강인한 대응책이긴 하지만 결과는 썩 좋았다. 경계면 부근에서의 의미불명한 그림자색(陰色)을 없애는데는 성공. 하지만 그 부작용으로써 차폐율이 높은 부분에서의 그림자색(陰色)이 보다 짙게 나와버리게 되었다. 뭐 그렇게 큰 문제는 아니라고 판단..

최종적으로는 이 방식을 채택하는 것으로 결정했다.. 여기까지 내용을 영상으로 종합한 것이 아래에 있는 영상이다


【AO Fields 조정 -2】
경계면에서의 불필요한 그림자(陰)색。원으로 표시한 부분이 해당 부분이다(왼쪽)。셰이딩 대상 장소의 법선 벡터의 방향에 따라 반 복셀 빗겨서 AO Fields를 샘플링한 방식을 넣는 것으로 아티펙트를 제거(오른쪽)



■ AO Decals 이라는 발상~ 두께가 얇은 오브젝트를 위한 AO Fields


「inFAMOUS 2」에서는 창문이나 문 같은 두께가 엷은 오브젝트에 관해서는 보다 간략화한 모델의 AO Fields를 채택했다고 한다.

기본적인 방식은 AO Fields와 같으며 대상 오브젝트를 복셀 화 하여 구분하고 각 복셀 단위로 차폐 항목을 구해 나간다는 방식이지만 엷은 오브젝트이므로 대담하게 간략화 한것을 도입했다.

우선 3D 텍스처는 사용하지 않으며 AO 정보는 2D 텍스처로 다룬다. 그리고 복셀의 레이어는 4층으로 한정하고 첫번째 계층의 정보를 R 텍셀에 2,3,4계층의 정보를 각각의 G, B, α 에 저장한다.

각 수치가 스칼라로 되버리므로 AO Fields에 저장되어있던 차폐방향벡터는 버리고, 차폐율만을 저장하도록 하는 것이다. 이 오브젝트의 4계층분의 차폐율을 저장한 텍스처를 AO Decals라고 부른다

AO Decals에서는 차폐방향은 각 복셀에서 동일방향을 가리키고있다 라고 가정되버린다.
예를들면 창문이라면 벽에 붙어있으므로 차폐방향은 벽면으로부터봤을때 옥외 방향으로의 반구방향뿐이라고 생각한다 (반대측은 항상 벽으로 차폐되어 있다고 간주한다)

AO Decals에서도 사전계산으로써 차폐율 계산은 필요하지만 AO Fields에서 했던 각 복셀에서의 큐브맵 렌더링은 사용하지 않으며 보다 간략화한 알고리즘을 사용한다

대상 오브젝트를 텍스처에 하이트맵으로써 렌더링하고, 그 결과에 대해서 4계층분의 레이어로 구분짓고 각 계층의 각 텍셀에 대응하는 하이트맵의 높치값과 그 주변의 높이값을 검토하여 차폐율을 계산한다. 하이트맵을 깊이 버퍼로써 보면 이 방식은 SSAO와 거의 동일하다

SSAO 처리와 다른것은 공중에 뜬 지점에 대해서도 차폐율을 계산하는 점이다. 이 것은 창틀을 걸치는것 같은 제3의 오브젝트에 대해서도 그림자색(陰色) 입히도록 하기 위해서 일 것이다

【AO Decals -1】
AO Decals에서는 αRGB의 4텍셀을 4계층의 각 지점의 차폐율로 할당한다. 기본적인 사고는 AO Fields와 같지만 AO Decals에서는 차폐방향은 생략하고 복셀도 4계층으로 한정하고 있다(왼쪽)。오브젝트를 하이트맵으로써 렌더링. 이것에 대해 4계층을 잘라 각 지점에서의 차폐율을 구한다. 차폐방향은 오른쪽 그림에서 말하면 바로위에 반구방향에 대해서 구한다(오른쪽)

【AO Decals -2】
엷은 오브젝트라고는 해도 4계층은 꽤 엉성한 공간분할이 된다(왼쪽)。차폐율은 오브젝트의 표면상 각지점으로부터 수행된다. 결과는 각 지점에 가장 가까운 계층에 저장되어있는(파란점). 다른 물체로의 차폐에도 대응하기 위해 공중점에서도 차폐율을 계산한다(초록색 점)(오른쪽)。

「inFAMOUS 2」에서의 AO Decals 방식。AO Decals에서는ε=0.7로써 AO Fields보다도 작은 경계상자를 할당하고있다
AO Decal 의 크기도 AO Fields 때와 같이 각 오브젝트가 완전히 들어가 있으며 약간 크긴 하지만 AO Fields때보다도 크기는 작은편이다.

이것은 처리부하에 배려와 오브젝트의 두께가 애초에 엷기 때문에
그정도로 크게 하지 않아도 충분하기 때문일 것이다.

데이터 사이즈로써의 AO Decals는 각 변 64~128 텍셀 정도로 하고 저장하는 것이 스칼라 값의 차폐율 이므로 다소의 오차도 허용할수있다는 것으로 DXT5 압축을 적용하고 있다. 데이터 사이즈로써는 4~16KB이고 AO Fields와 동일하다. 그렇게 크지는 않다.

런타임에서의 AO Decals의 적용은 AO Fields와 완전히 똑같다. 각 오브젝트의 렌더링을 끝낸 씬에 대해 AO Decals 사이즈의 직육면체를 렌더링하여 구해진 교차 부분에서 AO Decals에 저장된 차폐율을 바탕으로 그림자색(陰色)을 입혀 나간다. AO Decals에서는 차폐방향이 간략화되어 존재하지 않으므로 AO Fields보다도 단순하다

단 씬과 AO Decals의 교차 부분의 깊이값에 대응하는 차폐율을 4계층의 슬라이스한 AO Decals의 1계층부터 읽어내는 것 뿐으로는 포인트 샘플링이 되어 지저분하게 되버린다. 따라서 몇가지의 텍스처 필터링을 도입할 필요가 있다. 하지만 단일 텍스처내에 있는 αRGB의 각 수치에 대해서의 필터링 처리라는 것은 하드웨어에서는 불가능하므로 각자 픽셀 셰이더프로그램측에서 수행할 필요가 있다

【AO Decals - 3】
차폐율과 차폐강도 값만으로 그림자색陰色을 결정한다(왼쪽)、AO Decals로부터 차에 위치를 가져올 때의 필터링 처리를 하는 셰이더 코드 예(오른쪽))



■ AO Decals의 개량


원으로 그린 부분이 하얀선의 아티펙트가...
라는 것으로 지금까지의 처리를 적용한 것이 오른쪽 스크린 샷이다.
그러나 벽면과 창틀의 교차 부분의 어렴풋이 하얀 선이 보이고 있다.

실은 이부분이 AO Fields에서도 보이지만 엉성한 복셀화의 폐해로
경계면에서의 급격한 차폐율 변화가 생기는 것이 원인이다.

구체적으로는 경계면안쪽이 차폐율 0이고 경계면 바깥쪽이 "높은" 차폐율일 경우에 텍스처 필터링의 효과에 의해 얻어낸 차폐율이 엷어져 버린다는 것으로 인해 생긴다

이것을 막기 위해서는 하이트맵에 숨어 있는 부분에 대한 경계면의 차폐율은 그 주변에 있는 하이트맵에 숨어있지 않은 경계면에 차폐율로 덮어 씌워 버리면 된다. 이것으로 경계면의 차폐율이 급격히 변하게 되지 않게 되어 하얀 선의 아티펙트를 사라지게 할 수 있다

파란색은 통상의 차폐율을 구한 점. 빨간점은 하이트맵에 숨어있는 특수한 점(왼쪽)、경계부근의 차폐율은 균일화해 버리므로 이 부근에서의 텍스처 필터링에 의한 차폐율의 급격한 변화를 회피한다(右)

해당 처리를 적용하기전(왼쪽)、하얀 선의 아티펙트가 사라짐(오른쪽)

위의 스크린샷은 실은 조금 결과를 미리 보여준 샷으로 실은 아직 벽면에 대해서 직교하는 오브젝트의 측면의 그림자색(陰色)이 부드럽게 되버린다는 아티펙트가 남아있다. 창문의 예를 들자면 마치 측면이 둥근형태를 띄고있는 것 같은 그림자색(陰色)이 되어 버린다는 것이다

이것에 관해서는 AO Decals를 더욱 개량하는것이 아닌 각 오브젝트 측에 배후의 무한으로 퍼지는 벽면 평면이 있다고 하고 각 버텍스의 부가정보로써 이 벽면으로부터의 차폐율을 새겨넣어버리는 것으로 대처한다. 즉 벽면에 직교한느 측면에 나오는 짙은 그림자색(陰色)은 AO Decals의 효과가 아닌 버텍스의 새겨넣은 AO의 효과 라는 것이다. 여기까지 내용을 영상으로 종합한 것이 아래의 영상이다



AO Decals 만으로는 이와 같은 측면에 그림자색(陰色)이 바로 위로 갈수록 엷어져 버려, 마치 둥근형태를 띄고있는 것 같은 음영이 보이게 되버린다(왼쪽)。그림 중간에 있는 식으로 구한 벽으로부터의 차폐율을 오브젝트의 각 버텍스에 새겨넣는 것으로 대처한다(오른쪽)



도입하기전(왼쪽)、도입한 결과。측면의 그림자색陰色이 확실히 나오게 되었다(오른쪽)



■ 마치며~ AO Fields의 방식을 몸이 변형되는 인체에 적용하는 방법


이후에는 AO Fields 방식을 몸이 변형하는 인체같은 오브젝트에도 적용해 나가는 것을 과제로 하고 있다. 게다가 그것은 결코 꿈은 아니다
세션에 마지막에는「inFAMOUS 2」에서 AO Fields와 AO Decals을 도입함으로써 증가한 추가 텍스처 용량의 레포트가 공개되었다。

「inFAMOUS 2」에서는 이 AO Fields 나 AO Decals의 개념을 도입한 유니크 오브젝트는 119종이라고 하며 그것들의 총 텍스처 용량( AO Fields의 3D 텍스처와 AO Declas의 2D 텍스처의 합산용량)은 불과 569KB라는 것。

119개의 오브젝트에 대한 추가 텍스처 용량으로써는 그럭저럭의 증가량으로 이걸로 얻을 수있는 효과를 생각해보면 합리적인 대책이었다라고 할 수있겠다

퍼포먼스적으로는 1프레임당 평균적으로 봤을때 20~100개의 AO Fields, AO Decals의 렌더링 부하가 걸리고 있지만 소요시간은 PS3에서 0.3~ 1.0ms Worst Case라도 2.3ms였다라고 분석되고 있다. 기본적으로는 버텍스부하보다는 픽셀 부하가 높은 처리 이므로 표시 프레임에 대해서 이 방식에 의해 그림자색(陰色)이 그려지고 있을때에 크게 부하가 높아지는 것 같다.

AO Fields, AO Decals는 현재 오브젝트 자신이 변형되지 않는 자동차등의 대도구(大道具)오브젝트, 쓰레기통 같은 소도구(小道具)오브젝트에서 밖에 적용되고 있지 않다. 오브젝트가 움직여도 제대로 그 움직인 부분에다른물체의 차폐는 고려되지만, 손발이 움직이는 인체 같은 그 자체가변형되는 오브젝트에 관해서는 이 AO Fields, AO Decals는 대응하고 있지 않다. 이 부분이 이후 연구해 나가야 할 확장 방향성이라고 Reed씨는 말한다.

단,인체 같이 [ 자신이 변형하는 ] 오브젝트에 대해서의 AO Fields의 아이디어는 Reed씨도 기다리고 있으다.인체라면 팔, 다리, 목, 몸통 같은 주요 인체 구성 파츠를 분해하여 각각의 부위에 대해 AO Fields를 적용한다 같은 연구이다.
즉 변형하는 오브젝트를 변형하지않는 오브젝트의 집합체로써 처리해 준다라는 것이다.
이렇게 함으로써 추가 부하도 리니어 스케일로 어림잡는 것이 가능하다

SSAO를 발단으로 한 새로운 셰이딩 방식은 당분간은 앞으로도 진화를 보여줄 것이며 계속 유행해나갈 것이라고 필자는 생각한다.

AO Field와 AO Decals에 의한 추가메모리 사용량(왼쪽)과
추가 GPU 부하(오른쪽)




원문 : http://wiki.gamedev.net/index.php/D3DBook:Screen_Space_Ambient_Occlusion
         
SSAO( Screen Space Ambient Occlusion )

http://bleedmin.blogspot.com/2012/04/ssao.html  

컴퓨터로 생성된 이미지에 사실성을 향상시키기 위한 방법들은 매우 많다. 그런 방법들 중 하나가 물체의 라이팅 방정식을 이용해 값을 구할때 그림자 효과를 계산하는 방법이다. 실시간 컴퓨터 그래픽에서 유용한 그림자 기법들은 매우 많으며, 그러한 각각 기법들은 장단점을 모두 보인다. 일반적으로, 그림자 기법들은 그림자 품질과 실행 효율 사이의 균형을 취하려 노력한다.

그런 기법들중 하나가 바로 SSAOScreen Space Ambient Occlusion라고 불리는 기법이다. 이 기법은 Martin Mittring이 2007 SIGGRAPH 컨퍼런스에서 발표한 논문 "Finding Next Gen"에서 처음 논의되었다. 이 알고리즘의 기본 컨셉은
전역 조명 과정Ambient lighting term을 장면의 한 점이 얼마나 차폐되는지를 구하는 기반으로 바꾸는 것이다. 이 방법이 AO의 컨셉을 처음으로 이용하는 기법은 아니지만, 이후의 장chapter에서 보게 되겠찌만 SSAO는 꽤나 현명한 추정과 단순함으로 매우 높은 성능을 유지하면서도 만족할만한 결과를 보여준다.

이 번 장에서는, SSAO의 배경이 되는 이론을 살펴보고, Direct3d 10 파이프라인을 이용하여 이 기법을 구현하는 방법을 제공하며, 성능과 품질의 향상을 위해 구현 단계에서 사용할 수 있는 파라미터들에 대하여 논의할 것이다. 마지막으로, 주어진 구현을 이용한 데모에 대하여 논의하여 이를 통해 알고리즘의 성능에 대해서 몇 가지 사항을 알리고자 한다.

알고리즘 이론

이제 우리는 SSAO의 유래에 관한 배경지식에 대해서 살펴보았으므로, 기법에 대하여 더욱 자세하게 살펴볼 준비가 되었다. SSAO는 단 하나의 단순한 명제를 사용함으로써 이전의 기법들과는 다른점을 보인다: SSAO는 이전에 레스터화되기 이전의 장면 정보를 사용하는 대신 현재 장면에서 주어진 점의 차폐의 양을 구하는데 오직 깊이 버퍼만을 이용한다. 이를 위해 깊이 버퍼를 직접적으로 사용하는 방법이나 깊이 정보를 보관하기 위해 특별한 렌더타겟을 생성하는 방법 모두 가능하다. 깊이 정보는 주어진 점의 주변점을 구하는 즉시 세밀하게 조사하여 그 점이 얼마나 차폐되었는지 계산하는데 사용한다. 차폐 인수occlution factor은 한지점에 얼마만큼의 전역광을 적용할것인지, 그 양을 조절하는데 사용한다.

SSAO 알고리즘에서 사용하는 차폐 인수을 생성하는 방법은 논리적으로 두 단계 과정으로 이루어져 있다. 먼저 우리는 장면의 현재 시점을 기준으로 깊이 텍스처를 생성해야하고, 이 깊이 텍스처를 이용하여 현 장면의 최종 화면에서 각 픽셀의 차폐 정도를 결정한다. 이 과정은 아래의 그림 2에서 볼 수 있다.



그림2. SSAO 알고리즘의 개요
깊이 버퍼만을 사용하는 이 단순함은 성능과 품질면에서 몇가지 효과가 있다. 장면 정보가 렌더타겟으로 레스터화 하는 경우, 장면을 나타내는데 사용하는 정보가 3차원에서 2차원으로 줄어들게 된다. 데이터 축소는 매우 효율적인 명령을 제공하는 표준 GPU 파이프라인 하드웨어를 통해 수행된다. 또한 장면의 전체 차원을 줄임으로, 2D 장면 표현을 장면의 가시 영역으로 한정하여 이 프레임에 보이지 않는 물체에 대한 불필요한 연산을 제거할 수 있게 된다.

데이터 축소는 차폐 인수을 계산하는데 있어서 확실한 성능상의 이득을 제공하지만, 동시에 연산시에 필요한 정보 또한 제거한다. 이것은 우리가 계산으로 얻은 차폐 인수 값이 꼭 정확한 값이 아닐 수 있음을 의미하지만, 이후의 장에서 보게 되겠지만 전역 조명 과정은 근사치 일지라도 훌륭하게 장면의 현실감을 부여해 준다.

텍스처에 장면의 깊이 정보를 저장한 뒤, 깊이 값의 직접 인접에 기반하여 각 점이 얼마나 차폐되었는지 결정해야 한다. 이 연산은 다른 AO 기법들과 꽤나 비슷하다. 그림 3에 보이는 바와같이 깊이 버퍼를 고려해야한다 :


그림 3. 뷰view 방향이 아래쪽으로 향한 샘플 깊이 버퍼

이상적으로 주어진 점을 중심으로 둘러싼 반지름 r인 구를 생성하고, 그 구와 깊이 버퍼에 있는 물체와 교차하는 부피를 구하기 구를 따라 통합해 나간다. 이 컨셉은 여기 그림 4에서 볼 수 있다.


그림 4. 차폐를 계산하기 위한 이상적인 샘플링 구

물론, 렌더된 이미지의 모든 픽셀에서 복잡한 3D 통합을 수행하는 것은 전적으로 비효율적이다. 대신, 이 구의 부피 내에 존재하는 3D 샘플링 커널을 이용하여 이 구를 추정할 것이다. 샘플링 커널은 명시된 지점의 깊이 버퍼를 살펴, 그 각 가상의 점이 깊이 표면에 의해 가려지는지 결정하는데 이용된다. 샘플링 커널의 예시는 그림 5에 나타나 있으며, 우리의 표면점에 어떻게 적용해야 하는지도 보여준다.


그림 5. 단순화한 샘플링 커널과 깊이 버퍼 내에의 위치

차폐 인수는 차폐가 발생한 샘플링 커널의 합으로 구할 수 있다. 구현 부분에서 살펴보겠지만, 이 개별적인 계산은 카메라부터의 거리, 차폐 지점과 기준점 간의 거리, 아티스가 지정한 비례축소 인수 등의 몇가지 다른 인수들에 따라 더 민감하게, 혹은 덜 민감하게 할 수 있다.
구현

알고리즘이 어떻게 기능하는지 이해했따면, 이제 구현하는 방법에 대해 논의해 보자. 알고리즘 이론 파트에서 논의했듯이, 장면의 깊이 정보가 담긴 소스가 필요하다. 간단하게 하기 위해, 우리는 깊이 정보를 저장하기 위해 서로다른 부동소수 버퍼를 사용할 것이다. 비록 조금 더 복잡할지는 모르겠지만, 더 효율적인 기법은 장면의 깊이 값을 얻기 위해 z-버퍼를 이용할 것이다. The following code snippet shows how to create the floating point buffer, as well as the render target and shader resource views that we will be binding it to the pipeline with:

// 깊이 버퍼 생성
D3D10_TEXTURE2D_DESC desc;
ZeroMemory( &desc, sizeof( desc ) );
desc.Width = pBufferSurfaceDesc->Width;
desc.Height = pBufferSurfaceDesc->Height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R32_FLOAT;
desc.SampleDesc.Count = 1;
desc.Usage = D3D10_USAGE_DEFAULT;
desc.BindFlags = D3D10_BIND_RENDER_TARGET | D3D10_BIND_SHADER_RESOURCE;

pd3dDevice->CreateTexture2D( &desc, NULL, &g_pDepthTex2D ); 

// 렌더 타겟 리소스 뷰 생성
D3D10_RENDER_TARGET_VIEW_DESC rtDesc;
rtDesc.Format = desc.Format;
rtDesc.ViewDimension = D3D10_RTV_DIMENSION_TEXTURE2D;
rtDesc.Texture2D.MipSlice = 0; 

pd3dDevice->CreateRenderTargetView( g_pDepthTex2D, &rtDesc, &g_pDepthRTV ); 

// 셰이더-리소스 뷰 생성
D3D10_SHADER_RESOURCE_VIEW_DESC srDesc;
srDesc.Format = desc.Format;
srDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE2D;
srDesc.Texture2D.MostDetailedMip = 0;
srDesc.Texture2D.MipLevels = 1; 

pd3dDevice->CreateShaderResourceView( g_pDepthTex2D, &srDesc, &g_pDepthSRV );


깊이 정보를 생성한 후, 다음 단계는 최종 장면 내 각 픽셀의 차폐 인수를 계산하는 것이다. 이 정보는 두번째 단일 성분 부동소수 버퍼에 저장할 것이다. 이 버퍼를 생성하기 위한 코드는 부동소수 깊이 버퍼를 생성하기 위해 사용한 코드와 매우 유사하므로, 페이지 길이를 줄이기 위해 생략한다.

이제 두 버퍼가 모두 생성되었으므로, 각 버퍼에 저장할 정보를 생성하는 부분을 상세히 살펴보자. 깊이 버퍼는 뷰 공간의 깊이값을 기본적으로 저장한다. 절단 공간 깊이clip space depth 대신 선형 뷰 공간 깊이linear view space depth를 사용하여 투시 투영perspective projection으로 인한 깊이 범위 왜곡을 방지한다. 뷰 공간 깊이는 입력 버텍스와 월드뷰world-view 행렬을 곱해서 구할 수 있다. 그런뒤 뷰 공간 깊이는 단일 성분 버텍스 속성으로 픽셀 셰이더로 전달된다.

fragment VS( vertex IN )
{
    fragment OUT;
    // 절단 공간 좌표를 출력
    OUT.position = mul( float4(IN.position, 1), WVP );
    // 뷰 공간 좌표를 계산
    float3 viewpos = mul( float4( IN.position, 1 ), WV ).xyz;
    // 뷰 공간 깊이값을 저장
    OUT.viewdepth = viewpos.z;
    return OUT;
}


깊이값을 부동소수 버퍼에 저장하기 위해, 픽셀 셰이더에서는 근거리 클리핑 평면과 원거리 클리핑 평면간의 거리에 따라 깊이값을 확대/축소한다. 이것은 범위 [ 0, 1 ]을 가진 선형, 정규화된 깊이 값을 생성한다.

pixel PS( fragment IN )
{
    pixel OUT;

    // 뷰 공간 범위에 따라 깊이값을 확대/축소
    float normDepth = IN.viewdepth / 100.0f;

    // 확대/축소된 값을 출력
    OUT.color = float4( normDepth, normDepth, normDepth, normDepth );

    return OUT;
}


저장된 정규화된 깊이 정보를 가지고, 각 픽셀의 차폐 인수를 생성하기 위해 깊이 버퍼를 셰이더 리소스로 바인딩하고 각 차폐 버퍼에 저장한다. 차폐 버퍼 생성은 단일 전체 스크린 화면을 렌더링함으로써 초기화한다. 전체 스크린 화면의 버텍스들은 단지 단일 2-성분 속성을 가지고 있으며, 이는 깊이 버퍼 내의 자신의 위치에 대응하는 텍스처 좌표를 나타내고 있다. 버텍스 셰이더는 단순히 이 파라미터들을 픽셀 셰이더에 전달만 한다.

fragment VS( vertex IN )
{
    fragment OUT;
    OUT.position = float4( IN.position.x, IN.position.y, 0.0f, 1.0f );
    OUT.tex = IN.tex;

    return OUT;
}


필셀 셰이더는 3D 샘플링 커널의 형태를 3-성분 벡터의 배열로 정의하는 것으로 시작한다. 벡터의 길이는 [ 0, 1 ] 범위 사이에서 다양히 변하며 각 차폐 테스트에서 약간의 변화를 주게 된다.

const float3 avKernel[8] ={
    normalize( float3( 1, 1, 1 ) ) * 0.125f,
    normalize( float3( -1,-1,-1 ) ) * 0.250f,
    normalize( float3( -1,-1, 1 ) ) * 0.375f,
    normalize( float3( -1, 1,-1 ) ) * 0.500f,
    normalize( float3( -1, 1 ,1 ) ) * 0.625f,
    normalize( float3( 1,-1,-1 ) ) * 0.750f,
    normalize( float3( 1,-1, 1 ) ) * 0.875f,
    normalize( float3( 1, 1,-1 ) ) * 1.000f
}


다음으로, 픽셀 셰이더는 텍스처 룩업 내에서 샘플링 커널을 반사시키기 위한 렌덤 벡터를 찾는다. 이것은 사용한 샘플링 커널을 이용해 매우 다양한 변화를 줄 수 있으며, 이는 적은 수의 차폐 테스트를 하더라도 높은 수준의 결과를 제공할 수 있게 해준다. 이는 차폐를 계산하기 위한 깊이를 효과적으로 "흐트려jitter" 주어, 우리가 현재 픽셀의 주변 공간을 언더샘플링 한다는 사실을 숨겨준다.

float3 random = VectorTexture.Sample( VectorSampler, IN.tex.xy * 10.0f ).xyz;random = random * 2.0f - 1.0f;


이제 픽셀 셰이더가 현제 픽셀의 깊이값을 바탕으로 샘플링 커널에 적용하기 위한 확대/축소값을 계산할 것이다. 이 픽셀의 깊이값은 깊이 버퍼로부터 읽어들여 근거리 평면과 원거리 평면간의 거리를 곱함으로 뷰 공간으로 도로 확장된다. 그런 뒤 샘플링 커널의 x 와 y 성분을 위한 확대/축소값을 계산하는데, 이는 샘플링 커널의 희망 반지름( 미터 단위 )을 픽셀의 깊이값( 미터 단위 )로 나누면 된다. 이는 개별 샘플을 찾기위해 사용되는 텍스처 좌표를 확대/축소한다. z 성분의 확대/축소값은 희망 커널 반지름을 근거리 평면과 원거리 평면간의 거리로 나누어 구할 수 있다. 이는 모든 깊이값의 비교를 우리가 깊이 버퍼에 저장한것과 같은 정규화된 깊이 공간에서 이루어지도록 하게 해준다.

float fRadius = vSSAOParams.y; 
float fPixelDepth = DepthTexture.Sample( DepthSampler, IN.tex.xy ).r;
float fDepth = fPixelDepth * vViewDimensions.z;
float3 vKernelScale = float3( fRadius / fDepth, fRadius / fDepth, fRadius / vViewDimensions.z );


커널 확대/축소값이 구해지면, 개별 차폐 테스트를 수행할 수 있게 된다. 이것은 for 루프 내에서 샘플링 커널이 가르키는 각 점들을 계속해서 반복하는 것으로 이루어진다. 현재 픽셀의 텍스처 좌표는 렌덤하게 반사된 커널 벡터의 x와 y 성분을 통해 차감 계산되고 새 좌표에서 깊이값을 살펴보기 위해 사용된다. 이 깊이 값은 커널 벡터의 z 성분을 통해 차감 계산되고 현재 픽셀의 깊이와 비교된다.

float fOcclusion = 0.0f; 
for ( int j = 1; j < 3; j++ )
{
    float3 random = VectorTexture.Sample( VectorSampler, IN.tex.xy * ( 7.0f + (float)j ) ).xyz;
    random = random * 2.0f - 1.0f;

    for ( int i = 0; i < 8; i++ )
    {
        float3 vRotatedKernel = reflect( avKernel[i], random ) * vKernelScale;
        float fSampleDepth = DepthTexture.Sample( DepthSampler, vRotatedKernel.xy + IN.tex.xy ).r;
        float fDelta = max( fSampleDepth - fPixelDepth + vRotatedKernel.z, 0 );
        float fRange = abs( fDelta ) / ( vKernelScale.z * vSSAOParams.z );
        fOcclusion += lerp( fDelta * vSSAOParams.w, vSSAOParams.x, saturate( fRange ) );
    }
}


깊이값의 델타값( = 차이값 )은 어플리케이션의 선택가능한 범위값으로 정규화된다. 이것은 깊이값 차이의 관계 등급을 결정하기 위한 인수를 생성하기 위함이다. 차례로, 이 정보는 이 특정의 차폐 테스트의 범위를 수정할 수 있다. 예를 들어, 한 물체가 장면의 전경에 위치하고 다른 하나의 물체가 그 뒤에 부분적으로 가려져 있다면, 전경의 물체가 유일한 차폐물인 경우 뒷쪽 물체를 위해 불필요한 차폐 테스트를 계산할 필요가 없다. 반대로 배경의 물체에는 앞 물체의 가장자리를 따라 그림자 같은 후광이 나타날 것이다. The implementation of this scaling is to linearly interpolate between a scaled version of the delta value and a default value, with the interpolation amount based on the range value.

fOcclusion += lerp( ( fDelta * vSSAOParams.w ), vSSAOParams.x, saturate( fRange ) );


최종 픽셀 색상을 내놓기 전의 마지막 단계는 모든 커널 샘플로부터 평균 차폐값을 계산하고, 그 값을 최대/최소 차폐 값 사이를 보간하는데 사용하는 것이다. 이 마지막 보간은 광범위한 차폐값을 압축해주고 보다 부드러운 출력을 제공해준다. 이 장의 데모 프로그램에서 주의할 것은, 사진의 차폐를 계산하는데 단지 16샘플만을 사용했다는 것이다. 만일 더 많은 샘플을 사용했다면, 출력 차폐 버퍼는 더 부드러운 결과를 보여줄 것이다. 이것은 다른 등급의 하드웨어들에서 성능과 품질을 조정하는 좋은 방법이 된다.

OUT.color = fOcclusion / ( 2.0f * 8.0f ); 

// 범위 재매핑
OUT.color = lerp( 0.1f, 0.6, saturate( OUT.color.x ) );


생성한 차폐 버퍼를 가지고, 최종 렌더링 패스에서 셰이더 자원에 포함해 이를 이용할 수 있다. 렌더된 기하는 단수히 스크린 공간 텍스쳐 좌표를 계산하고, 차폐 버퍼를 샘플링한 뒤, 그 값을 이용하여 ambient 조건을 조정하면 된다. 예시로 제공되는 파일은 단순히 다섯 샘플의 평균을 이용하는 방법을 사용하였지만, 가우시안 브럴와 같은 좀더 세련된 방법을 대신 사용하는 것도 좋다.


SSAO 데모
데모 다운로드 : SSAO_Demo

이번 장을 위해 만든 데모 프로그램은 단순히 다수의 정방면체를 우리의 SSAO 셰이더만을 이용하여 렌더링한 것이다. 이번 기법에서 논의된 조절가능한 파라미터들에 대해서는 화면상의 슬라이더 콘트롤러를 이용하여 실시간으로 바꿀 수 있다. 그림 6 이하는 차폐 버퍼의 모습과 최종 출력 렌더링의 결과를 볼 수 있다. 한가지 알릴 사실은 차폐 효과를 보다 과장하기 위하여 차폐 파라미터들을 조절했다.


그림 6. 데모 프로그램의 차폐 버퍼의 모습

그림 7. 데모 프로그램의 최종 출력 렌더링의 모습

결론

In this chapter we developed an efficient, screen space technique for adding realism to the ambient lighting term of the standard phong lighting model. This technique provides one implementation of the SSAO algorithm, but it is certainly not the only one. The current method can be modified for a given type of scene, with more or less occlusion for distant geometry. In addition, the minimum and maximum amounts of occlusion, interpolation techniques, and sampling kernels are all potential areas for improvement or simplification. This chapter has attempted to provide you with an insight into the inner workings of the SSAO technique as well as a sample implementation to get you started.







일반적인 game scene 에서의 SSAO 요소.

SSAO 와 diffuse lighting 을 비교. 

Screen Space Ambient Occlusion( SSAO ) 는 이미 잘 알려진 ambient occlusion 이라는 효과를 실시간에 효율적으로 근사계산하기 위한 rendering 기법입니다. 이것은 Crysis 라는 PC game 에서 2007 년에 처음 사용되었습니다.

이 algorithm 은 순수하게 computer 의 GPU 상에서 실행되며, texture 에 저장된 scene depth buffer 를 분석함으로써 pixel shader 로서 구현됩니다. 화면상의 모든 pixel 에 대해서 pixel shader 는 현재 pixel 주변의 깊이값들을 sampling 하고 sampling 된 각 point 로부터 occlusion 의 총합을 게산하려고 시도합니다. 가장 단순한 구현에서는 occlusion factor 는 현재 point 와 sampling 된 point 사이의 깊이 차에만 의존합니다.

부가적인 세련된 방식을 사용하지 않는다면 그러한 막무가내식( brute force ) 기법은 좋은 가시 품질을 위해 pixel 당 약 200 번의 texture read 를 요구할 것입니다. 이것은 현대의 graphic hardware 상에서 실시간 rendering 에 적합하지 않습니다. 더 적게 read 하면서 더 좋은 결과를 얻기 위해서 sampling 은 무작위적으로 회전되는 kernel 을 사용해 수행됩니다. 최종 이미지에서 high-frequency noise 만을 획득하기 위해서 이 kernel orientation 은 모든 N screen pixel 들에 대해 반복됩니다. 마지막으로 이러한 high freqency noise 들은 NxN 크기의 후처리 blurring 단계에서 거의 대부분 제거되는데 깊이 불연속성을 고려하게 됩니다( 인접한 normal 과 dpeth 를 비교하는 것과 같은 기법을 사용하게 됩니다 ). 그러한 방식은 pixel 당 16 개 혹은 그 이하의 깊이 sample 을 사용해 고품질의 결과를 얻을 수 있게 해 주며, 기본적으로 computer game 과 같은 실시간 응용프로그램에서 SSAO 를 사용할 수 있게 해 줍니다.

다른 ambient occlusion 방식과 비교해 SSAO 는 다음과 같은 이점을 가집니다 :
  • Scene 복잡도에 독립적.
  • Data 에 대한 전처리, loading time, system memory 할당을 필요로 하지 않음.
  • 동적 scene 에서 잘 동작함.
  • 화면상의 모든 pixel 에 대해 동일한 방식이 적용됨.
  • CPU 를 사용하지 않음 - 온전히 GPU 상에서 실행됨.
  • 현대 graphics pipeline 과 쉽게 통합됨.
물론 단점도 가지고 있습니다 :
  • Rather local and in many cases view-dependent, as it is dependent on adjacent texel depth which may be generated by any geometry whatsover.
  • 개체의 모서리 같은 곳에서 깊이 불연속성을 저해하지 않고 올바른 smooth/blur 를 하기 어렵다( the occlusion should not "bleed" onto objects ).
See also
Externel links



오늘은 아주 아주 짧고, 간단한 내용 하나 적어볼까 합니다.

스마트폰에 대해서, 열풍이 불길래, 아주 잠깐, 옛날 기억을 살려서, OpenGLES용 렌더러를 살짝 만들어본적이 있는데요. 그 때, 렌더링 파워가 어느 정도까지 될까? 라는 궁금증이 있었는데, 마침 "인피니트 블레이드"라는 게임이 출시되어서, "아~ 이 정도는 되는구나!"라고 생각했었죠.

특히, 스펙큘러가 반짝 반짝 하는 것을 보고, PC에서 하는 것 처럼 했을까? 라는 생각이 들었었는데, 좀 찾아보니까, 고정 테이블을 이용한 방법이 있더군요. 살짜쿵 소개해볼까 합니다. (실제로 UDK의 모바일 셰이더를 열어보니, 요게 있더군요.)

일반적인 Specular 계산



  1. float BaseSpec = max(0, dot(normal, halfvector));  
  2. // SpecPower = 8, 16, 32, ...  
  3. float3 Specular = pow(BaseSpec, SpecPower);   
PC에서도 아깝지만, 스마트폰에서 pow(x, 32)를 하면 완전 아깝죠... (실제 테스트는 안해봤어요!)pow(x, n)을 이미 테이블로 만들어 놓은 값을 참고해서, max(A * x + B)로 만들 수가 있습니다.
  1. float BaseSpec = max(0, dot(normal, halfvector));  
  2. // N = 18, M = 2  
  3. #define SpecA 6.645  
  4. #define SpecB -5.645  
  5. float SpecularAmount = clamp( SpecA * BaseSpec + SpecB, 0, 1 );  
  6. float3 Specular = BaseSpec * SpecularAmount;  
A와 B는 이미 만들어진, 아래 테이블을 참조하여, 결정하면 됩니다.
(UDK를 보면, M=2일 때 보기가 좋다고 하네요...) 





이게 끝입니다. ㅎㅎ 간단하죠!

이 처럼 지수 계산을 간단한 곱셈으로 줄일 수 있어서, 스펙큘러를 사용하면서도 최적화를 할 수 있습니다. (결국, mad_sat instruction 하나로 처리가 가능!)
(물론, 오차 범위나 결과의 차이는 고려해야 합니다.)

그럼 어떤 차이가 있는지 한번 볼까요? 
(개인 학습용으로 개발된 렌더러에서 붙여보았습니다.)

[pow(n, 32)의 경우]
[N=18, M=2 테이블을 이용함]
직접적인 비교는 어렵긴한데, 대충 어느 정도 차이가 나는지를 보실 수는 있을 것입니다. 너무 하이라이트가 강하게 나오는 듯 하니, M, N을 테이블에서 바꾸보면서 테스트 해보면 될 듯 합니다.

간략하게 글을 썼습니다. 원본글을 읽어보시면, 더 도움이 되실거에요. 
(http://www.gamasutra.com/view/feature/2972/a_noninteger_power_function_on_.php)
사실, PC로 개발할 때에는 이 정도 비용은 크게 신경을 쓰지 않습니다. 하지만, 최근 게임들이 다양한 PC 사양에서 돌아가게 만들도록 옵션들을 다양하게 제공합니다. 심지어는 라이팅을 완전히 하지 않는 경우도 있지요.
생각해보면, 지수 연산을 마구 쓰기시작한지는 얼마 되지 않았습니다. 리니지2 정도의 시절?만 하더라도, Pixel 라이팅 자체를 거의 사용하지 않았으니까요. (이 때는 GlossMap으로 후려쳤지요!) 그러니, 당연하다고 하기에
는 생각보다 꽤 비싼 연산입니다.


/*************************************************************************/

/* Flip

/*************************************************************************/

LPDIRECTDRAWSURFACE7 m_pDDSPrimary;

LPDIRECTDRAWSURFACE7 m_pDDSBack;

LPDIRECTDRAWSURFACE7 m_pDDSOffScreen;

DDSURFACEDESC2 m_ddsd;

.....

 

/////////////////////////////////////////////////////////////////////////////////////////////////

//프리아머리 서페이스와, 백 서페이스, 오프 스크린 생성

ZeroMemory( &m_ddsd, sizeof(m_ddsd) );
m_ddsd.dwSize = sizeof(m_ddsd);
m_ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
m_ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
m_ddsd.dwBackBufferCount = 1;

 

if (FAILED(m_lpdd->CreateSurface(&m_ddsd, &m_pDDSPrimary, NULL)))
{
  MessageBox( NULL, "CreateSurface 실패했습니다.", "에러", MB_OK );

  return FALSE;
}

 

DDSCAPS2 ddscaps;

ZeroMemory( &ddscaps, sizeof(ddscaps) );

ddscaps.dwCaps = DDSCAPS_BACKBUFFER;

 

if( FAILED(m_pDDSPrimary->GetAttachedSurface(&ddscaps, &m_pDDSBack)) )
{
    MessageBox( NULL, "GetAttachedSurface 실패했습니다.", "에러", MB_OK );
  
    return FALSE;
}

 

ZeroMemory( &m_ddsd, sizeof(m_ddsd) );
m_ddsd.dwSize = sizeof(m_ddsd);
m_ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
m_ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
m_ddsd.dwWidth = 640;
m_ddsd.dwHeight = 480;

 

if (FAILED(m_lpdd->CreateSurface(&m_ddsd, &m_pDDSOffScreen, NULL)))
{
    MessageBox( NULL, "CreateSurface 실패했습니다.", "에러", MB_OK );

    return FALSE;

/////////////////////////////////////////////////////////////////////////////////////////////////

.....

 

m_pDDSBack->Blt( NULL, g_pGame->m_pDDSOffScreen, NULL, DDBLTFAST_WAIT, NULL );

m_pDDSPrimary->Flip( NULL, DDFLIP_WAIT );

LPDIRECTDRAWSURFACE7 m_pDDSOffScreen;

DDSURFACEDESC2 ddsd;

 

///////////////////////////////////////////////////////////////////////////

ZeroMemory( &ddsd, sizeof(m_ddsd) );
ddsd.dwSize = sizeof(m_ddsd);
ddsd.dwFlags = DDSD_CAPS DDSD_WIDTH | DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = 640;
ddsd.dwHeight = 480;

 

if (FAILED(m_lpdd->CreateSurface(&m_ddsd, &m_pDDSOffScreen, NULL)))
{
    MessageBox( NULL, "CreateSurface 실패했습니다.", "에러", MB_OK );

    return;

//////////////////////////////////////////////////////////////////////////

밉맵은 하나의 텍스쳐를 일정한 크기로 축소시켜서 여러장을 가지고 그려질때 현재 사이즈에

가장 적합한 이미지를 찾아서 쓰는 것을 말한다.

이것의 이점을 본다면, 일단 기본적으로 폴리곤에 텍스쳐를 입힐때 폴리곤에 맞는 텍셀을 찾아야 

한다.  따라서 폴리곤이 화면에 보일때 텍스쳐 실제 사이즈와 동일하다면, 내부적으로 매우 쉽게

찾을 것이다. 하지만 그렇지 못할때, 적당한 값을 찾아서 그리는데 시간을 소모할 것이다. 
( 보통 필터링을 사용해서 색보간을 해서 그린다. 작게 할때는 색을 합치고 키울때는 보간하고...)
문제는 실제 화면에 한픽셀을 차지하는 폴리곤이 있다고 했을때 그 한픽셀을 그리기 위해서

512 X 512 크기의 텍스쳐를 모두 필터링해서 하나의 픽셀을 가져온다고 생각해보자.
생각만으로도 느릴것같다는 생각이 들지 않는가? ( 아니라고 말하는 당신. 그냥 그렇다고 생각해주시구려... ) 그래서 나온것이 바로 밉맵이다.


실제로 밉맵은 구형 그래픽카드일수록 그 효과가 잘 나타나지만(속도라던지..), 최신카드일 수

록 잘 나타나지 않는다.
일단 자동으로 밉맵을 만들기위해서는 2의 지수형 정방형 텍스쳐이어야만한다.
밉맵을 쓰지않고 하나만 만들면 사이즈를 자유롭게 할수있지만, 그게 아니면 2의 지수형태를

띠어야만한다.

( OGL에서는 무조건적으로 2의지수형이어야만 만들어졌었다. 최신OGL은 바뀌었다는 이야기가

들리지만.. )

그렇다. 다 아는 내용을 이야기 했다. 사실 내가 하고싶은 말은 여기서 부터다. (라고해봐야...ㅡ.ㅡ)

기본적으로 다이렉트X에서 텍스쳐를 생성할때 밉맵체인 (그러니까 하나의 이미지를 축소시키는

단계를 말한다.)의 수를 정할 수 있다. 기본적으로 옵션에 그부분에 0을 넣는다.
0은 디폴트값을 의미하며, 자동으로 알아서 끝까지 만들어 준다는 의미다. 그래서 언제나 텍스쳐를

 로드할때 그냥 밉맵레벨에 0을 넣어서 자동으로 밉맵을 만들어서 사용하는 것이다.

문제는 생성한 텍스쳐를 가지고 장난을 칠때 종종 실수를 한다는 것.
일단 텍스쳐에 렌더링을 한다고 하자. 그때는 렌더텍스쳐를 생성하는데 이때도 그냥 아무 생각없이

 밉맵을 자동으로 생성해버리기 일수이다. 
뭐 그렇게 해서 생성이 안된다면 다행이지만(다행일까?), 분명한 것은 아무 문제없이 생성이 된다.
그다음에 우리는 0번표면에 렌더링을 걸고, 이후 그것을 폴리곤 어딘가에 발라서 사용할때, 문제가 발생한다. 아무것도 안그려졌거나, 괴상하게 깨져나가는 텍스쳐를 발견하게 될것이다.
물론 운이 좋아서, 아무 문제없이 잘 그려지기도 한다. 
 처음에 잘그려짐을 경험하고, 나중에 안됨을 경험하면 왜 안되는건지 파악하는데 제법시간이

걸린다. ( 그게바로 나다. ㅡ.ㅡ )

 

 밉맵체인은 텍스쳐를 로딩할때 자동으로 한번 만들어 주는것 뿐이지, 이후 표면을 수정했다고

자동으로 나머지들도 만들어주는 것은 아니라는 사실을 몰랐던 시절이다.

 이것은 오직 렌더텍스쳐만 말하는것이 아니다. 텍스쳐에 lock을 걸고 편집작업을 하는것도 역시

마찬가지다. 보통 0번만 편집하고 사용하는데, 역시 위와 같은 문제가 발생해서 왜 안되는건지

대책없이 멍하니 있거나 엄한 D3D와 Ms를 욕하는 일을 벌일 수 있다는 것이다.

 

그럼 이글을 읽은 초심자라던가, 아직 D3D라는 것을 해봤지만, 텍스쳐가지고 장난 안 친 사람들

이라면, 밉맵체인을 자동으로 만들고, 0번만 편집하고 똑바로 안된다고 시간허비하지 말기를...

" 알파합성 "

 

1. 알파합성이란

  - 폴리곤을 렌더링할 때, 폴리곤 색을 그냥 출력하는 것이 아니라 이미 렌더링된 화면에 여러 가지 합성을 해서 출력하는 방법

 

2. 주의 사항

  - 알파 합성은 현재 그려져 있는 색과 렌더링하는 폴리곤의 색을 합성한다. 따라서 반투명 폴리곤을 렌더링하기 저에, 불투명 오브젝트를 렌더링해야 한다.

 

3. 알파합성 사용법

  // 폴리곤을 반투명으로 렌더링할지 여부 결정

  m_pd3dDeviece->SetRenderState(D3DRS_ALPHABLENDANABLE, TRUE);

  m_pd3dDeviece->SetRenderState(D3DRS_ALPHABLENDANABLE, FALSE);

  // 어떻게 색을 조합할지

  m_pd3dDeviece->SetRenderState(D3dRS_BLENDOP, 값);

  // 폴리곤의 합성 강도

  m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, 값A);

  m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, 값B);

 

4. 다양한 합성 방법

  - 선형 합성

     : 최종색 = ( 1 - a ) * 바닥색 + a * 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

  - 덧셈 합성

     : 최종색 = 바닥색 + a * 덮일색

     : 빛의 표현에 자주 사용

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

  - 뺄셈 합성

     : 최종색 = 바닥색 - a * 덮일색

     : 간단한 그림자 표현에 자주 사용

     m_pd3dDeviece->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_REVSUBTRACT);

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

  - 곱셈 합성

     : 한쪽이 검으면 최종색이 검게 된다.

     : 최종색 = 바닥색 * 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);

  - 제곱 합성

     : 어두운 곳은 더 어둡게 하고, 밝은 곳은 그대로 표시

     : 최종색 = 바닥색 * 바닥색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_DESTCOLOP);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);

  - 네거티프 포지티프 반전

     : 흰 폴리곤을 렌더링하면, 검은 부분은 희게, 흰 부분은 검게 출력 되도록 색을 역전하는 변환

     : 최종색 = ( 1 - 바닥색) * 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_INVDESTCOLOR);

  - 불투명

     : 최종색 = 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);

stencil buffer 특수한 효과를 위한 off-screen buffer, back buffer depth buffer 동일한 해상도를 가진다. 따라서, stencil buffer 내의 (i, j)번째 픽셀은 back/depth buffer (i, j)번째 픽셀과 대응된다.

이름이 의미하는 처럼 stencil buffer back buffer 일정 부분이 렌더링되는 것을 막는 효과를 위해 사용된다. 예를 들어, 거울에 특정 물체를 반사하도록 할려고 한다면, 거울에 반사되는 부분에 대해서만 드로잉을 수행하면 된다. 거울에 비치지 않는 부분은 렌더링되는 것을 막을 있는 있도록 하는 것이 바로 stencil buffer.

stencil buffer 이용한 것과 그렇지 않은 것에 대한 그림 보기(m)

Figure. stencil buffer 이용하지 않고 그림

Figure. stencil buffer 이용하여 그림

stencil buffer 공부하기 위한 가장 좋은 방법은 이전의 활용 예를 확인하는 것이며, 이를 통해 필요에 따른 다양한 응용 능력을 기를 있다.

Table of Contents

· 8.1 스텐실 버퍼 이용하기

o 8.1.1 스텐실 버퍼 요청하기

o 8.1.2 스텐실 테스트

o 8.1.3 스텐실 테스트 제어하기

§ 8.1.3.1 스텐실 참조

§ 8.1.3.2 스텐실 마스크

§ 8.1.3.3 스텐실

§ 8.1.3.4 비교 연산자

o 8.1.4 스텐실 버퍼 갱신하기

o 8.1.5 스텐실 쓰기 마스크

· 8.2 예제 애플리케이션: 거울

o 8.2.1 반사를 위한 수학

o 8.2.2 거울 구현의 개관

o 8.2.3 코드와 설명

§ 8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 지정

§ 8.2.3.2 스텐실 버퍼에 거울 렌더링

§ 8.2.3.3 거울로 렌더링 부분 표시

§ 8.2.3.4 장면 내에서 반사될 위치 지정

§ 8.2.3.5 반사된 주전자 그리기

· 8.3 예제 애플리케이션: 평면 그림자

o 8.3.1 평행 그림자

o 8.3.2 조명 그림자

o 8.3.3 그림자

o 8.3.4 더블 블렌딩을 막기 위한 스텐실 버퍼 이용

o 8.3.5 코드와 설명

8.1 스텐실 버퍼 이용하기

stencil buffer 이용하기 위해서는 먼저 Direct3D 초기화하는 시점에 stencil buffer 요청해야 하며, 이용할 이를 활성화 시켜야 한다. stencil buffer 활성화/비활성화 하려면 D3DRS_STENCILENABLE 렌더 상태를 IDirect3DDevice9::SetRenderState 통해 true/false 지정해야 한다. stencil buffer 디폴트 값으로 되돌리기 위해서는 IDirect3DDevice9::Clear 이용한다:

// 세번째 인자로 D3DCLEAR_STENCIL 넣고, 여섯번째 인자는 stencil buffer clear하는데 이용될 값을 지정하는 것으로 여기에서는 0 이용하였다.
Device->Clear( 0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL, 0xff000000, 1.0f, 0 );
...
Device->SetRenderState( D3DRS_STENCILENABLE, true );
...
// stencil 관련 작업을 수행한다.
...
Device->SetRenderState( D3DRS_STENCILENABLE, false );

책에서는 다루고 있지 않지만 DirectX 9.0 그림자 볼륨을 그리는 필요한 렌더링 단계를 축소하여 그림자 볼륨의 속도를 향상시켜주는 "Two-Sided Stencil" 기능을 포함하고 있다. 자세한 내용은 여기에서 관련된 토픽을 찾아보자.

"Creating Reflections and Shadows Using Stencil Buffers (Mark J. Kilgard)" 따르면 depth buffer 이용하는 요즘의 하드웨어에서는 stencil buffer 이용하는데 드는 비용이 거의 존재하지 않는다고 한다.

8.1.1 스텐실 버퍼 요청하기

stencil buffer depth buffer 만들 함께 만들 있으며, depth buffer 포맷을 지정할 stencil buffer 포맷도 함께 지정할 있다. 실제로 stencil buffer depth buffer 동일한 off-screen 버퍼를 공유하며, 픽셀 내의 메모리 세그먼트만 해당 buffer 이용될 뿐이다.

D3DFMT_D24S8

32비트 depth/stencil buffer 만들어 depth buffer에는 픽셀 24비트를, stencil buffer에는 픽셀 8비트를 할당한다.

D3DFMT_D24X4S4

32비트 depth/stencil buffer 만들어 depth buffer에는 픽셀 24비트를, stencil buffer에는 픽셀 4비트를 할당한다(나머지 4비트는 사용하지 않는다).

D3DFMT_D15S1

32비트 depth/stencil buffer 만들어 depth buffer에는 픽셀 15비트를, stencil buffer에는 픽셀 1비트를 할당한다.

일부 그래픽 카드는 8비트 stencil buffer 지원하지 않는다.

8.1.2 스텐실 테스트

앞서 언급했듯이 back buffer 일부 영역이 렌더링되는 것을 막는 stencil buffer 이용할 있는데, 여기서 특정 피셀의 렌더링을 막을 것인지의 결정은 stencil test 통해 이루어지며, 이는 다음과 같은 표현식으로 나타낼 있다.

(참조 & 마스크) 비교 연산자 ( & 마스크)

stencil 활성화되어 있다는 가정하에 모든 픽셀에 대해 stencil test 수행되며, 개의 피연산자를 이용한다.

  • 왼쪽 피연산자(LHS, 참조 & 마스크): 애플리케이션에서 정의한 stencil 참조 값과 마스크 값의 AND 연산으로 얻어진다.
  • 오른쪽 피연산자(RHS, & 마스크): 현재 테스트하려는 픽셀의 stencil buffer 애플리케이션에서 정의한 마스크 값의 AND 연산으로 얻어진다.

비교 연산자에 지정된 방법으로 LHS RHS 비교하는 stencil test 수행한 후에 값이 true이면 back buffer 픽셀을 출력하며, false이면 픽셀이 출력되지 않는다. 당연하지만, back buffer 픽셀이 쓰여지지 않으면 depth buffer에도 쓰여지지 않는다.

8.1.3 스텐실 테스트 제어하기

Direct3D stencil test 이용되는 변수들을 제어(참조/매스크 값과 비교 연산자) 있는 방법을 제공한다.

8.1.3.1 스텐실 참조

stecil 참조 값은 디폴트로 0이지만 D3DRS_STENCILREF 렌더 상태를 이용해 값을 바꿀 있다.

Device->SetRenderState( D3DRS_STENCILREF, 0x1 );
// 16진수를 이용하면 정수의 비트 배열을 확인하거나 AND 등의 비트 연산 시에 유리하다.

8.1.3.2 스텐실 마스크

stencil 마스크 값은 참조와 변수 양쪽의 비트를 마스크하는 데에 이용된다. 디폴트 마스크는 0xffffffff이며, 이는 어떤 비트도 마스크하지 않겠다는 의미이다. D3DRS_STENCILMASK 렌더 상태를 이용하여 변경할 있다.

// 상위 16비트를 마스크한다.
Device->SetRenderState( D3DRS_STENCILMASK, 0x0000ffff );

8.1.3.3 스텐실

값은 stencil test 수행하고 있는 현재 픽셀의 stencil buffer 값이다. 각각의 stencil 값을 지정할 수는 없지만 stencil buffer clear 수는 있다. 또한 부가적으로 stencil 렌더 상태를 이용하면 stencil buffer 쓰여질 것에 대한 제어가 가능하다.

8.1.3.4 비교 연산자

D3DRS_STENCILFUNC 렌더 상태를 이용하여 비교 연산자를 지정할 있으며 값으로는 D3DCMPFUNC 열거형의 멤버 하나이다:

typedef enum _D3DCMPFUNC {
D3DCMP_NEVER = 1, // stencil test 항상 실패한다.
D3DCMP_LESS = 2, // LHS < RHS 경우 stencil test 성공한다.
D3DCMP_EQUAL = 3, // LHS = RHS 경우 stencil test 성공한다.
D3DCMP_LESSEQUAL = 4, // LHS <= RHS 경우 stencil test 성공한다.
D3DCMP_GREATER = 5, // LHS > RHS 경우 stencil test 성공한다.
D3DCMP_NOTEQUAL = 6, // LHS != RHS 경우 stencil test 성공한다.
D3DCMP_GREATEREQUAL = 7, // LHS >= RHS 경우 stencil test 성공한다.
D3DCMP_ALWAYS = 8, // stencil test 항상 성공한다.
D3DCMP_FORCE_DWORD = 0x7fffffff
} D3DCMPFUNC;

8.1.4 스텐실 버퍼 갱신하기

특정 픽셀이 back buffer 쓰여질지 여부를 결정하는 이외에도 다음과 같은 가지 경우에는 stencil buffer 항목이 갱신되는 방법을 정의할 있다:

(i, j)번째 픽셀에서 stencil test 실패

D3DRS_STENCILFAIL 렌더 상태를 지정하여 이러한 상황이 발생했을 stencil buffer 내의 (i, j)번째 항목을 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILFAIL, StencilOperation );

(i, j)번째 픽셀에서 depth test 실패

D3DRS_STENCILZFAIL 렌더 상태를 지정하여 (i, j)번째 항목이 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILZFAIL, StencilOperation );

(i, j)번째 픽셀에서 depth test stencil test 성공

D3DRS_STENCILPASS 렌더 상태를 지정하여 (i, j)번째 항목을 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILPASS, StencilOperation );

여기에서 사용되는 StencilOperation 값에는 다음 상수 하나를 사용할 있다:

D3DSTENCILOP_KEEP

stencil buffer 항목을 변경하지 않는다(현재의 값을 유지한다).

D3DSTENCILOP_ZERO

stencil buffer 항목을 0으로 지정한다.

D3DSTENCILOP_REPLACE

stencil buffer 항목을 stencil 참조 값으로 대체한다.

D3DSTENCILOP_INCRSAT

stencil buffer 항목을 증가시킨다(증가된 값은 최대치를 넘지 않는다).

D3DSTENCILOP_DECRSAT

stencil buffer 항목을 감소시킨다(감소된 값은 0보다 작지 않다).

D3DSTENCILOP_INVERT

stencil buffer 항목을 반전시킨다.

D3DSTENCILOP_INCR

stencil buffer 항목을 증가시킨다(증가된 값이 최대치를 넘을 경우 0으로 돌려진다).

D3DSTENCILOP_DECR

stencil buffer 항목을 감소시킨다(감소된 값이 0보다 작을 경우 최대치로 돌려진다).

8.1.5 스텐실 쓰기 마스크

지금까지 언급한 렌더 상태 이외에도 stencil buffer 쓰여지는 모든 값을 마스크하는 쓰기 마스크를 설정할 수도 있다. 쓰기 마스크의 디폴트 값은 0xffffffff이며, D3DRS_STENCILWRITEMASK 렌더 상태를 이용하여 지정할 있다:

// 상위 16비트를 마스크한다.
Device->SetRenderState( D3DRS_STENCILWRITEMASK, 0x0000ffff );

8.2 예제 애플리케이션: 거울

예제 파일 다운로드: chapter8_source.zip (8 예제 모두 포함)

Figure. 거울 예제 실행 화면

단순한 구현을 위해서 평평한 표면의 거울을 구현하는 것으로 내용을 제한한다. 거울을 구현하기 위해서는 다음 가지 문제를 해결해야 한다:

올바르게 반사를 그려내기 위해 임의의 평면에 물체가 반사되는 방법을 알아야 한다.

벡터 기하학을 통해 해결할 있다.

거울 영역에만 반사 효과가 나타나도록 해야 한다.

stencil buffer 통해 해결할 있다.

8.2.1 반사를 위한 수학

임의의 평면 n^ . p + d = 0 v = (vx, vy, vz) 반사된 v' = (v'x, v'y, v'z) 계산하는 방법을 알아본다. 다음 그림을 참고하자:

Figure. 임의의 평면에 대한 반사. k v에서 평면으로 부호를 가진 최단거리이며, 그림에서 v 평면의 양의 영역에 있으므로 k역시 양수가 된다.

PART 1 "평면" 섹션에서 q = v - kn^임을 배웠다. k v에서 평면으로의 부호를 가진 최단거리이므로, 평면 (n^, d) 대한 v 반사는 다음과 같이 얻을 있다:

v' = v - 2kn^
= v - 2(n^ . v + d)n^
= v - 2( (n^ . v)n^ + dn^ )

v에서 v'로의 변환은 다음 행렬로 표현할 있다.

D3DX 라이브러리는 R 같은 임의의 평면에 대한 반사 행렬을 만들어내는 함수, D3DXMatrixReflect 제공한다:

D3DXMATRIX *WINAPI D3DXMatrixReflect(
D3DXMATRIX *pOut, // 결과 행렬
CONST D3DXPLANE *pPlane // 반사할 평면
);

여기에서는 반사 변환에 대해 살펴보고 있으므로 반사 변환의 가지 특수한 경우를 확인해보도록 하자. 가지 특수한 경우란 표준 좌표 평면인 yz 평면, xz 평면, xy 평면을 말하는 것으로 다음과 같은 가지의 행렬을 통해 나타낼 있는데, 만약, yz 평면 반대쪽의 포인트를 반사하기 위해서는 x성분의 반대를 취하면 된다. 나머지 경우도 비슷하다.

8.2.2 거울 구현의 개관

거울을 구현할 중요한 요점 가지는 거울의 앞에서만 반사된다는 것이다. 경우 물체가 거울 앞에 있는지를 공간적으로 확인한다면 너무 복잡한 작업이 필요할 것이다. 따라서, 거울을 포함하는 모든 표면에서 항상 물체를 반사시키도록 렌더링하는 방법을 선택하고, stencil buffer 이용하여 back buffer내의 특정 영역이 렌더링되는 것을 막아준다. 이와 같은 작업을 위해서는 다음과 같은 순서를 따른다:

1. 바닥과 , 거울, 주전자를 포함하는 전체 장면을 (보통 때와 마찬가지로) 렌더링한다. 아직 주전자의 반사는 포함되지 않으며, 단계에서는 아직 stencil buffer 수정하지 않는다.

2. stencil buffer 0으로 clear한다.

3. 거울을 구성하는 기본형을 stencil buffer에만 렌더링한 다음, stencil test 항상 성공(D3DCMP_ALWAYS)하도록 렌더 상태를 변경하고 test 성공하면 stencil buffer 항목을 1 대체(D3DSTENCILOP_REPLACE)하도록 지정한다. 이렇게 하면 거울만을 렌더링하는 것이므로 거울에 해당하는 픽셀만 1이란 값을 가지며, 나머지 영역은 0 된다.

4. 이제 반사된 주전자를 back buffer stencil buffer 렌더링한다. 하지만 이번에는 stencil test 통과한 부분만 back buffer 렌더링 된다. , stencil buffer 항목이 1 경우에만 테스트를 통과하도록 지정한다. stencil buffer 내의 거울에 해당하는 항목만이 1 값을 가지므로 반사된 주전자는 거울에만 렌더링된다.

8.2.3 코드와 설명

예제와 관련된 코드는 RenderMirror 함수에 포함되어 있다. 함수는 stencil buffer 거울을 렌더링하고, 거울에 해당되는 부분에만 반사된 주전자를 렌더링 한다. 코드를 단계적으로 살펴보자.

8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 지정

Device->SetRenderState(D3DRS_STENCILENABLE, true); // 스텐실 활성화
Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); // 스텐실 테스트가 항상 성공하도록 지정
Device->SetRenderState(D3DRS_STENCILREF, 0x1); //
Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff); //
Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff); //
Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP); // 깊이 테스트가 실패하면 스텐실 버퍼 항목을 유지함
Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP); // 스텐실 테스트가 실패하면 스텐실 버퍼 항목을 유지함(D3DCMP_ALWAYS 인해 테스트가 실패하지는 않는다)
Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE); // 깊이/스텐실 테스트가 모두 성공하면 스텐실 버퍼 항목을 스텐실 참조 , 0x1 대체

8.2.3.2 스텐실 버퍼에 거울 렌더링

D3DRS_ZWRITEENABLE false 지정하면 depth buffer 쓰여지는 것을 막을 있다. , D3DRS_SRCBLEND D3DBLEND_ZERO, D3DRS_DESTBLEND D3DBLEND_ONE으로 지정하고 블렌딩을 이용하면 back buffer 갱신되는 것을 막을 있다. 이는 blending 방정식에 직접 인수를 넣어 확인할 있다:

Pixelresult
= Pixelsource
(0, 0, 0, 0) + Pixeldestination (1, 1, 1, 1)
= (0, 0, 0, 0) + Pixeldestination
= Pixeldestination

// disable writes to the depth and back buffers
Device->SetRenderState(D3DRS_ZWRITEENABLE, false);
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

// draw the mirror to the stencil buffer
Device->SetStreamSource(0, VB, 0, sizeof(Vertex));
Device->SetFVF(Vertex::FVF);
Device->SetMaterial(&MirrorMtrl);
Device->SetTexture(0, MirrorTex);
D3DXMATRIX I;
D3DXMatrixIdentity(&I);
Device->SetTransform(D3DTS_WORLD, &I);
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 18, 2);

// re-enable depth writes
Device->SetRenderState( D3DRS_ZWRITEENABLE, true );

8.2.3.3 거울로 렌더링 부분 표시

이제 stencil buffer내의 거울에 해당하는 픽셀은 0x1 값을 가지게 된다. 이제 주전자 렌더링을 준비한다.

// only draw reflected teapot to the pixels where the mirror was drawn to.
Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL);
Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP);

새로운 비교 연산자를 지정하고 다음과 같이 stencil test 구성한다:

(ref & mask) == (value & mask)
(0x1 & 0xffffffff) == (value & 0xffffffff)
(0x1) == (value & 0xffffffff)

이제, 반사된 주전자 중에서 거울에 비취는 부분만 렌더링된다.

8.2.3.4 장면 내에서 반사될 위치 지정

반사될 위치를 지정하기 위해 먼저 반사되지 않은 주전자 위치로 이동한 , xy 평면으로 반사를 수행한다(변환의 순서는 행렬을 곱하는 순서에 따라 정해진다).

// position reflection
D3DXMATRIX W, T, R;
D3DXPLANE plane(0.0f, 0.0f, 1.0f, 0.0f);
D3DXMatrixReflect(&R, &plane);

D3DXMatrixTranslation(&T,
TeapotPosition.x,
TeapotPosition.y,
TeapotPosition.z);

W = T * R;

8.2.3.5 반사된 주전자 그리기

// 반사된 주전자의 깊이가 거울의 깊이보다 크므로(거울이 반사된 주전자를 가리므로),
// 현재 상태에서는 반사된 주전자를 그려도 나타나지 않는다. 따라서, depth buffer clear한다.
Device->Clear(0, 0, D3DCLEAR_ZBUFFER, 0, 1.0f, 0);
// 단순히 depth buffer clear하면 반사된 주전자가 거울 전면에 그려지게 되는데,
// 이는 원하는 결과와는 차이가 있다. 따라서,
// depth buffer clear함과 동시에 반사된 주전자를 거울과 섞어(blend) 주어야 한다.

// 거울과 반사된 주전자와의 blending한다:
// result_pixel = source_pixel dest_pixel + dest_pixel (0, 0, 0, 0)
// = source_pixel dest_pixel
Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR);
Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

// 반사된 주전자를 그릴 준비가 완료 되었다. 이제 반사된 주전자를 그린다.

Device->SetTransform(D3DTS_WORLD, &W); // W 반사된 주전자를 장면 내의 적절한 위치로 이동시킨다.
Device->SetMaterial(&TeapotMtrl);
Device->SetTexture(0, 0);
Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // 물체가 반사될 때에 물체의 전면과 후면이 뒤바뀌기는데 반해 winding-order 유지된다.
// 따라서, winding-order 변경시키기 위해 후면 추려내기 방법을 변경해야 한다.
Teapot->DrawSubset(0);

// 작업이 끝나면 렌더 상태를 되돌려 준다.
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); // blending 비활성화
Device->SetRenderState( D3DRS_STENCILENABLE, false); // stencil 비활성화
Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); // winding-order 원래대로

8.3 예제 애플리케이션: 평면 그림자

예제 파일 다운로드: chapter8_source.zip (8 예제 모두 포함)

Figure. 그림자 예제 실행 화면

이와 같은 형태의 그림자는 매우 간소화된 것으로 장면의 사실감을 높여주기는 하지만 그림자 볼륨과 같이 사실적이지는 못하다. 자세한 내용은 여기에서 관련된 토픽을 찾아보도록 하자.

평면 그림자를 구현하기 위해서는 먼저 물체가 만들어내는 그림자를 찾고 렌더링할 있도록 기하학적으로 구성해야 다음, 약간의 투명도(예제에서는 50%) 가진 검은 재질을 이용해 렌더링 하면 된다. 이와 같은 렌더링에는 "double blending"이라 불리는 부작용이 따르는데, stencil buffer 이용해 이를 해결할 있다. 이에 대해서는 뒤에서 살펴본다.

8.3.1 평행 그림자

Figure. 평행 광원에 의해 발생하는 그림자

그림에서 L 방향을 가지는 평행 광원에서 p 통하는 광선은 r(t) = p + tL 얻을 있고, 광선 r(t) 평면 n . p + d = 0 교차하면 s 얻을 있다. 또한 광선 r(t) 물체의 점에서 평면으로 발사해 얻은 교차점의 집합으로 그림자의 기하 정보를 정의할 있다. 교차점 s 광선/평면 교차 테스트를 통해 간단히 얻을 있다:

1. n . (p + tL) + d = 0 : r(t) 평면 방정식에 넣는다.

2. n . p + t(n . L) = -d

3. t(n . L) = -d - n . p : t 풀어낸다.

4. t = (-d - n . p) / (n . L)

s = p + ((-d - n . p) / (n . L))L

8.3.2 조명 그림자

Figure. 조명 광원에 의해 발생하는 그림자

그림에서 L 위치에 존재하는 조명 광원에 의해 발생하는 그림자를 보여주고 있다. 조명에서 p 발사되는 광선은 r(t) = p + t(p - L) 얻을 있으며, 광선 r(t) 평면 n . p + d = 0과의 교차점으로 s 얻을 있다. 광선 r(t) 물체의 점에서 평면으로 발사해 얻은 교차점의 집합으로 그림자의 기하 정보를 얻을 있으며, 평행 그림자에서와 같은 방법(평면/광선 교차)으로 s 풀어낼 있다.

8.3.3 그림자 행렬

평행 조명을 보여주는 그림에서 그림자는 결국 지정된 광선 방향으로 평면 n . p + d = 0 물체를 평행 투영한 것이다. 비슷하게 조명을 보여주는 그림자는 광원의 관점에서 물체를 평면 n . p + d = 0 원근 투영한 것이다.

p에서 평면 n . p + d = 0으로의 투영 s 하나의 행렬로 표현될 있다. 또한, 동일한 행렬로 직각 투영과 원근 투영을 동시에 표현하는 것도 가능하다.

그림자가 만들어질 평면의 공통 평면 방정식은, 계수로 4D 벡터 (nx, ny, nz, d) 이용하고, 평행 조명의 방향이나 조명의 위치를 표현하는 4D 벡터로 L = (Lx, Ly, Lz, Lw) 이용하자. w 다음과 같이 이용된다:

1. w = 0이면, L 평행 조명의 방향을 나타낸다.

2. w = 1이면, L 조명의 위치를 나타낸다.

평면의 법선이 정규화되었다고 가정하고 k = (nx, ny, nz, d) . (Lx, Ly, Lz, Lw) = nxLx + nyLy + nzLz + dLw라고 하자.

이제 다음의 그림자 행렬을 이용해 p에서 투영 s로의 변환을 나타낼 있다.

행렬의 유도 과정은 우리에게 그다지 중요한 내용이 아니므로 다루지 않는다. 관심 있는 사람은 Jim Blinn's Corner: A Trip Down the Graphics Pipeline에서 6 "Me and My (Fake) Shadow" 부분을 참고하기 바란다.

D3DX 라이브러리에서는 그림자 행렬을 만들어내는 D3DXMatrixShadow 제공한다. 결과로 얻어진 행렬은 w = 0 경우 평행 조명에서, w=1 경우 조명에서 주어진 평면으로 그림자를 투영한다:

D3DXMATRIX *WINAPI D3DXMatrixShadow(
D3DXMATRIX *pOut, // 결과 행렬
CONST D3DXVECTOR4 *pLight, // L
CONST D3DXPLANE *pPlane // 그림자를 만들 평면
);

8.3.4 더블 블렌딩을 막기 위한 스텐실 버퍼 이용

물체의 기하 정보를 평면에 납작하게 만들어 그림자를 표현하면 이상의 펴진 삼각형이 겹치는 현상이 발생할 있다. (blending 이용해) 반투명한 그림자를 렌더링하면 겹쳐진 영역들이 여러 차례 블렌드 되어 더욱 어둡게 나타난다. 이를 해결하기 위한 방법이 바로 stencil buffer이다. , 처음으로 렌더링되는 픽셀만을 받아들이도록 stencil test 구성하여 하나의 픽셀에 이상 blending 적용되는 것을 막을 있다.

8.3.5 코드와 설명

예제와 관련된 코드는 RenderShadow 함수에 포함되어 있다. 여기에서는 stencil buffer 이미 0으로 clear했다고 가정하고 있다는 점에 주의하자.

void RenderShadow()

{

// stencil buffer 이미 0으로 clear했다고 가정한다.

/*

* 먼저 stencil 렌더 상태를 지정하고, 비교 함수를 D3DCMP_EQUAL 지정한 다음,

* D3DRS_STENCILREF 0x0으로 지정하여 stencil buffer 내의 대응되는 항목이 0x0 경우에만

* back buffer 그림자를 렌더링하도록 하였다.

* stencil buffer 0(0x0)으로 clear되어 있으므로 처음 픽셀을 때에는 항상 테스트가 성공하지만

* D3DRS_STENCILPASS D3DSTENCILOP_INCR 지정하였으므로

* 이미 쓰여진 픽셀을 쓰려고 때는 테스트가 실패한다.

* , 픽셀의 덮어쓰기를 막는 방법으로 double-blending 현상을 제거한 것이다

*/

Device->SetRenderState(D3DRS_STENCILENABLE, true);

Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL);

Device->SetRenderState(D3DRS_STENCILREF, 0x0);

Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff);

Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff);

Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP);

Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP);

Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCR); \// increment to 1

/*

* 다음에는 그림자 변환을 계산하고 장면 내의 적절한 위치로 그림자를 이동한다:

*/

// position shadow

D3DXVECTOR4 lightDirection(0.707f, -0.707f, 0.707f, 0.0f);

D3DXPLANE groundPlane(0.0f, -1.0f, 0.0f, 0.0f);

D3DXMATRIX S;

D3DXMatrixShadow(

&S,

&lightDirection,

&groundPlane);

D3DXMATRIX T;

D3DXMatrixTranslation(

&T,

TeapotPosition.x,

TeapotPosition.y,

TeapotPosition.z);

D3DXMATRIX W = T * S;

Device->SetTransform(D3DTS_WORLD, &W);

/*

* 마지막으로 50% 투명도를 갖는 검은 재질을 지정하고 depth test 비활성화 다음

* 그림자를 렌더링 한다. 렌더링 후에는 다시 렌더 상태를 원래대로 되돌려 준다.

* z-쟁탈을 막기 위해 depth buffer 비활성화 하였는데, 바닥을 먼저 렌더링하고

* depth test 상태로 그림자를 렌더링하면 의도한대로 바닥 위에 그림자를 그릴 있다.

*/

// alpha blend the shadow

Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true);

Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

D3DMATERIAL9 mtrl = d3d::InitMtrl(d3d::BLACK, d3d::BLACK, d3d::BLACK, d3d::BLACK, 0.0f);

mtrl.Diffuse.a = 0.5f; // 50% transparency.

// Disable depth buffer so that z-fighting doesn't occur when we

// render the shadow on top of the floor.

Device->SetRenderState(D3DRS_ZENABLE, false);

Device->SetMaterial(&mtrl);

Device->SetTexture(0, 0);

Teapot->DrawSubset(0);

Device->SetRenderState(D3DRS_ZENABLE, true);

Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false);

Device->SetRenderState(D3DRS_STENCILENABLE, false);

역주 : 이 글에 대한 번역본은 없는 것 같아서 올려 봅니다. 예전에도 번역했던 것 같은 기억이 있는데... 홈페이지를 날려 먹어서 기억이 안 나네요. 일단 소개의 링크에 있는 글은 한 번씩 읽어보시기 바랍니다.

 

원문 : http://www.gamedev.net/reference/articles/article2238.asp

 

Texture Splatting In Direct3D

 

by Nate Glasser

 

 

소개

 

만약 지형 텍스처링 기법에 대해 공부해 왔다면, 아마도 텍스처 스플래팅에 대해서 들어 보았을 것이다. 이 개념은 Charles Bloom 에 의해서 만들어졌는데, 그는http://www.cbloom.com/3d/techdocs/splatting.txt 에서 이에 대해 논의하고 있다 (번역본은http://blog.naver.com/dkdn111/8941198 에 있음. 번역자에 대해서는 그 글의 상위에 있음). Charles Bloom 을 무시하려는 것은 아니지만, 그것은 더 이상 명확하거나 간결한 기사가 아니며, 혼란스러움을 남겨 왔다. 많이 사용되고 추천되는 반면, 일부는 그것을 충분히 설명하는데 시간을 할애해 왔다. 나는 그것을 둘러 싼 의혹들을 명확히 하고 당신의 지형 엔진에서 그것을 구현하는 방법에 대해서 설명하고자 한다.

 

기초

 

텍스처 스플래팅이란 무엇인가? 가장 간단한 형식으로 살펴 보자면 그것은 알파맵을 사용하여 서피스 상의 텍스처를 서로 블렌딩하는 방식이다.

 

나는 알파맵이라는 개념을 사용하여 단일 채널의 텍스처에 존재하는 그레이스케일 이미지를 참조할 것이다. 그것은 어떠한 채널이라도 될 수 있다. alpha, red, green, blue, luminance. 텍스처 스플래팅에서, 그것은 주어진 위치에서 텍스처가 얼마나 가시화될 것인지를 제어하는데 사용될 것이다. 그것은 단순한 곱셈( 알파맵 * 텍스처 ) 에 의해서 수행될 것이다. 만약 알파맵의 텍셀 값이 1 이라면 그 텍스처는 그곳에서 전체 값으로 보일 것이다; 만약 알파맵의 텍셀 값이 0 이라면, 그 텍스처는 그곳에서 전혀 보이지 않게 될 것이다.

 

지형에 대해 그 텍스처는 잔디, 진흙, 암석, 눈, 또는 당신이 생각할 수 있는 여러 가지 유형의  지형일 수 있다. Bloom 은 텍스처 및 그것의 관련 알파맵을 스플랫(splat) 으로서 참조한다. 그것은 캔버스 위에 떨어진 페인트 방울(glob)로 비유될 수 있다. 스플랫은 당신이 그 페인트 방울을 볼 수 있는 모든 곳에 존재한다. 다중의 페인트 방울은 서로의 위에 겹쳐서 최종 그림을 만들어 낸다.

 

당신이 128 x 128 높이맵 지형을 가지고 잇으며, 32 x 32 의 크기를 가진 청크로 나누었다고 하자. 각 청크는 33 x 33 개의 정점으로 구성된다. 각 청크는 그 위에서 여러 번 반복되는 기저 텍스처를 가지고 있지만, 알파맵은 전체 영역에 대해서 펼쳐진다. 청크의 (0, 0) 좌표는 알파맵 좌표 (0, 0)을 가지며, 텍스처 좌표 (0, 0)을 가진다. 청크의 (33, 33) 좌표는 알파맵 좌표 (1, 1) 을 가지며, 텍스처 좌표 (x, x)를 가진다. 여기에서 x 는 텍스처가 반복되는 회수를 의미한다. x 는 텍스처 해상도에 달려 있다. 가까운 것은 충분히 반복할 필요가 있지만, 먼 것은 그렇게 많이 반복할 필요가 없다.

 

청크당 알파맵의 해상도는 당신 맘대로 하면 되지만, 2의 배승수를 추천한다. 32 x 32 청크에 대해 당신은 32 x 32 알파맵(유닛당 1텍셀), 64 x 64 알파맵(유닛당 2텍셀), 128 x 128 알파맵(유닛당 4 텍셀)을 가질 수 있다.(역주 : 여기에서 유닛(unit)이라고 하는 것은 타일을 의미한다.) 해상도를 결정할 때 주어진 청크 상에서 가시화되는 모든 텍스처에 대해서 알파맵을 필요로 한다는 것을 명심하기 바란다. 해상도가 높을 수록 블렌딩에 대한 제어는 더 많이 필요하고, 메모리를 더 필요로 하게 된다.

 

청크의 크기를 결정하는 것은 약간 미묘하다. 너무 작으면 상태 변경과 드로우 호출을 너무 많이 하게 되고, 너무 크면 알파맵의 대부분의 영역이 빈 공간이 된다. 예를 들어 유닛당 1텍셀을 가진 알파맵을 128 x 128 청크와 함께 생성하기로 했다고 할 때, 알파맵의 0 이 아닌 값이 4 x 4 영역이라면, 알파맵의 124 x 124 만큼의 메모리가 낭비된 것이다. 만약 청크 크기가 32 x 32 라면 단지 28 x 28 의 메모리만이 낭비될 것이다. 이것은 중요한 점을 상기시킨다 : 만약 주어진 텍스처가 주어진 청크에서 전혀 나타나지 않는다면, 그 청크에 그 텍스처를 위한 알파맵을 만들지 말라.

 

지형이 청크로 나뉘는 이유가 이제 나온다. 첫째 가장 중요한 부분으로 그것은 비디오 메모리를 절약할 수 있다. 둘째 그것은 필레이트(fillrate) 소비를 줄여줄 수 있다. 작은 텍스처를 사용함으로써 텍스처가 모든 청크에서 나타나지 않을 경우 비디오 카드가 수행해야 할 샘플링이 줄어든다. 셋째 그것은 지형이 어떤 식으로든 청크로 나뉘는 것을 요구하는 geomipmapping 과 같은 일반적인 LOD 기법에 들어 맞는다.

 

블렌드 생성하기

 

부드러운 블렌딩을 획득하는 핵심은 알파맵의 선형 보간이다. 0 다음에 바로 1이 온다고 가정하자. 알파맵이 전체 지형에 펼쳐질 때, Direct3D 는 두 값 사이의 블렌드를 생성한다. 그리고 나서 펼쳐진 알파맵은 지형 텍스처와 결합하여 텍스처 자체가 블렌딩되게 만든다. 

 

Rendering then becomes the simple matter of going through each chunk and rendering the splats on it. 일반적으로 첫 번째 스플랫은 완전히 불투명할 것이며, 그 다음의 스플랫들은 알파맵에서 값이 변할 것이다. 특정한 상황에 대해 설명하도록 하겠다. 첫 번째 스플랫이 진흙이라고 하자. 그것은 먼저 청크상에 나타나기 때문에, 완전히 채워진(solid) 알파맵을 가지게 될 것이다. 

첫 번째 스플랫이 렌더링된 후, 이 청크는 진흙으로 뒤덮힌다. 그리고 나서 그 위에 잔디 레이어가 추가된다 :

이 작업은 청크의 나머지 스플랫에 대해서 반복된다.

 

중요한 것은 각 청크에 대해서 같은 순서로 모든 것을 렌더링한다는 것이다. 스플랫 덧셈에는 교환법칙이 성립하지 않는다. 스플랫을 건너 뛰는 것은 어떠한 해도 끼치지 않지만, 순서를 변경하는 것은 다음과 같이 다른 모양으로 나타나게 만든다 :

잔디 스플랫은 가려진다. 왜냐하면 진흙 스플랫이 완전히 불투명하고 두 번째로 렌더링되었기 때문이다.

 

당신은 왜 첫 번째 스플랫이 불투명해야만 하는지에 대해 궁금해 할 것이다. 그것이 불투명하지 않다고 하자. 그리고 대신에 잔디 스플랫이 존재하는 곳만 채워져 있다고 하자. 다음과 같은 일이 발생할 것이다 :  

이전에 블렌딩 했던 것과 비교했을 때 좋게 보이지 않는다는 것은 명확하다. 첫 번째 스플랫을 완전히 불투명하게 만듦으로써 당신은 위의 그림처럼 나타나는 것을 막을 수 있다.
 
알파맵 생성하기
 
이제 우리는 텍스처 스플래팅이 무엇인지 알게 되었다. 우리는 캔버스를 기술하기 위해서 알파맵을 생성할 필요가 있다. 그러나 알파맵에 어떤 값을 부여할 지를 어떻게 결정해야 하는가?

 

어떤 사람들은 지형 높이에 기반해서 그것을 결정하지만, 나는 알파맵을 당신이 원하는데로 만드는 기능을 부여하는 것을 추천한다. 이것은 제약 없이 원하는 곳에 텍스처를 배치하기 위한 유연성을 제공한다. 페인트 프로그램에서 채널을 그리기만 하면 된다. 더 좋은 방법은 아티스트가 알파맵을 확인하고 실제 월드에서 수정해 볼 수 있는 간단한 월드 에디터를 생성하는 것이다. (역주 : 차라리 자동으로 계산하는 것이 훨씬 나을 듯 합니다. 직접 알파맵 제작할 바에야 그냥 통맵 만들고 말지... 그렇지만 에디터에서 알파맵을 수정하는 기능을 추가해 준다면 더 낫겠죠.)

 

구현

 

단계를 다시 돌아보고, 우리가 가진 것들을 살펴 보자 :

  1. 높이맵과 같은 지형 표현의 정렬
  2. 지형에 렌더링될 텍스처 집합
  3. 각 텍스처를 위한 알파맵 

    세 번째를 살펴 보자. 우리는 각 알파맵이 텍스처로 존재해야 함을 알고 있다. 이것은 모든 알파맵이 자신만의 텍스처를 필요로 한다는 것을 의미하는가? 고맙게도 대답은 '그렇지 않다' 이다. 알파맵은 단지 텍스처의 단일 채널로서만 존재하기 때문에, 우리는 네 개의 알파맵을 단일 텍스처로 묶을 수 있다. 하나는 red, 하나는 green, 하나는 blue, 하나는 alpha 에 넣는다. 이들 개별 채널에 접근하기 위해, 우리는 픽셀 쉐이더를 사용할 필요가 있다. 그리고 다섯개의 텍스처를 필요로 하기 때문에(하나는 알파맵과 함께, 넷은 블렌딩을 위해), PS 1.4 가 요구된다. 안타깝게도 이것은 아직까지는 무리한 요구이다. 그래서 나는 픽셀 쉐이더 뿐만 아니라 고정함수 파이프라인을 사용해 텍스처 스플래팅을 사용하는 방법에 대해서도 보여줄 것이다.

     

    고정함수 파이프라인을 사용한 스플래팅

     

    고정 함수 파이프라인을 사용하는 것은 픽셀 쉐이더 기법을 사용하지 않는다는 이점을 가진다 : 그것은 비디오 카드에서 가상적으로 실행될 것이다. 그것이 요구하는 것은 알파맵당 하나의 텍스처 유닛, 텍스처당 하나의 텍스처 유닛, 올바른 블렌딩 상태이다.

     

    나는 알파맵을 스테이지 0에 넣고, 텍스처를 스테이지 1에 넣기로 했다. 이것은 픽셀 쉐이더와의 일관성을 위한 것인데, 픽셀 쉐이더에서는 스테이지 0 에 알파맵을 설정한다. 텍스처 스테이지 스테이트는 그것보다는 상대적으로 직관적이다. 스테이지 0 은 그것은 알파값을 스테이지 1 로 전달한다. 스테이지 1 은 그 알파값을 자신의 것처럼 사용하며 자신의 색상 값과 그것을 한쌍으로 만든다.

     

    // 알파맵 : 알파맵으로부터 알파를 취함. 색상은 신경쓰지 않음.
    g_Direct3DDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
    g_Direct3DDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

    // 텍스처 : 텍스처로부터 색상을 취함. 이전 스테이지로부터 알파를 취함.
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_CURRENT);

     

    다중 스플랫을 정확하게 결합하기 위해서 블렌딩 렌더링 스테이트도 설정해야만 한다. D3DRS_SRCBLEND 는 렌더링되고 있는 스플랫으로부터 오는 알파이며, 우리는 그것을 D3DBLEND_SRCALPHA 로 설정한다. 우리가 원하는 최종 방정식은 FinalColor = Alpha * Texture + (1 - Alpha) * PreviousColor 이다. 이것은 D3DRS_DESTBLEND 를 D3DBLEN_INVSRCALPHA 로 설정함으로써 수행된다.

     

    g_Direct3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
    g_Direct3DDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
    g_Direct3DDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

     

    픽셀쉐이더를 사용한 스플래팅

     

    왜 픽셀 쉐이더에 대해서 걱정하는가? 하나의 채널을 사용하는 것보다 텍스처 내의 이용 가능한 모든 채널을 사용하는 것이 메모리를 절약해 준다. 또한 단일 패스에서 네 개의 스플랫을 렌더링할 수 있도록 해 주기 때문에 변환에 필요한 정점 개수를 줄일 수 있다. 결합되는 모든 텍스처는 쉐이더 내에 배치되기 때문에, 고려해야 하는 텍스처 스테이지 스테이트가 존재하지 않는다. 우리는 단지 스테이지 0 에다가 각 채널 단위의 알파맵을 가진 텍스처를 로드하고, 스테이지 1 부터 4까지는 텍스처를 로드하고, 그리고 나서 렌더링하면 된다.

     

    ps_1_4

     

    ////////////////////////////////
    // r0 : 알파맵
    // r1 - r4 : 텍스처
    ////////////////////////////////

     

    // 텍스처 샘플링
    texld r0, t0
    texld r1, t1
    texld r2, t1
    texld r3, t1
    texld r4, t1

     

    // 텍스처들을 그것들의 알파맵에 기반해 결합한다
    mul r1, r1, r0.x
    lrp r2, r0.y, r2, r1
    lrp r3, r0.z, r3, r2
    lrp r0, r0.w, r4, r3

     

    mul 명령어는 첫 번째 텍스처를 그것의 알파맵과 곱하는데, 이는 샘플러 0 의 텍스처의 red 채널에 저장되어 있는 알파값이다. lrp 명령어는 다음의 수학식을 수행한다 : dest = src0 * src1 + (1 - src0) * src2. r0.x 가 r1 에 저장된 진흙 텍스처의 알파맵이라고 하고, r0.y 는 r2 에 저장된 잔디 텍스처의 알파맵이라고 하자. r2 는 첫 번째 lrp 이후에 다음을 포함한다 : GrassAlpha * GrassTexture + (1 - GrassAlpha) * DirtBlended. 여기에서 DirtBlended 는 DirtAlpha * DirtTexture 이다. 당신도 알겠지만 lrp 는 이전에 우리가 설정했던 렌더 스테이트와 텍스처 스테이지 스테이트와 같은 작업을 수행한다. 최종 lrp 는 r0 을 출력 레지스터로 사용하는데, 이는 최종 픽셀 색상으로서 사용되는 레지스터이다. 이것은 최종 mov 명령어에 대한 필요성을 없애 준다.

     

    청크에 대해 두 개 혹은 세 개만의 스플랫을 렌더링할 필요가 있다면 어떻게 하는가? 만약 픽셀 쉐이더를 재사용하고자 한다면, 남은 채널들을 0 으로 채우기만 하면 된다. 그러한 방식을 사용하면 최종 결과에는 영향을 주지 않을 것이다. 아니면 두 개의 스플랫을 렌더링하거나 세 개의 스플랫을 렌더링하는 다른 픽셀 쉐이더를 생성할 수도 있다. 그러나 SetPixelShader 호출에 대한 부가적인 오버헤드는 명령어 두 개를 더 사용하는 것보다 더 안 좋을 수 있다.

    당신이 청크에 대해서 네 개 이상의 스플랫을 렌더링하고자 한다면 다중 패스가 요구된다. 7개의 스플랫을 렌더링해야 한다고 가정하자. 첫 번째로 네 개를 렌더링하고, 나머지 세 개를 렌더링한다. 두 번째 알파맵 텍스처의 알파 채널은 0 으로 채워지며, 이는 네 번째 텍스처가 방정식에서 취소되는 결과를 낳는다. 당신은 알파맵 텍스처를 설정하고 세 개의 텍스처를 블렌딩하고 렌더링하게 된다. D3DRS_BLEND 와 D3DRS_SRCBLEND 스테이지는 픽셀 쉐이더의 lrp 와 같은 작업을 수행하는데, 이는 두 번째 패스가 첫 번째와 연속적으로 결합되도록 만든다.

     

    데모 프로그램

     

    The demo application uses the two techniques described here to render a texture splatted quad. I decided not to go for a full heightmap to make it as easy as possible to find the key parts in texture splatting. Because of this, the demo is completely fillrate limited. The initial overhead of the pixel shader may cause some video cards to perform worse with it than with its fixed function equivalent, so take the frame rates with a grain of salt. The pixel shader will almost always come out ahead in a more complex scene.

    You can toggle between the fixed function pipeline and the pixel shader through the option in the View menu.

    The textures used are property of nVidia® and are available in their full resolution athttp://developer.nvidia.com/object/IO_TTVol_01.html.

     

    연결 부분의 문제점

     

    텍스처 스플래팅이 실망스러운게 있다면, 그것은 다음의 문제이다 : 두 개의 이웃하는 스플랫이 있을 때, 그것들 사이에 원하지 않는 단절 부분이 생성된다. 그 모양은 이전의 예제 스플랫을 네 번 타일링함으로써 생성될 수 있다.

     

     
    왜 이런 일이 발생하는 것일까? 위의 두 영역(section)의 알파맵을 살펴 보자.

    둘 사이에 공백이 추가되었고 문제가 가시화되었다. 왼쪽에 존재하는 것은 오른쪽에 있는 것의 경계 값을 알지 못한다. 비디오 카드가 자신의 선형 블렌딩을 수행할 때, 그것은 흰색 텍셀 다음에 검은 색 텍셀이 존재하는지의 여부에 대해서 알 방법이 없다. 그것은 경계 부분과 같은 색상이라고 간주한다.

     

    이것은 수정하기 쉬운 문제가 아니며, 많은 게임들이 그것을 방치해 둔다. 그것은 적절한 wrapping 텍스처 및 숙련된 레벨 디자이너에 의해서 감춰질 수 있다. 그러나 좋은 해결책이라 생각치는 않는다. 경험에 의하면 그것은 더 많은 문제를 가지고 있으며, 그것을 해결하는 것이 더 가치가 있다. 그리고 나는 텍스처 스플래팅의 이점이 그 이슈보다 중요하다고 믿는다.

     

    결론

     

    Hopefully this article has cleared up the mystery behind texture splatting. There are, of course, enhancements to be made, but texture splatting in its basic form is a powerful and flexible technique. It creates a smooth blend between different layers of terrain while giving detail at any distance and avoids the patterned look a detail map can give. Its main disadvantage is that it is very fillrate consuming, but with video cards becoming ever more powerful and the abilities of pixel shaders increasing, this is not an issue on modern and future hardware. Its ease of use and flexibility make it a perfect choice for texturing your terrain.

     

    소스

     

    Terrain Texture Compositing by Blending in the Frame-Buffer by Charles Bloom,http://www.cbloom.com/3d/techdocs/splatting.txt

     

    And, of course, the helpful people at http://www.gamedev.net

    Feel free to send any questions or comments to nglasser@charter.net or private message Raloth on the forums!

     

    Discuss this article in the forums


    Date this article was posted to GameDev.net: 4/23/2005 
    (Note that this date does not necessarily correspond to the date the article was written)

    See Also:
    Hardcore Game Programming 

     

    © 1999-2006 Gamedev.net. All rights reserved. Terms of Use Privacy Policy 
    Comments? Questions? Feedback? Click here!

     

    + Recent posts