참고
https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
Shadow Mapping
기본적으로, 그림자는 occlusion
으로 인한 빛의 부재로 인해 그림자가 발생하게 된다.
![[빛의 시점에서 렌더링.png]]
실시간 렌더링 환경에서 그림자를 그리는 아이디어는 빛의 시점에서 렌더링을 하는 것이다. 이러한 방법으로 수천 개의 빛 광선을 그리는 것은 극도로 비효율적인 접근이다. 실시간 렌더링에서는 부적합한 방법이다.
그래서 빛 광선에서의 시점을 그대로 그리는 대신에 유사한 방법으로 depth buffer를 이용할 것이다. 이것을 shadow map
이라고도 부른다. 빛의 시점에서 depth buffer를 그리는 것이다.
depth value를 이용해서 가장 가까운 점을 찾고 그림자가 생길 물체를 결정한다. 정확하게 어떻게 그림자가 생기는 물체인지 결정하는 것일까?
Shadow mapping은 두 순서로 이루어져 있다.
- depth map을 렌더링한다. (using DSV in d3d11)
- 만들어진 depth map을 이용하여 그림자가 생기는지 안 생기는지를 파악한다.
조명 시점의 depth map을 그려야 하는데 어떻게 해야할까? 현재 우리는 카메라 시점에 대해 정의를 해서 사용하고 있다. 이에 따라서 각각 조명마다 카메라 처럼 설정을 해줄 필요가 있다.
카메라 클래스의 벡터는 pos
, viewDir
, upDir
, rightDir
로 구성되어 있다. 우리는 이것을 조명을 이용해서 찾아내야 한다. upDir
의 경우에는 (0, 1, 0)으로 고정해도 무방할 것이다. 만약 viewDir과 upDir의 각도가 0도나 180도라면 곤란하므로, dot 연산을 통해서 이 부분을 예외처리를 해준다.
Vector3 up = Vector3(0.0f, 1.0f, 0.0f);
// 만약 빛의 방향과 upDir의 dot 연산이 -1에 가깝다면, 그것은 둘의
// 사이각이 180도에 가깝다는 이야기로, upDir의 방향을
// 수정해줘야한다.
if (abs(up.Dot(light.direction) + 1.0f) < 1e-5)
up = Vector3(1.0f, 0.0f, 0.0f);
// https://learn.microsoft.com/ko-kr/windows/win32/api/directxmath/nf-directxmath-xmmatrixperspectivefovlh
Matrix lightProjRow = XMMatrixPerspectiveFovLH(
XMConvertToRadians(120.0f), 1.0f, 0.01f, 25.0f);
// https://learn.microsoft.com/ko-kr/windows/win32/api/directxmath/nf-directxmath-xmmatrixlookatlh
Vector3 targetVec = (light.position + light.direction);
targetVec.Normalize();
Matrix lightViewRow =
XMMatrixLookAtLH(light.position, targetVec, up);
이렇게 그린 깊이 버퍼를 이용해서 그림자를 판정하는 방법은 조명의 시점값(view
, proj
)들을 이용해야 한다. 해당 값들을 이용해서 깊이버퍼의 depth 값과 PS를 거치고 있는 조명 시점
에서의 물체 depth 값을 서로 비교한다.
예를 들어, 그림자가 생겨야 할 위치라고 가정하자. 해당 부분을 PS에서 그릴 때 깊이 버퍼의 조명 시점에서의 depth값을 서로 비교할 것이다. 깊이 버퍼는 그림자가 생겨야 할 부분이므로 깊이 버퍼의 값은 해당 부분을 가리고 있는 물체의 depth 값을 가지고 있을 것이다.
하지만, 조명 시점에서의 depth값은 물체가 그림자를 만드는 물체가 아닌, 그림자가 생길 물체의 depth값이 이다. 그러므로 조명 시점의 depth값은 더 작을 것(더 멀리 있으니까)이고 그 부분은 그림자가 생긴다고 판단하는 것이다.
float ShadowCalculation(Light light, float3 posWorld, float3 normalWorld, Texture2D shadowMap)
{
// light의 inputTexcoord를 찾아야한다.
// 빛에서 볼 때를 기준으로 값을 정한다.
// 1. Light Proj -> Screen(Clip)
float4 lightScreen = mul(float4(posWorld, 1.0), light.viewProj);
lightScreen /= lightScreen.w; // Clip -> NDC
// 2. [-1, 1] -> [0, 1]
lightScreen.y *= -1;
lightScreen = (lightScreen + 1.0) * 0.5;
float closestDepth = shadowMap.Sample(shadowPointSampler, lightScreen.xy).r;
float currentDepth = lightScreen.z;
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
return shadow;
}
왜 그림자가 잘리는 현상이 발생할까?
현재 원리는 빛에서 인식하는 물체 depth값과 물체 자체의 depth값의 비교를 통해서 그림자가 생기는지를 체크한다.
DSV를 통해서 가져오는 물체 depth 값은 정사각형의 모양처럼 생겼다. 왜냐하면, 우리가 DSV를 만들 때 Teuxture2D
버퍼와 연결을 하는데 이때, Texture2D를 초기화할 때 정사각형의 사이즈로 설정해주기 때문이다.
그렇기 때문에, 깊이 버퍼가 그리는 범위를 넘어서게 되면 그림자가 짤리는 것처럼 표현되는 것이다. 이러한 문제를 해결하기 위해서는 시각적인 방법으로는 해당 그림자는 Directional Light 또는 Spot Light의 불빛이므로 일정 공간의 범위를 설정해서 이러한 느낌이 나지 않도록 하는 것이다.
시각적인 방법으로 해결하기
불빛은 기본적으로 원의 형태로 띄워서 주변을 밝힌다. 그렇다면, 그림자에도 이러한 특성을 부여해서 원밖에 점점 다가갈 수록 그림자가 사라지는 방식으로 연출하면 되지 않을까 생각했다. 적용 후가 확실히 자연스러운 것을 확인할 수 있었다.
초록색 : 적용 전
분홍색 : 적용 후
(1.0 - shadow) * intensity가 아닌 이유는 그렇게 적용한다면, 이미 그림자 처리가 끝난 뒤로 결과가 달라지지 않기 때문이었다.// for Spot Light float theta = dot(normalize(-lights[i].direction), lightVec); float epsilon = (cos(lights[i].fallOffStart) - cos(lights[i].fallOffEnd)); float intensity = clamp((theta - cos(lights[i].fallOffEnd)) / epsilon, 0.0, 1.0);
float3 color = (diffuse + specular) * (1.0 - shadow * intensity); if(light.type & LIGHT_SPOT) color *= intensity;
만들면서 발생한 문제점
1. 그림자가 생각보다 더 멀리 나오는 현상
그림자가 이상하게 나와서, 이것저것 찾아보다가, 그림자가 이상하게 나오던 현상은 내가 shadow mapping을 할 depth buffer를 만들 때, buffer의 width와 height를 기존 렌더링할 때의 크기로 했기 때문에 위치가 이상하게 잡혀 있었기 때문이다. 즉 정사각형이 아닌 직사각형의 depth buffer를 가지고 렌더링을 진행하였다가 발생한 문제이다. 이는 간단히 정사각형 비율로 설정해주면 바로 해결되었다.
2. 직교 투영 시 물체의 색깔이 이상하게 나옴
설정한 1대1 비율의 넓이와 높이로 맞춰주면 렌더링이 잘 되는 것을 확인할 수 있었다. 첫사진은 원근 투영을 적용한 시점이고 두번째 사진은 직교 투영을 적용한 시점이다.
직교 투영을 적용 시 물체의 색깔이 그림자 색깔이 덮히는 현상이 발생하는데, 이 현상에 대해서는 왜 그런지 정확히 파악하지 못했다.
-> 혹시나, 깊이 버퍼의 해상도(정사각형 사이즈)를 크게하면 달라지지 않을까 해서 해상도를 키워봤는데 해당 문제가 완화되는 것을 볼 수 있었다. 그림자도 더 부드럽게 보인다.
PCF
계단현상이 나타나는 이유는 depth가 texel당 하나 이상 fragment를 가지고 있기 때문에 생기는 현상이다. 참고로 texel이란 텍스처에서의 화소이다. 결과적으로, 여러 fragment가 같은 depth값을 샘플링함으로써 계단 현상이 발생하는 것이다.
이 방법을 줄이는 기술 중 하나로 PCF(percentage-closer filtering)이 있다. 아이디어는 depth map으로부터 depth값을 한번만 받는 것이 아닌 여러 번 받는 것이다. 각각 텍스쳐 좌표를 조금씩 다르게 조정해서 말이다.
float ShadowCalculation(Light light, float3 posWorld, float3 normalWorld, Texture2D shadowMap)
{
float shadow = 0.0;
if (light.type & LIGHT_SHADOW)
{
// light의 inputTexcoord를 찾아야한다.
// 빛에서 볼 때를 기준으로 값을 정한다.
// 1. Light Proj -> Screen(Clip)
float4 lightScreen = mul(float4(posWorld, 1.0), light.viewProj);
lightScreen /= lightScreen.w; // Clip -> NDC
lightScreen.y *= -1; // texcoord는 y축 방향이 다르므로
// 2. [-1, 1] -> [0, 1]
lightScreen *= 0.5;
lightScreen += 0.5;
float2 lightTex = float2(lightScreen.x, lightScreen.y);
float2 texel;
shadowMap.GetDimensions(texel.x, texel.y);
float2 texelSize = 1.0 / texel;
for (int x = -1; x <= 1; ++x)
{
for (int y = -1; y <= 1; ++y)
{
float closestDepth = shadowMap.Sample(shadowPointSampler, lightTex.xy + float2(x, y) * texelSize).r;
float currentDepth = lightScreen.z;
float bias = 0.005;
shadow += currentDepth - bias > closestDepth ? 1.0 : 0.0;
}
}
}
shadow /= 9.0;
return shadow;
}
해당 사진은 PCF 적용 시의 모습이다. 위의 사진보다 확실히 그림자의 계단현상이 완화된 것을 볼 수 있었다.
'Graphics' 카테고리의 다른 글
[D3D11] 물체를 그리는 과정 (1) | 2024.06.24 |
---|---|
4. Point Shadow Mapping (1) | 2024.06.24 |
2. Depth Buffer와 안개 효과 (1) | 2024.06.14 |
[D3D11] 사각형 및 육면체 그리기 (0) | 2024.05.20 |
1. Blinn Phong 모델 (0) | 2024.05.14 |