프로그래밍 2017. 7. 12. 22:59
728x90

스크린 스페이스 데칼 구현 원리

 

 

이전부터 구현해 보고 싶었던 데칼!

쉐이더 작업 이전에는, 실시간으로 면을 구성해서(버텍스)

UV 를 만들고, 데칼이미지를 붙이는 방식으로,

면 구성에 있어서도 복잡성이 높고,

문제점도 많은 방식으로 알고 있었다

 

지금에는 실시간 면 구성이라는 부분이 필요없고,

씬의 장면이 렌더링된 깊이 버퍼를 이용해서,

기존의 오브젝트 렌더링 쉐이더에 추가적인 작업만 해주면,

비교적 간단하게 구현이 가능하다

 

포스트 이펙트용 2D 이미지에 덧그린다던지 하는 방식이 아니다!

기존의 오브젝트 렌더링 쉐이더 코드에 추가적인 작업이 필요할 뿐이다

렌더링 스테이트가 바뀐다던지 하는 부분도 없다

 

 

//참고

 

https://www.slideshare.net/blindrendererkr/40000

 

http://ttmayrin.tistory.com/37

 

http://ldh9451.tistory.com/entry/Screen-Space-Decal

 

 

구현에 대한 프리텐테이션과, 소스가 있음에도 불구하고,

원리에 대해서는 설명들이 좀 부족한거 같다

소스 자체는 라인도 적고, 간단한 편인데,

일단 이해가 잘가지 않으니...

 

이것저것 뒤져보고, 구현해 보면서,

이해한 내용을 정리해 놓으려 한다

관심있으신 분들은 참고하시길 ^^

 

 

픽셀의 깊이값으로 월드값을 만들어 내는 방법

참고

 

http://m.blog.daum.net/rockeracer/38

 


월드, 뷰, 프로젝션 변환이 끝난 정점을

다시 로컬위치로 되돌릴때


대략,

vP = 역프로젝션 * 정점

vP = 역뷰 * vP

vP = 역월드 * vP


요런식으로 구할수 있는 것으로 기억하는데,

오래전 공부한 내용이고, 실제로 사용한 일이 없어서,

뭔가 틀릴수도 있으니, 참고만 하자


뭐, 소스코드에서 실제 계산해서 값을 확인해보면, 명확해지겠지만

귀찮으니까 일단 패스~ ㅎㅎ


 

 

데칼 구현의 원리에 대해서 간단하게 정리해 보자면,

 

 

 

 

 

위 이미지에서 보듯이

땅과 박스의 접점 픽셀을 골라내고, (주황색 영역)

이 픽셀에 데칼 이미지를 입히는 것이라 할수 있다

 

일단, 데칼의 이미지를 입히는 부분은 빼고,

접점 픽셀을 골라내는 부분에 집중해보자

(우리가 좀더 궁금한 부분은 이부분일 것이다)

 

접점 픽셀이란 곧,

씬의 깊이버퍼의 픽셀중

박스의 min, max 값 사이에 위치하는 픽셀이라 할수 있다,

개념적으로는 매우 간단하다,

실제 이것만 구현이 가능하다면, 땅이 얼마다 울퉁불퉁하던지 간에

땅에 착 달라붙는 데칼을 만들수가 있다

 

월드영역이라면, 우리는 꽤 간단하게 구할수 있을 것이다

하나의 점이 AABB( 또는 OBB) 의 박스 영역에 들어오는지를 체크하는 것과 동일하다

 

쉽지 않은가?

 

ㅎㅎㅎ

 

그런데, 사실, 쉐이더의 PS 내부가 아니라면, (cpp 소스 구현 상에서는)

하나하나의 픽셀의 위치를 알수는 없다,

그러므로, 픽셀 하나하나가 박스 경계속에 포함되는지 체크해 볼수가 없다

 

그럼, 쉐이더 연산에서는 구할수 있을까?

PS() 에서는,

하나 하나의 픽셀을 처리하므로, 가능하다

 


직관적인 월드값 비교에 대해서 상상해보자

만약,

땅의 픽셀의 월드값을 알수 있다면,

픽셀 쉐이더 함수에서,

데칼박스의 min, max 월드값( 월드상에서의 값, 박스를 회전시키지 않는다는 가정하에)

사이에 위치하는 지를 체크하고,

매핑을 시키던지, 색상을 바꾸던지해서 표시해볼수 있을 것이다,

 

 

실제 구현을 한다고 생각해 보자

 

1. 데칼 박스를 그릴때

PS() 에서, 장면이 그려진( 위의 이미지에서는 땅 ) depth 버퍼를 가져와,

데칼 박스와 겹쳐지는 땅픽셀의 깊이값을 가져올수 있다

 

픽셀의 깊이값이 있으면, 알려진 몇 개의 방법으로,

월드값으로 바꾸어줄수 있다

 

이 픽셀의 월드값이,

PS() 로 넘겨진, 데칼 박스의 min, max 월드값 사이에 있는지만 체크해서,

사이에 있다면, 이미지를 매핑하여

 

땅에 달라붙은 데칼을 그릴수 있다

 


-> 사실, 이론상으로만 적어보았고, 테스트를 해보지는 않았다

 

-> 데칼박스를 렌더링할때 PS() 에서

-> 렌더링하는 픽셀 위치에 해당하는

-> 깊이버퍼의 깊이값을 구하고( z 값만이 있으므로), xy 값도 구해둔다 (월드위치값)

-> 이 월드값이

-> 데칼박스의 월드값 영역안에 있는지를 체크하면,

-> 위 이미지에서의 주황색 영역을, 정확히 구할수 있다라는 것을 이야기하고 싶었다

 

-> 땅이 울퉁불퉁하다던지, 면이 어떤식으로 얽혀있다던지 아무런 문제가 되지 않는다

-> 픽셀 하나하나가 영역안에 있는지를 체크한 것이기 때문데

 

-> 결론적으로, 

-> 데칼박스의 회전이 없는 경우이고,

-> 화면안에서 데칼박스가 여러개 있을때라든지,

-> 최종적으로 uv 를 어떤식으로  매핑시킬 것인지

-> 이것저것 문제가 많은 방법이긴 하다

 

 

알려진 방식으로 구현한 스크린 스페이스 데칼

(밑의 이미지들)

 

 

 

 

 

 


위에 링크걸어둔 사이트에서의 데칼 구현 방식에 대해서 단계적으로 나누어 보자면,

(최종적으로는 픽셀의 local 값을 구해서 비교한다, 이 방법이 여러가지 측면에서 완전하다)



1. 데칼을 그릴때, PS 에서 

우리가 잘아는 방식으로 픽셀의 uv (0~1) 를 계산해서, 뎁스버퍼에서 uv 에 해당하는 깊이값을 가져온다


2. 이 깊이값을 위 링크된 블러그에서 설명한대로,

역프로젝션 변환등을 이용하여, 최종적으로 local 값으로 변경시킨다


(여기서 좀 헷갈릴수 있는데, 

기본적으로는 데칼을 렌더링하면서, -데칼박스를 월드 어딘가에 렌더링하면서- 

진행되는 과정이라는 것을 기억해야 한다, 즉 PS_Decal() 내의 과정이라는 것,

z 값은 데칼이 자신의 z 값을 사용하지않고, depth buffer 의 깊이값을 사용한다는 것,


사실 데칼의 로컬값은 이미 알고 있으니, 

(데칼은 정점의 값이 min xyz(-0.5,-0.5,-0.5) max xyz( 0.5, 0.5, 0.5 )인 정육면체이다)

데칼은 실제 역변환 과정을 거칠 필요가 없다,


그런데도 데칼을 그릴때, 역변환과정을 거치는 이유는,

decal uv로 depth buffer 의 깊이값을 읽어와야하고, 

이 읽어들인 깊이값을 local 로 만들기위해

decal 의 invViewWorld 를 이용한다는 것이다


즉 일단은, depth buffer 의 깊이값이 decal 영역에 있다고 가정하고,

decal 의 역변환 과정을, 그대로 수행해주는 방법이라고 볼수있다

역변환 과정을 거친 최종 local 값이, 

-0.5~0.5 범위가 아니면은 decal 범위가 아니라고 판단할수 있다는것

)


3. 데칼박스의 local 은 무조건 (-0.5 ~ 0.5) 범위 이므로,

깊이 local을, 데칼박스 local 범위체크를 해서, 범위안의 픽셀은 적당하게 색칠해준다




local 값을 구하기 위한 역변환 계산에는 여러가지 방식이 있는데,



구현에 참고한 코드에서는,

Proj 을 Inv 한후, Vector4 에 프로젝션의 화면정보의 역값만을 저장

(실제 사용하는 값은 x, y 값으로, 값을 확인해보면, -1 ~ 1 사이의 값들이다)

(매트릭스로 사용하지않고, Vector4 로 사용)


zMatrix matInvProj;
zMatrixInverse( &matInvProj, NULL, &g_matProj );

 

Vector4 vInvProj;
vInvProj.x = 1;
vInvProj.y = 1;
vInvProj.z = 1;
vInvProj.w = 1;

D3DXVec4Transform( &vInvProj, &vInvProj, &matInvProj );
vInvProj.y = -vInvProj.y; //?

(vInvProj 을 PS에 넘겨준다)



View 와 World 의 inv 는 미리 소스코드에서 계산후 PS 에 넘겨준다

View 와 World 의 inv 를 구하는 방법은 대략 요런식으로

하나의 matrix 값으로 만들어 사용할수 있다


//g_matView 는 전역 카메라뷰

//matDecalWorld 는 데칼박스의 회전, 스케일, 이동이 포함된 매트릭스


//참고로, 매트릭스 곱셈의 경우 A*B 의 결과와 B*A 결과는 다르다

//매트릭스 곱셈을 할경우, 순서가 무척 중요하다


-> zMatrix zMatInv, zMatW;
-> zMatrixInverse( &zMatInv, NULL, &g_matView );    
-> zMatrixInverse( &zMatW, NULL, &matDecalWorld );

-> zMatrixMultiply( &zMatInv, &zMatInv, &zMatW );

-> zMatInv 값을 넘겨준다 ( View 와 World 의 Inv 를 하나로 묶은 matrix )

(데칼박스를 그릴때, PS 에 넘겨주어야 할 값이다) 




최종적으로, 로컬위치를 비교하는 부분에 대해 추가적으로 설명해보자면,

 


씬의 픽셀의 local 위치값을

데칼박스의 local 값 min, max 와 비교해서,

영역안에 들어온다면, 현재 렌더링되는 데칼박스의 pixel 색상을 변경

-> 어차피 겹쳐지는 부분이므로,

-> 현재 렌더링되는 데칼박스의 pixel 의 색상을 조정해주면, 이것이 데칼로 표시된다

-> 이것이 핵심!

 

주의할것은, 데칼박스는 계산의 편의를 위해서,

정점의 xyz 위치가 -0.5 ~ 0.5 사이의 값들로 이루어진 박스라는 것이다

(그대로 맵에 띄워보면, 아주 아주 작아서 찾기 힘들정도다)

(맵에서는 필요한만큼 스케일해서 사용)

 

min( -0.5, -0.5, -0.5 ) max( 0.5, 0.5, 0.5 ) 

이렇게 구성이 되어 있으니, UV 값 역시 1:1 매칭이 가능하다

 



그래서, PS() 에서 이런식으로 구현이 가능해진다

 

//decalLocalpos 는 depth buffer 픽셀의 로컬위치값

 

//local xyz 각각의 축으로 0.5 범위가 넘으면 버려진다

clip( 0.5 - abs( decalLocalpos.xyz ) );

 

// 위치값을 그대로 uv 값으로 사용

TexUV = decalLocalpos.xz + 0.5f;    

 

 


구현 소스는 위에 링크된 블러그 들에서 자세하게 구현되어 있으니

(나역시 거의 그대로 베껴서... ^^ 감사합니다 ~! )

생략해도 될듯


 



_______________쉐이더 구현에 대한 코드를 추가_________________



PS_Decal()

{

      float3 uv = GetPixelUV( _In ); 

      float4 vPixelDepth = tex2D( Samp_Tex_DepthBuffer, uv.xy ); 

      float3 decalLocalpos = GetPixelLocal( uv, vPixelDepth.a );

 

      clip( 0.5 - abs( decalLocalpos.xyz ) );

      

      

      영역안의 픽셀들은 매핑 처리

      우리가 원하는 데칼로 출력된다

      

      float2 decalUV = decalLocalpos.xz + 0.5;


      이런식으로, 데칼uv 를 간단하게 계산할수 있지만,

      데칼 출력에는 여러가지 상황이 있기 때문에,

      실제 decalUV 구성은, 좀더 고민해 줄 필요가 생긴다


}



//모든 변환이 끝난 픽셀에서 uv 계산하기

float2 GetPixelUV(  정점정보 _In )

{

float4 vWVP = _In.wvp;

 

        /*

        모든 변환이 끝난 최종단계의

   vWVP.w 에는 카메라로부터의 거리값이 보존되어있다

  이 깊이값은 proj 의 원근값과는 상관이 없다

   proj 의 어떠한 값도 vWVP.w 에 영향을 끼치지 않는다


   proj 를 곱할때, 

   w 에는 proj 의 원근값이 곱해지는것이 아니고,

   v * world * view 의 결과깊이값만을 보존하고 있다 

   이때의 결과깊이값 == 카메라로부터의 거리값

   이때 x,y,z 에는 이 거리값이 곱해져 저장된다

  

   이때의 w 값은 특이한 성격으로,

   이부분에 대해서는 (동차좌표와 w) 내용을 살펴보자


         최종적인 vWVP.xy 에는 카거리값이 곱해져 있으므로,

         vWVP.xy / vWVP.w 로 카거리값을 상쇄시키면, 

         프로젝션 영역 (-1~1) 안의 값을 만들수 있다

         */


//tex2Dproj 와 같은 작동

//위치좌표를 텍스쳐 uv 범위로 변환시킨것 

float2 uv = vWVP.xy / vWVP.w;       -> (-1~1)

uv = uv * float2(0.5, -0.5) + 0.5;     -> (0~1)


return uv;

}




//픽셀의 로컬포지션 구하기

//매트릭스의 역변환 과정을 거쳐서, local 좌표로 변환

float3 GetPixelLocal( float2 _uv, float _fDepth )

{

        /*

  _uv값은 w 로 나누어져 있다


  이때의 vInvProj 은, 보통의 모든 변환이 끝난 정점의 경우와는 다르다

  즉, world, view 매트릭스가 곱해지지 않았다

  로컬값도 1,1,1,1 일뿐이고,


  view(camera view) 매트릭스를 곱하지 않았으므로, 

  카메라와의 거리값이 적용되지 않았다

  즉, view(camera view) 매트릭스를 곱하지 않았을때는, /w 를 할필요가 없다

   /w 는 카메라로부터의 거리로 나눠주는 과정이니까


  여기서는, 오직 proj 의 inv

  즉 proj 의 정보만을 담고있다 (proj 의 화면값의 역정보)

  

  D3DXVec4Transform( &vInvProj, &vInvProj, &matInvProj ); 에서

  vInvProj.z = 1 이므로, 계산상 vInvProj.w = 1이 저장되어야 하는게 맞지만, 


  zMatrixInverse( &matInvProj, NULL, &g_matProj ); 할때

  matInvProj 의 경우, Proj 와 매트릭스성분이 달라지는 부분이 생겨, 

  결과적으로, w 에 역행렬 변환관련값이 저장된다

  일반적이지 않은, 특이한 경우이다

  이때의 vInvProj.w 는 사용할수 없다

  */

 

  //역변환 과정 처리

  //Inv Proj 적용

  //0~1 사이의 xy 값을 -InvProj.xy ~ vInvProj.xy 사이의 값으로 선형보간

float3 vProj = float3( lerp(-vInvProj.xy, vInvProj.xy, _uv), vInvProj.z );


        //_fDepth 는 자신의 카메라거리값이 아닌, depth buffer 의 깊이값(카메라거리값)

float4 vPos = float4( vProj * _fDepth, 1.f ); 

  


  자, 여기서 잠깐 생각해보자

  왜 depth buffer 의 카메라거리값을 곱해줄까, 자신의 카메라거리값이 아니라?


  자신의 카거리값을 곱해주는 경우를 생각해보자

  이것은 최종적으로 항상 데칼박스의 로컬포지션 -0.5 ~ 0.5 안에 있을것이다

  당연히, 자기 픽셀은 자신영역안에 있으니까


  역으로 생각하면,

  depth buffer 의 카거리값을 곱해서, 최종적으로 나온 로컬값이 

  -0.5 ~ 0.5 안에 있다면,

  우리는

  depth buffer 의 픽셀이, 데칼박스안에 위치한다고 판단할수 있다,

  (이때 우리는 데칼을 입힌다)

  그렇지 않다면, 데칼박스안에 위치하지않는 경우일 것이고



        //_matInvWV = 자신(데칼박스)의 world * view 의 inv 

float3 Localpos = mul( vPos, _matInvWV );

        

        //

return Localpos;

}



하트 콕~ ^^



728x90
posted by BK dddDang
: