Texture Array를 이용한 Cascaded
Shadow Maps
유영천
Microsoft Visual C++ MVP
tw:@dgtman
http://megayuchi.wordpress.com
• 현 시점에서 가장 대중적으로 사용되고 있는 그림자 렌더링 기법
• 구현하기 쉽다.
• Soft-shadow처리도 쉽다.
Shadow Maps
• 광원(그림자를 드리울)의 뷰공간에서 Shadow Caster가 될
오브젝트들(혹은 월드 전체)을 렌더링 -> Depth Buffer(Texture)를
구성
구현 – Pass 0 , Shadow Caster
구현 – Pass 1 , Shadow Receiver
• 광원(그림자를 드리운)의 View- Space로 변환.
• -1 ~ 1사이의 좌표공간을 0 – 1사이로 변환.
• 변환된 (0 – 1사이) 좌표로 Depth Buffer로부터 depth값을 샘플링
• Receiver의 depth값과 Depth Buffer의 depth값을 비교
• receiver_z > depth_value -> 그림자가 드리워짐
Shadow Map in Shadow Space On Rendering in Camera Space
Shadow Map applied
① ②
③
Shader Resource View
Sampling z-value from
Shadow Map
Compare z-value ① and ②
per pixel.
• 한 장의 Texture로 넓은 공간을 표현하기엔 해상도가 부족함.
• Texture 사이즈를 크게? -> 품질향상, But 성능 하락, GPU메모리
낭비
• PSM, TSM, LiPSM 등등 한 장의 Texture만을 사용하면서 최대한
낭비 없이 사용하는 기법들 등장
그냥 쓰기엔 약간 문제가…
• Shadow Map을 만들 때 한 장의 Texture가 아닌 여러 장의 Texture에
나눠 그리자.
• 뷰프러스텀 영역을 여러 개의 공간으로 잘라서 여러 장의 Shadow
Map Texture를 할당.
• 가까운 영역과 먼 영역에 따라 Shadow Map 정밀도 조절 가능.
• Shadow Map 해상도 증가에 따른 낭비가 적다.
Cascaded Shadow Maps
eye
Shadow Light
Shadow Map Texture – Size(Width x N , Height)
⓪ ① ② ③
⓪ ① ② ③
⓪ ① ② ③
• Shadow Map으로 사용할 Texture 준비
• N개의 Cascaded 단계를 사용한다면 Texture 사이즈는 Width x N, Height
• Width x N인 이유는 한 장의 Texture에 N단계의 Shadow Map을 담기
위함이다.
• 루프를 돌며 0 ~ N-1단계까지 Shadow Caster를 렌더링.
• 뷰포트 설정을 바꿔가며 한 장의 Texture에 모두 담는다.
• Shadow Receiver를 렌더링할때 픽셀이 어느 Cascaded단계에
들어가는지 찾아서 tex좌표의 u성분 offset조정
• 이후는 일반적인 Shadow Map과 똑같음.
구현
• Cascaded단계에 따라 Draw Call회수가 늘어남. CPU 자원 낭비.
• 코드 복잡해짐.
• 한방에 모든 Cascaded 단계를 처리할 수 없을까?
개선하고 싶은 점
• 말 그대로 Texture배열
• 배열이지만 단일 Texture처럼 다룰 수 있다.
• SRV, RTV로 사용 가능.
• CPU측 코드에서 API사용 방법은 일반 Texture와 똑같다.
• Texture로부터 샘플링시 좌표의 x,y,z성분중 z성분을 배열의
인덱스로 사용
Texture Array
Texture2DArray texDiffuseArray: register(t0);
SamplerState samplerDiffuse: register(s0);
float4 psArrayDiffuse(PS_INPUT input) : SV_Target
{
float3 texCoord = float3(input.TexCoord.xy,TexArrayIndex);
float4 texColor = texDiffuseArray.Sample(samplerDiffuse, texCoord);
float4 outColor = texColor;
return outColor;
}
Using Texture Array as SRV
Using Texture Array as RTV
struct PS_OUT_TEX_ARRAY
{
float4 Pos : SV_POSITION;
uint RTIndex : SV_RenderTargetArrayIndex;
};
[maxvertexcount(3)]
void gsDefault ( triangle GS_INPUT input[3], inout TriangleStream<PS_OUT_TEX_ARRAY> TriStream )
{
PS_OUT_TEX_ARRAY output;
for (uint i=0; i<3; i++)
{
output.Pos = mul(input[i].PosWorld,matViewProjList[i]);
output.RTIndex = N;
TriStream.Append(output[j]);
}
TriStream.RestartStrip();
}
• N개의 Cascaded단계가 있을 때 사이즈 Width , Height에 배열이
N개인 Texture 생성
• 일반 Texture와 마찬가지로 RTV와 SRV생성
• Shader안에서 어느 Cascaded 단계에 포함되는지 계산하고 그
인덱스 값을 텍스쳐 좌표의 z성분으로 사용
Texture Arrary를 Shadow Maps에 적용
UINT Width = DEFAULT_SHADOW_MAP_WIDTH;
UINT Height = DEFAULT_SHADOW_MAP_HEIGHT;
UINT ArrayCount = MAX_CASCADE_NUM;
D3D11_TEXTURE2D_DESC texDesc =
{
Width, Height, 1, ArrayCount, DXGI_FORMAT_R32_TYPELESS, 1, 0, D3D11_USAGE_DEFAULT, D3D11_BIND_DEPTH_STENCIL |
D3D11_BIND_SHADER_RESOURCE, 0, 0
};
D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc = { DXGI_FORMAT_D32_FLOAT, D3D11_DSV_DIMENSION_TEXTURE2DARRAY, 0 };
dsvDesc.Texture2DArray.FirstArraySlice = 0;
dsvDesc.Texture2DArray.ArraySize = ArrayCount;
dsvDesc.Texture2DArray.MipSlice = 0;
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { DXGI_FORMAT_R32_FLOAT, D3D11_SRV_DIMENSION_TEXTURE2DARRAY, 0, 0 };
srvDesc.Texture2DArray.FirstArraySlice = 0;
srvDesc.Texture2DArray.ArraySize = ArrayCount;
srvDesc.Texture2DArray.MipLevels = 1;
srvDesc.Texture2DArray.MostDetailedMip = 0;
ID3D11Texture2D* pTex = nullptr;
HRESULT hr = m_pD3DDevice->CreateTexture2D(&texDesc, NULL, &pTex);
if (FAILED(hr))
__debugbreak();
hr = m_pD3DDevice->CreateDepthStencilView(pTex, &dsvDesc, &m_pShadowMapDSV);
if (FAILED(hr))
__debugbreak();
hr = m_pD3DDevice->CreateShaderResourceView(pTex, &srvDesc, &m_pShadowMapSRV);
if (FAILED(hr))
__debugbreak();
pTex->Release();
Creating SRV,DSV from Texture Array
Texture Texture Array
// set matrix for shadow-space,
SetCascadedLightSpaceAll(N);
// Draw shadow casters to Depth Buffer
DrawShadowCasters();
for (DWORD i=0; i<N; i++)
{
// set matrix for shadow-space,
SetCascadedLightSpace(i);
// Draw shadow casters to Depth Buffer
DrawShadowCasters();
}
CPU Code – Shadow Caster 렌더링 비교
cbuffer ConstantBufferShadowMap : register( b0 )
{
matrix matWorld;
matrix matViewList[MAX_CASCADE_NUM];
matrix matProjList[MAX_CASCADE_NUM];
}
struct PS_OUT_TEX_ARRAY
{
float4 Pos : SV_POSITION;
uint RTIndex : SV_RenderTargetArrayIndex;
};
struct GS_INPUT
{
float4 PosWorld: POSITION;
};
Shader Code – Pass 0 , Shadow Caster
float4 vsShadowCaster( VS_INPUT_VL input ) : POSITION
{
float4 PosWorld = mul( input.Pos, matWorld );
return PosWorld;
}
[maxvertexcount(3*MAX_CASCADE_NUM)]
void gsShadowCaster( triangle GS_INPUT input[3], inout TriangleStream<PS_OUT_TEX_ARRAY> TriStream )
{
PS_OUT_TEX_ARRAY output[3];
for (uint i=0; i<MAX_CASCADE_NUM; i++ )
{
for (uint j=0; j<3; j++)
{
float4 PosView = mul(input[j].PosWorld,matViewList[i]);
PosView.z += 2.5f; // bias value
output[j].Pos = mul(PosView,matProjList[i]);
output[j].RTIndex = i;
TriStream.Append(output[j]);
}
TriStream.RestartStrip();
}
}
Shader Code – Pass 0 , Shadow Caster
cbuffer ConstantBufferGBufferShader : register (b0)
{
matrix ViewInv;
matrix matShadowViewProjCascade[MAX_CASCADE_NUM];
CASCADE_CONSTNAT CascadeConst[MAX_CASCADE_NUM];
};
Texture2DArray texShadowMap: register(t2);
SamplerComparisonState samplerComp : register(s2);
void CalcIndex(out float OutIndex, in float Dist)
{
uint index = MAX_CASCADE_NUM - 1;
for (uint i = 0; i < MAX_CASCADE_NUM; i++)
{
if (Dist <= CascadeConst[i].Dist)
{
index = i;
break;
}
}
OutIndex = index;
}
Shader Code – Pass 1 , Shadow Receiver
float3 CalcShadowColor3x3(Texture2DArray texShadowMap, SamplerComparisonState samplerComp, float4 PosWorld, float Dist)
{
float3 shadowColor = float3(1, 1, 1);
uint index;
CalcIndex(index, Dist);
float4 PosShadowSpace = mul(PosWorld, matShadowViewProjCascade[index]);
float4 texCoord = PosShadowSpace / PosShadowSpace.w;
float cmp_z = texCoord.z;
float litSum = 0;
int2 offset[9] = {-1,-1, 0,-1, 1,-1, -1,0, 0,0, 1,0, -1,1, 0,1, 1,1 };
for (int i = 0; i < 9; i++)
{
litSum += texShadowMap.SampleCmpLevelZero(samplerComp, float3(texCoord.xy,index), cmp_z, offset[i]);
}
float shadowValue = litSum / 9.0f;
shadowColor = lerp(float3(0,0,0), float3(1, 1, 1), shadowValue);
return shadowColor;
}
Shader Code – Pass 1 , Shadow Receiver
• Draw Call을 대폭 줄일 수 있다. CPU측 병목을 줄임
• 코드가 보다 간결해짐.
Texture Array사용의 장점
• 각각의 Light View Space 프러스텀에 대한 culling
• 프러스텀에 대한 culling을 먼저 수행한 후 Constant Buffer를 통해 bit
flags로 전달.
• Geometry Shader에서 0,1,2,3… 각 비트를 체크하며 0 – N까지의
Cascaded단계에 포함되는지를 검사. 비트가 0이면 그대로 폐기.
• GPU상에서 클리핑이 이루어지므로 CPU측에서 별도의 Culling작업에
필요한 시간을 감안하면 굳이 필요가 없을지도????
추가적인 최적화
Reference
• https://msdn.microsoft.com/en-
us/library/windows/desktop/ee416307(v=vs.85).aspx
• http://developer.download.nvidia.com/SDK/10.5/opengl/src/cascade
d_shadow_maps/doc/cascaded_shadow_maps.pdf

Implements Cascaded Shadow Maps with using Texture Array

  • 1.
    Texture Array를 이용한Cascaded Shadow Maps 유영천 Microsoft Visual C++ MVP tw:@dgtman http://megayuchi.wordpress.com
  • 2.
    • 현 시점에서가장 대중적으로 사용되고 있는 그림자 렌더링 기법 • 구현하기 쉽다. • Soft-shadow처리도 쉽다. Shadow Maps
  • 3.
    • 광원(그림자를 드리울)의뷰공간에서 Shadow Caster가 될 오브젝트들(혹은 월드 전체)을 렌더링 -> Depth Buffer(Texture)를 구성 구현 – Pass 0 , Shadow Caster
  • 4.
    구현 – Pass1 , Shadow Receiver • 광원(그림자를 드리운)의 View- Space로 변환. • -1 ~ 1사이의 좌표공간을 0 – 1사이로 변환. • 변환된 (0 – 1사이) 좌표로 Depth Buffer로부터 depth값을 샘플링 • Receiver의 depth값과 Depth Buffer의 depth값을 비교 • receiver_z > depth_value -> 그림자가 드리워짐
  • 5.
    Shadow Map inShadow Space On Rendering in Camera Space Shadow Map applied ① ② ③ Shader Resource View Sampling z-value from Shadow Map Compare z-value ① and ② per pixel.
  • 6.
    • 한 장의Texture로 넓은 공간을 표현하기엔 해상도가 부족함. • Texture 사이즈를 크게? -> 품질향상, But 성능 하락, GPU메모리 낭비 • PSM, TSM, LiPSM 등등 한 장의 Texture만을 사용하면서 최대한 낭비 없이 사용하는 기법들 등장 그냥 쓰기엔 약간 문제가…
  • 7.
    • Shadow Map을만들 때 한 장의 Texture가 아닌 여러 장의 Texture에 나눠 그리자. • 뷰프러스텀 영역을 여러 개의 공간으로 잘라서 여러 장의 Shadow Map Texture를 할당. • 가까운 영역과 먼 영역에 따라 Shadow Map 정밀도 조절 가능. • Shadow Map 해상도 증가에 따른 낭비가 적다. Cascaded Shadow Maps
  • 8.
    eye Shadow Light Shadow MapTexture – Size(Width x N , Height) ⓪ ① ② ③ ⓪ ① ② ③ ⓪ ① ② ③
  • 9.
    • Shadow Map으로사용할 Texture 준비 • N개의 Cascaded 단계를 사용한다면 Texture 사이즈는 Width x N, Height • Width x N인 이유는 한 장의 Texture에 N단계의 Shadow Map을 담기 위함이다. • 루프를 돌며 0 ~ N-1단계까지 Shadow Caster를 렌더링. • 뷰포트 설정을 바꿔가며 한 장의 Texture에 모두 담는다. • Shadow Receiver를 렌더링할때 픽셀이 어느 Cascaded단계에 들어가는지 찾아서 tex좌표의 u성분 offset조정 • 이후는 일반적인 Shadow Map과 똑같음. 구현
  • 10.
    • Cascaded단계에 따라Draw Call회수가 늘어남. CPU 자원 낭비. • 코드 복잡해짐. • 한방에 모든 Cascaded 단계를 처리할 수 없을까? 개선하고 싶은 점
  • 11.
    • 말 그대로Texture배열 • 배열이지만 단일 Texture처럼 다룰 수 있다. • SRV, RTV로 사용 가능. • CPU측 코드에서 API사용 방법은 일반 Texture와 똑같다. • Texture로부터 샘플링시 좌표의 x,y,z성분중 z성분을 배열의 인덱스로 사용 Texture Array
  • 12.
    Texture2DArray texDiffuseArray: register(t0); SamplerStatesamplerDiffuse: register(s0); float4 psArrayDiffuse(PS_INPUT input) : SV_Target { float3 texCoord = float3(input.TexCoord.xy,TexArrayIndex); float4 texColor = texDiffuseArray.Sample(samplerDiffuse, texCoord); float4 outColor = texColor; return outColor; } Using Texture Array as SRV
  • 13.
    Using Texture Arrayas RTV struct PS_OUT_TEX_ARRAY { float4 Pos : SV_POSITION; uint RTIndex : SV_RenderTargetArrayIndex; }; [maxvertexcount(3)] void gsDefault ( triangle GS_INPUT input[3], inout TriangleStream<PS_OUT_TEX_ARRAY> TriStream ) { PS_OUT_TEX_ARRAY output; for (uint i=0; i<3; i++) { output.Pos = mul(input[i].PosWorld,matViewProjList[i]); output.RTIndex = N; TriStream.Append(output[j]); } TriStream.RestartStrip(); }
  • 14.
    • N개의 Cascaded단계가있을 때 사이즈 Width , Height에 배열이 N개인 Texture 생성 • 일반 Texture와 마찬가지로 RTV와 SRV생성 • Shader안에서 어느 Cascaded 단계에 포함되는지 계산하고 그 인덱스 값을 텍스쳐 좌표의 z성분으로 사용 Texture Arrary를 Shadow Maps에 적용
  • 15.
    UINT Width =DEFAULT_SHADOW_MAP_WIDTH; UINT Height = DEFAULT_SHADOW_MAP_HEIGHT; UINT ArrayCount = MAX_CASCADE_NUM; D3D11_TEXTURE2D_DESC texDesc = { Width, Height, 1, ArrayCount, DXGI_FORMAT_R32_TYPELESS, 1, 0, D3D11_USAGE_DEFAULT, D3D11_BIND_DEPTH_STENCIL | D3D11_BIND_SHADER_RESOURCE, 0, 0 }; D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc = { DXGI_FORMAT_D32_FLOAT, D3D11_DSV_DIMENSION_TEXTURE2DARRAY, 0 }; dsvDesc.Texture2DArray.FirstArraySlice = 0; dsvDesc.Texture2DArray.ArraySize = ArrayCount; dsvDesc.Texture2DArray.MipSlice = 0; D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { DXGI_FORMAT_R32_FLOAT, D3D11_SRV_DIMENSION_TEXTURE2DARRAY, 0, 0 }; srvDesc.Texture2DArray.FirstArraySlice = 0; srvDesc.Texture2DArray.ArraySize = ArrayCount; srvDesc.Texture2DArray.MipLevels = 1; srvDesc.Texture2DArray.MostDetailedMip = 0; ID3D11Texture2D* pTex = nullptr; HRESULT hr = m_pD3DDevice->CreateTexture2D(&texDesc, NULL, &pTex); if (FAILED(hr)) __debugbreak(); hr = m_pD3DDevice->CreateDepthStencilView(pTex, &dsvDesc, &m_pShadowMapDSV); if (FAILED(hr)) __debugbreak(); hr = m_pD3DDevice->CreateShaderResourceView(pTex, &srvDesc, &m_pShadowMapSRV); if (FAILED(hr)) __debugbreak(); pTex->Release(); Creating SRV,DSV from Texture Array
  • 16.
    Texture Texture Array //set matrix for shadow-space, SetCascadedLightSpaceAll(N); // Draw shadow casters to Depth Buffer DrawShadowCasters(); for (DWORD i=0; i<N; i++) { // set matrix for shadow-space, SetCascadedLightSpace(i); // Draw shadow casters to Depth Buffer DrawShadowCasters(); } CPU Code – Shadow Caster 렌더링 비교
  • 17.
    cbuffer ConstantBufferShadowMap :register( b0 ) { matrix matWorld; matrix matViewList[MAX_CASCADE_NUM]; matrix matProjList[MAX_CASCADE_NUM]; } struct PS_OUT_TEX_ARRAY { float4 Pos : SV_POSITION; uint RTIndex : SV_RenderTargetArrayIndex; }; struct GS_INPUT { float4 PosWorld: POSITION; }; Shader Code – Pass 0 , Shadow Caster
  • 18.
    float4 vsShadowCaster( VS_INPUT_VLinput ) : POSITION { float4 PosWorld = mul( input.Pos, matWorld ); return PosWorld; } [maxvertexcount(3*MAX_CASCADE_NUM)] void gsShadowCaster( triangle GS_INPUT input[3], inout TriangleStream<PS_OUT_TEX_ARRAY> TriStream ) { PS_OUT_TEX_ARRAY output[3]; for (uint i=0; i<MAX_CASCADE_NUM; i++ ) { for (uint j=0; j<3; j++) { float4 PosView = mul(input[j].PosWorld,matViewList[i]); PosView.z += 2.5f; // bias value output[j].Pos = mul(PosView,matProjList[i]); output[j].RTIndex = i; TriStream.Append(output[j]); } TriStream.RestartStrip(); } } Shader Code – Pass 0 , Shadow Caster
  • 19.
    cbuffer ConstantBufferGBufferShader :register (b0) { matrix ViewInv; matrix matShadowViewProjCascade[MAX_CASCADE_NUM]; CASCADE_CONSTNAT CascadeConst[MAX_CASCADE_NUM]; }; Texture2DArray texShadowMap: register(t2); SamplerComparisonState samplerComp : register(s2); void CalcIndex(out float OutIndex, in float Dist) { uint index = MAX_CASCADE_NUM - 1; for (uint i = 0; i < MAX_CASCADE_NUM; i++) { if (Dist <= CascadeConst[i].Dist) { index = i; break; } } OutIndex = index; } Shader Code – Pass 1 , Shadow Receiver
  • 20.
    float3 CalcShadowColor3x3(Texture2DArray texShadowMap,SamplerComparisonState samplerComp, float4 PosWorld, float Dist) { float3 shadowColor = float3(1, 1, 1); uint index; CalcIndex(index, Dist); float4 PosShadowSpace = mul(PosWorld, matShadowViewProjCascade[index]); float4 texCoord = PosShadowSpace / PosShadowSpace.w; float cmp_z = texCoord.z; float litSum = 0; int2 offset[9] = {-1,-1, 0,-1, 1,-1, -1,0, 0,0, 1,0, -1,1, 0,1, 1,1 }; for (int i = 0; i < 9; i++) { litSum += texShadowMap.SampleCmpLevelZero(samplerComp, float3(texCoord.xy,index), cmp_z, offset[i]); } float shadowValue = litSum / 9.0f; shadowColor = lerp(float3(0,0,0), float3(1, 1, 1), shadowValue); return shadowColor; } Shader Code – Pass 1 , Shadow Receiver
  • 21.
    • Draw Call을대폭 줄일 수 있다. CPU측 병목을 줄임 • 코드가 보다 간결해짐. Texture Array사용의 장점
  • 22.
    • 각각의 LightView Space 프러스텀에 대한 culling • 프러스텀에 대한 culling을 먼저 수행한 후 Constant Buffer를 통해 bit flags로 전달. • Geometry Shader에서 0,1,2,3… 각 비트를 체크하며 0 – N까지의 Cascaded단계에 포함되는지를 검사. 비트가 0이면 그대로 폐기. • GPU상에서 클리핑이 이루어지므로 CPU측에서 별도의 Culling작업에 필요한 시간을 감안하면 굳이 필요가 없을지도???? 추가적인 최적화
  • 23.