posts - 0, comments - 1, trackbacks - 0, articles - 25
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

Cubic Shadow Mapping in Direct3D

Posted on 2009-10-13 14:03 cngamedev 阅读(431) 评论(0)  编辑 收藏 引用 所属分类: DirectX

GPU들이 더 빠르게 성장함에 따라, GPU에 친숙한 알고리즘들은 더욱 대중적인 것이 되었습니다. 따라서 다른 쉐도우 기법들과 비교하여, 쉐도우 맵핑은 아마도 그림자들을 생성하기 위해 가장 광범위하게 사용되는 기법일 것입니다. 이 글은 전방향성 라이트들 (Omni-directional lights)을 위한 쉐도우 맵핑에 대한 구현의 기본적인 것들을 살펴볼 것이고 최적화와 기법의 향상을 위한 몇몇 이론들을 제공할 것입니다. 다른 접근법들과 방법들이 존재하기 때문에, 저는 최적화의 자세한 부분들을 처리하도록 시도하지 않을 것입니다. 또한, 독자가 C++과 Direct3D의 기본적인 내용과 기본적인 쉐도우 맵핑 기법들에 친숙하다고 가정합니다.

The Algorithm

전방향성 라이트들을 위한 쉐도우 맵핑과 일반 쉐도우 맵핑의 비교가 가능하도록, 저는 스팟 라이트들 (spot lights)을 위한 기본적인 쉐도우 맵핑 알고리즘을 설명할 것이어서, 당신은 이 방법들을 비교할 수 있습니다. 스팟 라이트들을 사용하여 그림자를 드리우도록 하는 것은 두가지 단계들로 구성됩니다 :

  1. 스팟 라이트의 위치에 카메라를 위치시키고 스팟 라이트의 시점에서 단일 구성요소 텍스쳐로의 장면 깊이를 렌더링합니다 (실수형이 보다 바람직합니다).
  2. 투영 텍스쳐를 입히도록 깊이 비교를 위한 깊이 텍스쳐 (쉐도우 맵)의 결과를 사용합니다.

전방향성 라이트들을 위해 그림자를 입히는 것 또한 두가지 단계들로 구성되지만, 스팟 라이트 쉐도우 맵핑 알고리즘에 몇몇 단순한 변형들이 적용되어져야 합니다 :

  1. 전방향 라이트의 위치에 카메라를 위치시키고 큐브맵의 6 면들에 깊이값을 저장하도록 6번 장면의 깊이를 렌더링합니다. 매번마다 카메라의 시야 벡터는 다음의 방향들 중 하나로 향하도록 해야 합니다 : 양의 X, 음의 X, 양의 Y, 음의 Y, 양의 Z, 음의 Z. 이것은 색상 대신 깊이값을 저장하는 것을 제외하고는 환경 맵핑을 위한 큐부맵을 생성하는 것과 거의 동일합니다.
  2. 환경 맵을 입히도록 깊이 비교를 위한 큐브 텍스쳐의 결과 (cubic shadow map)을 사용합니다.

여기서 볼 수 있듯이, 앞서 언급한 알고리즘들에는 두가지 차이점이 있습니다 : 우선 첫째로, 우리는 6 단계로 쉐도우 맵을 생성해야만 합니다. 두번째로, 우리는 두번째 단계의 투영 텍스쳐링 대신 환경 맵핑을 사용합니다. 이제 기본 알고리즘에 대해 간략하게 이해를 하게 되었으므로, 구현으로 넘어가 더 자세한 것을 얻도록 하겠습니다.

Implementation

저는 세단계로 구현을 나눌 것입니다 :

  • 초기화
  • 큐빅 쉐도우 맵에 장면 깊이의 렌더링
  • 큐빅 쉐도우 맵을 사용하여 장면을 렌더링

Step 1 : Initialization

초기화 부분은 약간 단순합니다. 이 부분에서는 다섯개의 작업들이 있지만 저는 두번째와 세번째를 다룰 것인데, 다른 것들은 이 글의 범위에 포함되지 않기 때문입니다 (첨부된 소스 코드는 모든 부분들을 다루고 있습니다) :

  • Direct3D 초기화
  • 큐빅 쉐도우 맵으로 사용되는 큐브 텍스쳐의 생성과 이 텍스쳐의 모든 여섯 표면을 얻어옴
  • 가상 카메라의 생성 (장면의 깊이를 렌더링하기 위해 라이트의 위치에 설치되어지도록)
  • 이펙트들의 로드
  • 메쉬들의 로드

다음의 코드 조각은 큐빅 쉐도우 맵을 생성하기 위해 사용됩니다 :

m_pd3dDevice->CreateCubeTexture(m_iCubeFaceSize, // Cube face edge length
1, // mip levels
D3DUSAGE_RENDERTARGET,
D3DFMT_R32F,  // could be D3DFMT_R16F
D3DPOOL_DEFAULT,
&m_pCubicShadowTex,
NULL);

다음으로 하는 것은 우리의 큐빅 렌더 타겟의 모든 6개의 표면을 얻는 것입니다. 이것은 각 면에 렌더링 하기 위한 SetRenderTarget() 함수를 사용하게 되기 때문에 필요합니다. 큐브 맵의 양의 X 면을 위해 다음의 C++ 코드가 사용될 것입니다 :

cubicShadowMap->GetCubeMapSurface(D3DCUBEMAP_FACE_POSITIVE_X, 0, &depthCubeFacePX);

다른 면들을 위해 우리가 받아오기를 원하는 큐브 면에 따라 처음 인자를 변경하여 큐브면과 연관되어 있는 Direct3D 표면을 함수에 넘기게 됩니다. (그래서 우리는 큐브 텍스쳐와 우리의 장면에서 사용하는 각 라이트를 위한 6개의 표면들이 필요하게 될 것입니다.)

가상 카메라의 초기화는 평범합니다; 단지 주의해야 할 점은 90도로 시야 (Field Of View)로의 투영 행렬 (projection matrix)을 생성하고 1.0f로 시야 비율 (aspect ratio)을 초기화 해야 한다는 것입니다. 다음의 코드는 90의 FOV와 1.0의 화면 비율로 투영 행렬을 생성하기 위해 D3DXMatrixPerspectiveFovLH를 사용하는 것입니다.

D3DXMatrixPerspectiveFovLH(&m_ProjMat, D3DX_PI / 2.0f, 1.0f, 1.0f, 500.0f);

Step 2 : Rendering to the cubic shadow map

큐빅 쉐도우 맵에 장면의 깊이를 렌더링하기 위해, 우리는 이전 단계에서 설명된 가상 카메라를 사용할 것입니다. 이 카메라의 방향은 매 단계에서, 각 단계에서 카메라의 뷰 벡터를 변경하고 그에따라 뷰 행렬이 갱신되어져야만 한다는 것을 의미하는 양의 X, 음의 X, 양의 Y, 기타 등등을 바라보도록 변경될 것입니다.

fig1.jpg
그림 1 : 큐빅 쉐도우 맵에 장면의 깊이를 렌더링하기 위해 6개의 방향들에서 라이트의 카메라.

따라서, 처음 단계를 위해 우리는 다음과 같이 해야 합니다 :

  1. 카메라를 양의 X 축을 바라보도록 설정합니다.
  2. 초기화 단계에서 얻은 적절한 큐브 면에 렌더 타겟을 설정하고 삭제합니다.
  3. 장면 깊이를 렌더링 합니다 (스팟 라이트 쉐도우 맵핑에서 깊이를 렌더링하는 것과 동일합니다).

두번째 단계를 위해 다음과 같이 해야 합니다 :

  1. 카메라를 양의 Y 축을 바라보도록 설정합니다.
  2. 초기화 단계에서 얻은 적절한 큐브 면에 렌더 타겟을 설정하고 삭제합니다.
  3. 장면 깊이를 렌더링 합니다 (스팟 라이트 쉐도우 맵핑에서 깊이를 렌더링하는 것과 동일합니다).

계속 합니다.

큐빅 쉐도우 맵 면에 장면 깊이를 렌더링하는 것은 일반적인 쉐도우 맵핑과 동일합니다. 우리는 카메라를 사용하고 우리의 타겟이 2D 소수점 텍스쳐입니다. 여기에 이 부분을 위한 작업을 하게 될 버텍스 쉐이더가 있습니다 :

VS_OUTPUT_DEPTH DepthMap_VS(float4 inPosition : POSITION)
{
VS_OUTPUT_DEPTH output;
 
float4 positionW = mul(inPosition, worldMat);
output.oPositionLight = mul(inPosition, worldViewProjMat);
 
output.lightVec = lightPosition - positionW.xyz;
 
return output;
}

픽셀 쉐이더는 단지 HLSL의 명령 함수를 사용하여 라이트 벡터의 길이를 계산할 것이고, 출력물들은 파이프라인의 결과입니다.

다음의 C++ 코드는 구현의 두번째 단계를 위한 작업을 하게 될 것입니다 :

// enable red channel for color write
m_pd3dDevice->SetrenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_RED);
 
m_pShadowEffect->m_pEffect->SetTechnique(m_pShadowEffect->m_DepthMapHandle);
m_pShadowEffect->m_pEffect->Begin(&numOfpasses, NULL);
 
// render the scene depth to positive X side of the cube map
createCamForPositiveX();  // a helper function for setting up the light's camera looking toward positive X axis
renderDepthToCubeFace(depthCubeFacePX);
 
// render the scene depth to positive Y side of the cube map
createCamForPositiveY();
renderDepthToCubeFace(depthCubeFacePY);
 
// render the scene depth to positive Z side of the cube map
createCamForPositiveZ();
renderDepthToCubeFace(depthCubeFacePZ);
 
// render the scene depth to negative X side of the cube map
createCamForNegativeX();
renderDepthToCubeFace(depthCubeFaceNX);
 
// render the scene depth to negative Y side of the cube map
createCamForNegativeY();
renderDepthToCubeFace(depthCubeFaceNY);
 
// render the scene depth to negative Z side of the cube map
createCamForNegativeZ();
renderDepthToCubeFace(depthCubeFaceNZ);
 
m_pShadowEffect->m_pEffect->End();
 
// enable color writes
m_pd3dDevice->SetRenderState(D3DRS_COLORWRITEENABLE,
D3DCOLORWRITEENABLE_ALPHA |
D3DCOLORWRITEENABLE_RED |
D3DCOLORWRITEENABLE_GREEN |
D3DCOLORWRITEENABLE_BLUE);

renderDepthToCubeFace(…)와 createCamFor…() 함수들에 대한 코드들 입니다 :

void CCubicShadowMapping::renderDepthToCubeFace(LPDIRECT3DSURFACE9 inCubeSurface)
{
D3DXMATRIXA16 worldViewProjMat;
 
// set and clear the cube map face surface
if (SUCCEEDED(m_pd3dDevice->SetRenderTarget(0, inCubeFaceSurface)))
{
m_pd3dDevice->Clear(NULL, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, NULL);
}
 
// render all geometries of the scene (assuming that there is no scene management or frustum culling algorithm)
}
 
void CCubicShadowMapping::createCamFor***()
{
m_pLightCamera->setLook(/*m_PositiveLookX for example*/);
m_pLightCamera->setUp(/*m_PositiveLookY for example*/);
m_pLightCamera->setRight(/*m_NegativeLookZ for example*/);
 
// update the camera's concatenated view-projection matrix with new look, up and right vectors
m_pLightCamera->updateViewProjMat();
}

우리가 R32F 텍스쳐 포맷을 가지고 단지 레드 채널만이 사용되기 때문에, 거의 대부분 본질 적으로우리의 큐브 텍스쳐에 장면 깊이를 렌더링 할 때 레드 채널은 있지만 색상들을 쓰기는 비활성화 됩니다. 왜냐하면 큐빅 쉐도우 맵들은 큰 텍스쳐들이어서, 우리는 이 기법을 사용할 때 필-레이트 문제들을 고려해야 합니다. 예를들어, 512 픽셀들의 경계 크기인 큐빅 쉐도우는 비디오 메모리에서 6 (면) * 262,144 (픽셀들) * 32 bit = 6144KB를 차지 합니다. 이것이 기법의 처음 부분을 구현하는데에 여러 최적화 기법들이 존재하는 가에 대한 이유입니다 (큐빅 쉐도우 맵의 색성). 당신은 이러한 퍼포먼스 향상 기법들에 친숙하도록 “최적화” 부분을 참조할 수 있습니다.

Step 3 : Rendering the Scene using cubic shadow map

큐빅 쉐도우 맵을 가지게 된다면, 이제 우리의 큐브 맵내의 대응하는 픽셀들과 각 픽셀의 깊이 비교를 하게 될 마지막 단계를 구현할 시간이 됐습니다. 현재 픽셀의 깊이가 큐빅 쉐도우 맵에서 샘플링된 깊이 보다 크다면, 픽셀은 그림자가 드리워 진것이고, 아니라면 빛을 받게 되는 것입니다. 알고리즘에서 언급했듯이, 투영 텍스쳐에 사용되는 것이 샘플링된 2D 텍스쳐라는 것 대신 샘플링한 큐브 맵이라는 것만이 차이점입니다.

큐브 텍스쳐의 샘플링은 세가지 구성 벡터가 요구됩니다. 이것을 위해, 우리는 라이트의 위치에서 현재 픽셀을 가리키는 곳에서 시작되는 라이트의 방향 벡터의 역을 사용합니다. 우리가 큐브 맵에 각 픽셀의 깊이를 렌더링하는 곳에서, 깊이 요소에 따라 라이트 벡터의 길이가 사용되는 것을 기억하십시요. 그래서 여기서 우리는 깊이 비교를 하기 위해 샘필링하는 우리의 큐브 맵과 현재 픽셀의 깊이를 위한 동일한 것을 하게 될 것입니다. 다음의 HLSL 코드는 장면의 그림자와 라이팅을 계산하게 되는 이펙트에서 추출된 함수입니다 :

lightFuncOutput LightPositionSH(float3 inObjPos, float3 inNormal, float3 inCam2Vertex)
{
lightFuncOutput output;
output.diffuseResult = float4(0.0f, 0.0f, 0.0f, 1.0f);
output.specularResult = float4(0.0f, 0.0f, 0.0f, 1.0f);
 
float4 PLightDirection = 0.0f;
PLightDirection.xyz = lightPosition.xyz - inObPos;    // inObjPos is the pixel's position in world space
float distance = length(PLightDirection.xyz);    // the depth of current pixel
PLightDirection.xyz = PLightDirection.xyz / distance;
 
// compute attenuation factor
PLightDirection.w = max(0, 1 / (lightAttenuation.x +
lightAttenuation.y * distance +
lightAttenuation.z * distance * distance);
 
// sample the cubic shadow map using the inverse of light direction
float shadowMapDepth = texCUBE(cubeShadpwMapSampler, float(-(PLightDirection.xyz), 0.0f)).x;
 
// do the depth comparison
if (distance > shadowMapDepth)
{
return output;    // the pixel is in shadow so only the ambient light is visible to eye
}
else
{
// the pixel is not in shadow so the phong lighting is applied
float3 floatVecTmp = normalize(inCam2Vertex + PLightDirection.xyz);
 
output.diffuseResult = PLightDirection.w * lightDiffuse * max(0, dot(inNormal, PLightDirection.xyz));
output.specularResult = PLightDirection.w * lightSpecular * pow(max, dot(inNormal, floatVecTmp)), specPower);
 
return output;
}
}

그리고 최종적으로 이 단계의 버텍스와 픽셀 쉐이더는 다음과 같습니다 :

VS_OUTPUT cubicShadowMapping_VS(float4 inPosition : POSITION, float3 inNormal : NORMAL)
{
VS_OUTPUT output;
 
float4 positionW = mul(inPosition, worldMat);
 
output.cam2Vert = (eyePosition - PositionW).xyz;
output.position = mul(inPosition, worldViewProjMat);
output.worldPos = positionW.xyz;
output.normalW = mul(inNormal, worldMat).xyz;
 
return output;
}
 
float4 cubicShadowMapping_PS(VS_OUTPUT In) : COLOR 0
{
lightFuncOutput lightResult;
 
float3 normal = nromalize(In.NormalW);
float3 cam2Vert = normalize(In.cam2Vert);
 
lightResult = LightPointSH(In.worldPos, normal, cam2Vert);
 
float4 ambient = materialAmbient * globalAmbient;
float4 diffuse = materialDiffuse * lightResult.diffuseResult;
float4 specular = materialSpecular * lightResult.specularResult;
 
float4 lighting Color = (ambient + (diffuse + specular));
 
return lightingColor;
}
cubic_shadowing_fig2.jpg
그림 2 : 전방향성 라이트에서의 최종 결과.

Optimization

이 글에서 표현된 기법은 전방향성 라이트들을 위한 가장 기본적인 쉐도우 맵핑 기법입니다. 기본 기법을 더욱 빠르게 실행하고 정확한 결과들을 더을 수 있도록 도와주는 여러 최적화와 품질 향상 기법들이 존재합니다. 이 부분은 이러한 기법들의 간략한 이해를 돕도록 할 것이지만 자세한 구현은 하지 않는데, 이렇게 하게 되면 이 글이 쉐도우 맵핑에 대한 책으로 만들어질 정도가 될 것이기 때문입니다! 그러기 때문에, 여기에 당신이 연구해 볼 수 있는 몇몇 기법들만을 소개하도록 하겠습니다 :

  • 처음 주목해 볼 것은 프러스텀 컬링입니다. 우리의 깊이 큐브 맵을 채우기 위해 6번 장면을 그리는 것을 기억하십시요. 따라서, 프러스텀 컬링의 적용은 그리기 호출을 많이 줄이는데 도움이 될 것입니다.
  • 두번째 것은 가능한 많이 처음 단계의 렌더링 패스들을 감소시키도록 하는 것입니다; 다른 말로 하면, 큐빅 쉐도우 맵의 면들을 렌더링 하지 않는 것입니다. 깊이 렌더링 단계는 6개의 카메라들을 필요로 하지만, 예를 들어, 이 카메라들의 원뿔의 일부분이 우리의 메인 카메라의 프러스텀 내부에 위치하지 않거나 오직 세가지 의 원뿔의 일부분만이 보일 수 있습니다. 이 기법은 구현하기 쉽고 렌더링 속도를 향상하는데 큰 효과가 있습니다.
  • 세번째 것은 그림자를 드리우는 오브젝트들의 컬링입니다. 이것을 위해, 우리는 라이트의 위치에 기반한 폭이 좁은 측면으로 라이트와 그림자를 드리우는 오브젝트 모두 감싸는 가상의 원뿔을 생성해야만 합니다. 그리고 나서 우리는 이 원뿔에서 프러스텀 컬링을 실행하고 그림자를 드리우는 오브젝트가 보이는지 아닌지를 결정합니다. 만일 왜 우리가 프러스텀에 대응하는 간단한 컬링 캐스터들 대신 원뿔을 사용하는지 궁금해 한다면, 이것은 시야에서 그림자가 튀는 현상을 막을 수 있기 때문입니다.
  • 네번째는 라이트에 의해 영향을 받는 화면의 영역을 표시하는 오려지는 사각형을 정의하고 라이트에 영향을 받지 않는 픽셀들을 버리는 하드웨어의 영역 오리기 검사를 사용하는 것입니다. 우리의 장면에 위치한 각 전방향성 라이트는 제한된 길이를 가지고 이 범위를 넘는 픽셀들의 처리는 소용이 없기 때문에, 이 기법 또한 구현하기 쉽고 굉장한 혹도 향상을 이룰 수 있습니다.
  • 다섯번째는 nVidia GeForce3 이상에서 가능하게된 하드웨어 쉐도우 맵핑을 사용하는 것입니다. 하드웨어 쉐도우 맵핑을 사용하는 것은 더 적은 메모리 대역폭 소비, 색상 버퍼 쓰기가 없고 오직 깊이 쓰기만에 대한 하드웨어 가속과 같은 여러 장점이 있습니다. 일반적인 쉐도우 맵핑을 위해 하드웨어 쉐도우 맵핑을 사용하는 것은 평범하지만 우리의 깊이 맵을 위해 큐브 텍스쳐를 사용하기 때문에, 우리는 전방향상 쉐도우 맵핑을 위해 이 기법을 직접적으로 구현 할 수 없습니다. 쉐도우 깊이 텍스쳐들이 (D24, D16) 큐브텍스쳐를 지원하지 않기 때문이지만, 큐빅 쉐도우 맵핑으로 하드웨어 쉐도우 맵핑을 사용할 수 없다는 것을 뜻하지는 않습니다. 해결 방법은 큰 깊이 텍스쳐 내에 큐브 맵의 모든 6면들을 통합하고 이 텍스쳐에서의 샘플 텍섿르에 특별한 어드레싱 기법을 사용하는 것입니다. 다른 말로 하면, 우리는 3방향의 규브 텍스쳐 좌표 벡터를 “VSDT” 또는 “Virtual Shadow Depth Cube Texture”라고 불리는 이 텍스쳐의 샘플링을 위해 2방향의 구성요소로 변경하는 것으로 큐브 맵을 이 텍스쳐로 처리하는 것입니다.

The Source code

이 글의 소스 코드에 대해 언급해야하는 몇가지 주의 사항들이 있습니다 :

  • 소스 코드는 nVidia PerfHUD에 준비되어 있습니다. 그래서 (만일 당신이 프로그램과 호환되는 비디오 카드를 가지고 있다면) 파이프 라인을 탐색해 보는 것과 실시간으로 알고리즘의 시각화를 보는데 문제가 없습니다. 또한 알고리즘의 속도가 집중되는 부분들을 찾을 수 있고 아마도 새로운 아이디어를 생각해 낼 수 도 있을 것입니다.
  • 소스 코드는 최적화되지 않아서 (C++과 HLSL 코드 모두) 당신은 이전에 기술된 최적화 기법들을 위한 코드를 추가 해 볼 수 있습니다.
cubic_shadowing_fig3.jpg
그림 3 : nVidia PerfHUD로 실행된 예제 어플리케이션.

References

  1. Gerasimov and Philipp. Omnidirectional Shadow Maps. In GPU Gems, Addison-Wesley. Pages 193-203, 2004.
  2. G king and W Newhall. Efficient Omnidirectional Shadow Maps. In ShaderX3: Advanced Rendering with DirectX and OpenGL, Charles River Media. Pages 435-448, 2004.

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理