Notice
Recent Posts
Recent Comments
Link
반응형
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

RenderLog

[번역]Dual paraboloid shadow mapping 본문

Graphics/참고자료

[번역]Dual paraboloid shadow mapping

scahp 2020. 2. 15. 19:22

개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.

또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.

원문 : http://gamedevelop.eu/en/tutorials/dual-paraboloid-shadow-mapping.htm

 

Preface

이 튜도리얼은 DirectX effect와 Shader model 3.0을 사용하여 이 주제를 설명합니다. 구현을 위한 렌더타겟들, effect 변수 설정 그리고 오브젝트의 변환 지식은 이 예제를 이해하기위한 필수요소입니다. 이 예제는 다른 쉐이더 언어로 쉽게 변환될 수 있습니다.

 

Introduction

컴퓨터 그래픽에서 Shadow mapping은 Shadow를 그리기 위한 기술입니다. 이 방법에서는 광원의 관점(viewpoint)에서 깊이값 텍스쳐(shadowmap)가 생성되어집니다. Scene을 그릴때, 필셀들의 깊이 값이 Shadow map에 있는 깊이 값과 비교되어 집니다. 이를 위해서 픽셀이 광원의 좌표 시스템으로 변환되어져야만 합니다. 깊이 값이 더 낮을 수록 픽셀들은 그림자 안에 있습니다. (역주 : 깊이 값이 더 낮을 수록 카메라와 가까운 경우이고, 그런 경우 이 Depth 값으로 인해 그림자가 만들어지는 픽셀이 더 많아질 것이란 의미로 보이네요.). 하나의 shadow map에만 적용되는 이 기법은 Spot light에서만 가능합니다. 만약 당신이 여러방향으로 빛을 비추고 싶다면, (즉, 포인트라이트라면) 이 방법은 더이상 충분하지 않습니다. 더 명확한 해결책은 빛이 비추는 각각의 방향마다 Shadow map을 사용하는 것입니다. 그결과 포인트 라이트는 6개의 쉐도우맵을 만들 것입니다. 그나저나 이 방법은 Cubical shadow mapping이라고 불립니다. 그럼에도 불구하고 퀄리티에 문제가 없고, 6개의 쉐도우맵을 렌더링 해야하기 때문에 성능은 좋지 않습니다. 

이 문제를 해결하기 위한 다른 방법은 Spherical shadow mapping으로 불립니다. 이 기술은 전통적인 Shadow mapping 기법 처럼 단 1개의 Shadow map을 사용합니다. 그래서 거의 기존것 처럼 빠릅니다. 그러나 불균일한 sample rate가 왜곡을 만들어서 퀄리티 문제가 있습니다. 더 연구하여 Dual paraboloid 를 얻어냈습니다. 1998년 “View-independent environment maps” 에 Wolfgang Heidrich 와 Hans-Peter Seidel에 의해 처음 기술되었습니다.
여기서 알아야할 점은 현재까지 기술한 Shadow mapping 문제는 Environment mapping에서는 깊이 값 대신에 컬러 값을 사용하는 것을 제외하면 Environment mapping 문제와 거의 일치한다는 점입니다. Paraboloid environment mapping에 사용된 paraboloid map 의 그림을 보여드리고 싶습니다. 이 그림을 목적은 여러분에게 Paraboloid projection에 대한 통찰력을 드리는 것입니다. 그것을 Paraboloid shadow map으로 하는것은 어렵습니다. 우리는 이것을 나중에 다시 볼 것입니다. 그러나 우리는 Dual paraboloid shadow mapping 을 주제로 계속 남겨두기를 원합니다.

 

 

 

Paraboloid environment mapping

 

 

 

Paraboloid maps 은 Spherical maps 보다 좋은 성능과 높은 퀄리티를 제공합니다. 먼저 Elliptical(타원형) paraboloid과 비슷한 것을 우리는 사용할 것입니다.

 

 

 

Elliptic paraboloid

 

 

 

Paraboloid의 장점은 초점으로 들어오는 입사광선들이 Paraboloid 방향과 평행하게 반사되어지는 것입니다. 그리고 그것은 투영 계산을 쉽게 해줍니다.

 

 

 

Paraboloid reflection

 

 

 

Praboloid mapping을 사용할 때, 광원을 둘러싼 공간은 2개의 반구(hemispheres-paraboloids)로 나누어집니다.

앞쪽과 뒤쪽의 반구가 있습니다. 그리고 그것은 위에 있는 그림처럼 열려있는 쪽 방향으로 서로 붙여두어 둘러쌓여진 공간을 형성합니다.

 

Mathematic derivation

먼저 목적에 적합한 Paraboloid를 결정합니다.

 

 

 

 

 

 

렌더링된 각각의 Vertex는 Paraboloid 표면위에 투영되어져야 합니다. 그림으로 설명:

 

 

 

Paraboloid projection

 

 

 

투영된 점의 x와 y 좌표를 찾아야만 합니다. 이 좌표계는 투영된 점에서의 normal vector를 사용하여 계산 될 수 있습니다. 이것을 위해 우리는 표면의 normal vector의 정의가 필요합니다. 이것은 Tangents의 Cross Product로 얻어낼 수 있습니다. 우리는 x, y에 대한 함수의 편미분(the partial deviation)으로 구할 수 있을 것입니다.

 

 

 

 

 

 

입사 투영 광선의 방향벡터는 Paraboloid 좌표계의 정규화된 Vertex 위치와 일치합니다. 만약 방향벡터에 반사벡터를 더한다면, 우리는 프로젝션 된 점의 길이는 다르지만 normal 방향의 vector를 얻을 수 있습니다. 반사 벡터는 Z-axis 방향이기 때문에 우리는 그냥 입사벡터의 Z-value에 1을 더해주기만 하면됩니다.

만약 위의 유도에서 이러한 것을 처리하면, 더해진 벡터를 z-value으로 나눠주는 것으로 원하는 프로젝션 된 점의 x, y 좌표를 계산해낼 수 있는 것으로 보입니다.

 

 

 

 

 

 

Implementation

a). Paraboloid shadow maps 의 생성:

Paraboloid shadow maps은 첫 2개의 렌더패스에서 생성되어져야 합니다. 나는 1024 x 1024 해상도를 maps을 위해 사용하였지만 이것은 원하는 대로 설정 가능합니다. 더 높은 해상도는 더 나은 Shadow 퀄리티를 제공하지만 성능은 더 나빠질 것입니다. 텍스쳐 포맷은 R32F floating point format을 추천합니다.

가장먼저, Shadow map의 앞쪽면을 렌더타겟을 설정해야 합니다. 다음으로 World, View Matrix를 Paraboloid 공간을 위해 생성하고 Effect의 parameter로 설정합니다. 절대공간에서의 변환은 World matrix와 일치하며 View matrix는 Light의 관점으로 부터 Scene을 렌더링 할 수 있게 해줍니다. 이 matrix의 위치는 Light의 위치이며 방향은 자유롭게 선택할 수 있습니다. 나는 방향을 오브젝트들을 향하도록 선택하여서 오브젝트들이 빛을 받을 수 있게 할것입니다. Projection matrix는 Projection을 직접 수행할 것이므로 필요하지 않습니다.

이제 우리는 쉐이더를 구현할 수 있습니다. 종종 쉐이더가 더 간단해 보입니다.

첫째로 Vertex Shader:

 float4x4 g_mDPWorldView;  

 float g_fFar;
 float g_fNear;
 
 float g_fDir;			   // 반구의 방향


 // DPDepth
 struct VSDPDepthIn
 {
	 float3 Pos : POSITION;
 };

 struct PSDPDepthIn
 {
	 float4 Pos : POSITION;	
	 float ClipDepth : TEXCOORD1;	
	 float Depth : TEXCOORD2;		
 };


 // DPDepth-vertex-shader
 PSDPDepthIn DPDepthVS(VSDPDepthIn In)
 {
	 PSDPDepthIn Out;	
	
	 // Vertex를 DP-space로 변환
	 Out.Pos = mul(float4(In.Pos, 1.0f), g_mDPWorldView);
	 Out.Pos /= Out.Pos.w;
	
	 // 뒷면 map을 위해서 z를 반전시킨다.
	 Out.Pos.z *= g_fDir;	
	 	
	 // 원점은 0에서의 proj-vector이므로 vertex-position 위치와 일치한다.
	 float fLength = length(Out.Pos.xyz);
	 
	 // normalize
	 Out.Pos /= fLength;
	 
	 // clipping 을 위해 저장
 	 Out.ClipDepth = Out.Pos.z;	
 	 	
	 // 교차하는 "normal"을 계산한다. 반사벡터(0, 0, 1)을 더하고 
     // z 값으로 나눠주므로써 텍스쳐 좌표를 얻는다.
	 Out.Pos.x /= Out.Pos.z + 1.0f;
	 Out.Pos.y /= Out.Pos.z + 1.0f;
	
	 // z-buffering을 위해서 z값을 설정해주고 w를 상쇄시킨다.
	 Out.Pos.z = (fLength - g_fNear) / (g_fFar - g_fNear);
	 Out.Pos.w = 1.0f;
	
	 // DP-depth
	 Out.Depth = Out.Pos.z;
	
	 return Out;
 }

 

먼저 Vertices가 Paraboloid 공간으로 변환되어 지고 w로 나눠짐으로써 동차좌표계를 얻을 수 있습니다. g_fDir의 의미는 나중에 설명할 것입니다. 이제 Vertex는 Paraboloid에 투영 되어져야 합니다. 우리가 아는 것 처럼 Paraboloid 공간에서의 정규화된 Vertex 위치는 입사 투영 광선의 방향벡터와 일치합니다.

그러므로 우리는 지금 Vertex 위치를 정규화하고 Z-좌표를 픽셀 쉐이더를 위해 저장합니다.

이제 결정적인 부분: 우리는 반사벡터 (0/0/1)을 투영된 방향벡터로 계산되어진 벡터에 더합니다. 그리고 투영된 점에 Normal 과 일치하는 벡터를 얻습니다. 우리는 그것을 z-value로 나눠고 원하는 x, y 좌표를 얻습니다. 이 예에서는 2단계를 한단계로 처리했으니 혼란스러워 하지 마세요. 계산된 좌표계는 shadow map의 텍스쳐 좌표계와 일치합니다. 다음의 z-buffer test를 위해 우리는 vertex의 z-value 를 정확하게 계산해야 합니다. 우리는 이것을 Far와 Near clipping plane을 사용하여 0과 1 사이로 만들어서 처리합니다. 우리는 이 값 역시 픽셀쉐이더를 위해서 저장합니다.  w-value는 상쇄되어집니다.

g_fDir의 설명: 2개의 거의 동일한 쉐이더를 만드는 것을 피하기 위해서, 우리는 뒷면의 반구(hemisphere)를 위해서 간단히 Vertex의 z-좌표를 뒤집습니다. 이것을 가능하게 하기 위해서 후면 culling 역시 비활성화 되어야만 합니다. 왜냐하면 z-좌표를 반대로 하면, 삼각형의 Vertices 순서가 반대가 되기 때문입니다.

 

픽셀 쉐이더:

 // DPDepth-pixel-shader
 float4 DPDepthPS(PSDPDepthIn In) : COLOR
 {
	 // clipping
	 clip(In.ClipDepth);
			
	 return In.Depth;
 }

 

여기서, 각각의 반구의 클리핑 앞에 위치한 픽셀들만 제거되어야하고, 픽셀의 깊이는 Shadowmap 에 쓰여져야 한다.

이제 우리는 앞쪽 반구를 g_fDir=1 로 설정하여 렌더링하였다. 그리고 한번은 뒷면 반구를 위해서 g_fDir=-1로 렌더링하여 Shadow maps이 생성되어졌다. 결과는 원칙대로라면 아래와 같은 그림과 같아야만 한다:

 

 

 

Dual paraboloid shadow map

 

 

 

 

b.) 마지막 Scene 렌더랑:

먼저 Scene을 위한 렌더타겟과 effect를 위한 몇몇 파라메터가 설정되어야 한다. 즉 그들은:

  • 이전에 생성된 두개의 Shadow map (for PS)
  • 만약 필요하다면, 필요한 텍스쳐들 (for PS)
  • Scene을 위한 World 그리고 결되어진 ViewProjection Matrix (for VS)
  • Paraboloid ViewMatrix (for PS)
  • Light 위치의 월드 좌표계가 Lighting을 위해 요구된다. (for PS)
  • Light 와 Material colors
  • Shadow mapping을 할때 원하지 않는 결과를 피하기 위해서 필요한 SHADOW_EPSILON
  • 뿐만 아니라 필요로하는 samples
 #define SHADOW_EPSILON 0.0005f

 texture g_DPFrontMap;
 texture g_DPBackMap;

 texture g_Terrain;

 float4x4 g_mDPView;  

 float4x4 g_mWorld;
 float4x4 g_mViewProj;
	
 float3 g_vLightPos;        // worldspace

 float4 g_Material = float4(1.0f, 1.0f, 1.0f, 1.0f);
 float4 g_LightAmbientColor = float4(0.2f, 0.2f, 0.2f, 1.0f);
 float4 g_LightDiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);


 // DPDepth
 sampler2D DPFrontSampler = 
 sampler_state
 {
	 Texture = <g_DPFrontMap>;
	 MinFilter = Linear;
	 MagFilter = Linear;
	 MipFilter = Linear;
	 AddressU = Border;
	 AddressV = Border;
	 BorderColor = float4(0.0f, 0.0f, 0.0f, 1.0f);
 };

 sampler2D DPBackSampler = 
 sampler_state
 {
	 Texture = <g_DPBackMap>;
	 MinFilter = Linear;
	 MagFilter = Linear;
	 MipFilter = Linear;
	 AddressU = Border;
	 AddressV = Border;
	 BorderColor = float4(0.0f, 0.0f, 0.0f, 1.0f);
 };

 // DPSM
 sampler2D TerrainSampler = 
 sampler_state
 {
	 Texture = <g_Terrain>;
	 MinFilter = Linear;
	 MagFilter = Linear;
	 MipFilter = Linear;
	 AddressU = Wrap;
	 AddressV = Wrap;
 };

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

 // Vertex-Shader:


 // DPSM
 struct VSDPSMIn
 {
	 float3 Pos : POSITION;	
	 float2 TexCoord : TEXCOORD0;
	 float3 Normal : NORMAL;	
 };

 struct PSDPSMIn
 {
	 float4 Pos : POSITION;	
	 float3 PosWorld : TEXCOORD0;
	 float2 TexCoord : TEXCOORD1;
	 float3 Normal : TEXCOORD2;
	 float3 Light : TEXCOORD3;
 };

 // DPSM-vertex-shader
 PSDPSMIn DPSMVS(VSDPSMIn In)
 {
	 PSDPSMIn Out;
	
	 // world space
	 float3 vPosWorld = mul(float4(In.Pos, 1.0f), g_mWorld);
	
	 Out.Normal = normalize(mul(In.Normal, (float3x3)g_mWorld));
	 Out.Light = normalize(g_vLightPos - vPosWorld);	
	 Out.Pos = mul(float4(vPosWorld, 1.0f), g_mViewProj);		
	 Out.PosWorld = vPosWorld;
	 Out.TexCoord = In.TexCoord;
		
	 return Out;
 }

 

Vertex Shader에서는 Praboloid mapping 을 위한 구체적인 내용이 없습니다. 그래서 여기서 설명할 필요가 없습니다.

이제 픽셀 쉐이더:

// DPSM-pixel-shader
 float4 DPSMPS(PSDPSMIn In) : COLOR
 {
	 float3 Color;
	
	 // Depth-VS에 있는 텍스쳐좌표 계산과 동일한 계산
	 // but texcoords have to be in range [0, 1]
	
	 // Light space로 변환
	 float3 vPosDP = mul(float4(In.PosWorld, 1.0f), g_mDPView);	
	
	 float fLength = length(vPosDP);
	 // normalize
	 vPosDP /= fLength;
	
	 // Depth에 따라 계산하고 읽기
	 float fDPDepth;
	 float fSceneDepth;
	
	 if(vPosDP.z >= 0.0f)
	 {		
		 float2 vTexFront;
		 vTexFront.x =  (vPosDP.x /  (1.0f + vPosDP.z)) * 0.5f + 0.5f; 
		 vTexFront.y =  1.0f - ((vPosDP.y /  (1.0f + vPosDP.z)) * 0.5f + 0.5f); 	
		
		 fSceneDepth = (fLength - g_fNear) / (g_fFar - g_fNear);
	
		 fDPDepth = tex2D(DPFrontSampler, vTexFront).r;			
	 }
	 else
	 {
		 // 뒷면을 위해서 z값이 뒤집어진다.
		 float2 vTexBack;
		 vTexBack.x =  (vPosDP.x /  (1.0f - vPosDP.z)) * 0.5f + 0.5f; 
		 vTexBack.y =  1.0f - ((vPosDP.y /  (1.0f - vPosDP.z)) * 0.5f + 0.5f); 
		
		 fSceneDepth = (fLength - g_fNear) / (g_fFar - g_fNear);
		
		 fDPDepth = tex2D(DPBackSampler, vTexBack).r;
	 }
	
	 // lighting and shadowing	
	 float3 vNormal = normalize(In.Normal);
	 float3 vLight = normalize(In.Light);
		
	 if((fDPDepth + SHADOW_EPSILON) < fSceneDepth)
	 {
	 	 Color = tex2D(TerrainSampler, In.TexCoord) * g_LightAmbientColor;
	 }
	 else
	 {
		 Color = tex2D(TerrainSampler, In.TexCoord) * saturate(dot(vLight, vNormal)) *
		         g_LightDiffuseColor * g_Material + g_LightAmbientColor;
	 }
	
	 return float4(Color, 1.0f);
 }
			

아래에서 보여지는 픽셀 쉐이더가 아무런 인상을 주지 않더라도, 설명되어야 할 것이 아무것도 없습니다. 왜냐하면 텍스쳐 좌표계의 계산이 Shadow map을 만들 때 사용한 것과 같기 때문입니다. (2개의 반구가 제공되는 것과 Vertex 위치에 따라서 반구가 결정되는 것을 제외하면). 2개 Paraboloid 중 선택된 하나를 위한 좌표계가 계산되어지고 0과 1 사이값을 얻어냅니다. 그 결과 Shadow map 옳바르게 읽혀질 수 있습니다. 픽셀 쉐이더의 마지막 부분은 전통적인 Shadow mapping 기법: 픽셀의 깊이 값이 비교되어지는 것이다. 만약 Shadow map에 있는 값보다 더 큰값이라면(역자 : 카메라에서 더 멀리 있는 픽셀이라면) 그 픽셀은 Shadow 안에 있는 것이며, 그 픽셀은 텍스쳐 컬러와 ambient light 만을 얻게 된다. 그렇지 않다면 적절하게 빛을 받을 수 있습니다.

 

Effect를 완료 위해서, 쉐이더 컴파일:

 technique DPDepth
 {
	 pass p0
	 {
		 VertexShader = compile vs_2_0 DPDepthVS();
		 PixelShader = compile ps_2_0 DPDepthPS();
	 }
 }

 technique DPSM
 {
	 pass p0
	 {
		 VertexShader = compile vs_3_0 DPSMVS();
		 PixelShader = compile ps_3_0 DPSMPS();
	 }
 }

 

이제 Scene 렌더링만 수행되어지면 됩니다.

Dual paraboloid Shadow mapping을 결론 짖기 위해서 (Dual Parabolid environment mapping에 유효함):

위에서 언급한것 처럼, Parabolid map은 좋은 성능과 품질적으로 Spherical maps 보다 더 낫다. 그러나 Cubical mapping에 비해 단점이 있다면 Parabolid로 부터 연관된 왜곡이 있다. 한가지 나타날 수 있는 효과는 만약 당신이 Scene을 그릴때, Paraboloid space의 Vertex 위치 변환을 Vertex shader에서 이미 했다면 왜곡이 Rasterizer의 보간 때문에 발생할 것이다. 우리의 예제에서는 Pixel shader에서 위치를 변환하여 문제를 제거하였습니다. 또 다른 약간 처리하기 어려운 문제는 두개의 반구의 클리핑 영역에서 발생하는 왜곡 문제이며 그 사이로 Cracks이 보이게 된다. 그러나 이 효과는 Geometry의 해상도에 크게 의존됩니다. 만약 그것이 충분히 크다면, 문제는 더 적어질 것입니다. 당신은 이 문제를 이 평면의 적절한 설정으로 대응할 수 있습니다. 당신은 가능하다면 이 평면과 Geometry를 충돌시키기를 피해야 합니다.

약간의 결점에도 불구하고 우리는 지금까지 Parabolid map의 유용함을 봤습니다. 더욱더 빨라지고 있는 그래픽 카드 때문에 Geometry의 해상도는 더 높아지고 있기 때문에 미래에는 Paraboloid mapping에 문제되는 점이 없을 것입니다.

당신은 완벽한 DirectX effect file 예제를 여기(역주 : 2020.02.15 기준 지금은 링크가 안열리네요)서 다운로드 할수 있습니다.

 

반응형