link : https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
Improving shadow maps
우리는 쉐도우 매핑의 기본 원리를 알 수 있었지만, 알 수 있듯이 더 더 나은 결과를 위해 수정하고자 하는 그림자 맵핑과
관련된 몇 가지 아티팩트가 있다. 다음 섹션에서 살펴 보겠다.
Shadow ance
이전 이미지에서 잘못된 것이 분명하다. 더 가까이서 확대하면 아주 명확한 Moire 같은 패턴을 볼 수 있다:
우리는 교묘하게 검은 선으로 렌더링된 바닥 쿼드의 대부분을 볼 수 있습니다. 이 그림자 맵핑 아티팩트는 그림자 여드름이라고
하며 간단한 이미지로 설명 할 수 있다:
그림자 맵은 해상도에 의해 제한되기 때문에 여러 조각이 광원에서 상대적으로 멀리 떨어져있을 때 깊이 맵에서 동일한 값을
샘플링 할 수 있다. 이미지는 각 기울어진 패널이 깊이 맵의 단일 텍셀을 나타내는 바닥을 표시한다. 보시다시피 여러 단편은
동일한 깊이 샘플을 샘플링한다.
이것은 일반적으로 괜찮지만 광원이 표면을 향한 각도를 볼 때 깊이 맵은 각도에서 렌더링되기 때문에 문제가 된다.
그런 다음 여러 조각이 동일한 기울어진 깊이 텍셀에 액세스하는 반면 일부는 위아래이며 일부는 바닥 아래에 있다.
그림자의 불일치가 발생한다. 이 때문에 일부 조각은 그림자로 간주되고 일부 조각은 이미지에서 스트라이프 패턴을
제공하지 않는다.
우리는 그림자 바이어스라고 불리는 작은 해킹으로 이 문제를 해결할 수 있다. 여기서 작은 편견으로 표면의 깊이를 간단히
오프셋해 단편이 표면 아래에서 잘못 간주되지 않도록한다.
바이어스를 적용하면 모든 샘플의 깊이가 표면의 깊이보다 작아지므로 전체 표면이 그림자없이 올바르게 조면된다.
우리는 다음과 같은 편향을 구현할 수 있다:
float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
0.005의 그림자 기울기는 우리 장면의 문제를 상당부분 해결하지만 광원에 대한 가파를 각도를 갖는 표면은 여전히 그림자 여드름을 유발할 수 있다.
보다 견고한 접근법은 빛에 대한 표면 각을 기준으로 기울기의 양을 변경하는 것이다: dot product로 해결할 수 있는것:
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
여기서 우리는 표면의 법선 방향과 경 방향을 기준으로 0.05의 최대 기울기와 0.005의 최소 기울기를 갖는다. 이 방식으로 광원에 거의
수직인 바닥과 같은 면은 작은 편향을 얻는 반면 큐브의 측면과 같은 면은 훨씬 큰 편향을 얻는다. 다음 이미지는 동일한 장면이지만
이제는 그림자 편향을 보여준다. 훨씬 나은 결과이다:
올바른 기울기 값을 선택하려면 각 장면마다 약간의 조정이 필요하지만 대부분의 경우 모든 여드름이 제거될 때까지 기울기를 증가시키는 문제일 뿐이다.
Peter panning
그림자 기울기를 사용할 때의 단점은 객체의 실제 깊이에 오프셋을 적용한다는 것이다. 결과적으로 기울기는 아래에서 볼 수 있듯이 실제 오브젝트 위치와
비교해 보이는 그림자 오프셋을 볼만큼 충분히 커질 수 있다.
이 그림자 아티팩트는 피터 패닝(peter panning)이라고 한다. 왜냐하면 사물이 그림자에서 약간 떨어져 나온 것처럼 보이기 때문이다.
깊이 맵을 렌더링 할 때 앞면 컬링을 사용해 대부분의 피터 패닝 문제를 해결할 수 있다. OpenGL이 기본적으로 면을 뒤쫓는다고
face culling 튜토리얼에서 배웠다. 우리는 OpenGL에게 앞면을 도려내 달라고 하길 원한다. 우리는 순서를 바꿈으로써 해결한다.
우리는 깊이 맵에 깊이 값만 필요하기 때문에 솔리드 오브젝트의 경우 앞면 또는 뒷면의 깊이를 취하는 것이 중요하지 않다.
그들의 면 깊이를 사용할 때 객체 내부에 그림자가 있으면 문제가 되지 않으므로 잘못된 결과를 주지는 않는다:
대부분 피터 패닝을 고치기 위해 앞면을 cull한다. 먼저 GL_CULL_FACE를 활성화해야한다.
glCullFace (GL_FRONT);
RenderSceneToDepthMap();
glCullFace (GL_BACK); // don't forget to reset original culling face
이렇게 하면 피터 패닝 문제가 효과적으로 해결되지만 뚫려있지 않은 내부가 실제로 있는 단단한 물체에만 적용된다.
예를 들어, 우리의 장면에서 이것은 큐브에서 완벽하게 작동하지만 정면을 도려내면 방정식에서 바닥을 완전히 제거하므로
바닥에서 작동하지 않는다. 바닥은 하나의 평면이므로 완전히 추려질 것이다. 이 트릭으로 피터 패닝을 해결하려면
조심스럽게 개체의 앞면을 제거해야 한다.
또 다른 고려 사항은 멀리 떨어진 큐브와 같이 그림자 수신기에 가까운 물체가 여전히 잘못된 결과를 제공 할 수 있다는 것이다.
그것이 의미가 있는 사물에 앞면 컬링을 사용하는데 주의를 기울여야한다. 그러나 일반적인 기울기 값을 사용하면 일반적으로
피터 패닝을 피할 수 있다.
Over sampling
당신이 좋아하거나 싫어하는 또 다른 시각적 불일치는, 빛이 보이는 좌절 밖의 일부 지역이 그늘에 가려져 있지 않을 때 그늘에 가려져 있는
것처럼 보이는 것이다. 이것은 광원의 절두체 외부에 투영된 좌표가 1.0보다 높기 때문에 [0,1]의 기본값 범위를 벗어나는 깊이 텍스처를
샘플링하기 때문에 발생한다. 텍스처의 배치 방법에 따라 광원의 실제 깊이 값을 기반으로 하지 않고 잘못된 깊이 결과를 얻는다.
이미지에서 상상의 빛 영역이 있고, 이 영역 외부의 큰 부분이 그림자에 있음을 알 수 있다. 이 영역은 바닥에 투영된 깊이 맵의 크기를 나타낸다.
그 이유는 이전에 깊이 맵의 래핑 옵션을 GL_REPEAT으로 설정했기 때문이다.
우리는 차라리 깊이 맵의 범위 밖의 모든 좌표가 1.0의 깊이를 가지므로 이 좌표가 결코 그림자에 있지 않음을 의미한다.
경계 색을 저장하고 깊이 맵의 텍스처 랩 옵션을 GL_CLAMP_TO_BORDER로 설정하면 된다.
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float borderColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };
glTexParameter fv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
이제 깊이 맵의 [0,1] 좌표 범위 밖에서 샘플링 할 때마다 텍스처 함수는 항상 1.0의 깊이를 반환해 0.0의 그림자 값을 반환한다.
결과는 이제 훨씬 더 그럴듯 해 보인다:
아직 어두운 영역을 보여주는 부분이 있는 것 같다. 그것들은 빛의 직각 절두체의 멀리 떨어진 평면 밖의 좌표이다. 이 어두운 영역은 항상 그림자 방향을
보고 광원의 절두체의 맨 끝에서 발생한다.
투영된 좌표는 z 좌표가 1.0보다 클 때 빛의 먼 평면보다 더 크다. 이 경우 GL_CLAMP_TO_BORDER 랩핑 메소드는 좌표의 z 구성 요소와 깊이 맵 값을
비교할 때 더 이상 작동하지 않는다. 1.0보다 큰 z에 대해서는 항상 true를 반환한다.
투영된 벡터의 z 좌표가 1.0을 초과 할 때마다 그림자 값을 0.0으로 강제 설정하기 때문에 이 문제를 비교적 쉽게 해결할 수 있다:
float ShadowCalculation(vec4 fragPosLightSpace)
{
[...]
if(projCoords.z > 1.0)
shadow = 0.0;
return shadow;
}
먼 평면을 확인하고 깊이 맵을 수동으로 지정된 테두리 색상으로 고정하면 깊이 맵의 오버 샘플링이 해결되고 마지막으로 원하는 결과가 제공된다:
이 모든 결과는 투영된 부분 좌표가 깊이 맵 범위 내에 있는 그림자만 가질 수 있음을 의미한다. 따라서 이 범위를 벗어나는 부분은 그림자가 보이지 않는다.
게임은 일반적으로 이것이 멀리서만 발생한다는 것을 확인하기 때문에 이전에 우리가 가졌던 명백한 흑색 지역보다 훨씬 더 그럴듯한 결과이다.
PCF
그림자는 풍경에 대한 멋진 추가 사항이지만 여전히 우리가 원하는 것은 아니다. 그림자를 확대하려는 경우 그림자 맵핑의 해상도 종속성이
신속하게 드러난다.
깊이 맵은 고정된 해상도를 가지고 있기 때문에 깊이는 텍셀당 하나 이상의 조각에 자주 분포한다. 결과적으로 여러 조각이 깊이 맵에서 동일한 깊이 값을
샘플링하고 동일한 그림자 결과를 얻는다. 이렇게하면 이러한 들쑥날쑥한 가장자리가 생긴다.
깊이 맵 해상도를 높이거나 빛의 절두체를 장면에 최대한 가깝게 맞추어 이러한 짙은 그림자를 줄일 수 있다.
이러한 들쭉날쭉한 가장자리에 대한 또 다른 해결첵은 PCF 또는 percentage-closer filtering 이라 불리는데, 이는 더 부드러운 그림자를 생성하는
많은 다른 필터링 기능을 호스트해 덜 거칠거나 단단하게 보이게 한다. 아이디어는 약간 다른 텍스처 좌표로 매번 깊이 맵에서 한 번 이상 샘플링하는
것이다. 각각의 개별 샘플에 대해 그것이 그림자인지 여부를 확인한다. 모든 하위 결과가 결합되어 평균화되고 멋진 부드러운 음영을 얻는다.
PCF의 간단한 구현은 깊이 맵의 주변 텍셀을 샘플링하고 결과를 평균화하는 것이다:
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
여기서 textureSize는 밉맵 레벨 0에서 주어진 샘플러 텍스처의 너비와 높이의 vec2를 반환하다. 1 이상으로 나누면 텍스처 좌표를 오프셋하는데
사용하는 단일 텍셀의 크기를 반환해 각각의 새 샘플이 다른 깊이 값을 샘플링하는지 확인한다. 여기서 우리는 투영된 좌표의 x와 y값 주위의 9개 값을
샘플링하고, 그림자 폐색을 테스트한 다음 최종 결과를 총 샘플 수로 평균화한다.
더 많은 샘플을 사용하거나 texelSize 변수를 변경하면 부드러운 그림자의 품질을 높일 수 있다. 아래에 간단한 PCF를 적용해 그림자를 볼 수 있다:
멀리서 보면 그림자는 훨씬 좋아보이며 덜 힘들어 보인다. 확대하면 여전히 그림자 맵핑의 해상도 아티팩트를 볼 수 있지만 일반적으로
대부분의 응용 프로그램에서 좋은 결과를 얻을 수 있다.
실제로 PCF와 부드러운 그림자의 품질을 상당히 향상시키는 몇 가지 기술이 있지만 이 튜토리얼의 길이를 위해 나중에 논의 할 것이다.
Orthographic vs projection
정사영 또는 투영 행렬을 사용해 깊이 맵을 렌더링하는 경우에는 차이점이 있다. 직교 투영 행렬은 원근감을 가지고 장면을 변형시키지
않으므로 모든 뷰/광선이 평행해서 방향 조명을 위한 훌륭한 투영 행렬이 된다. 그러나 perspective projection 행렬은 다른 결과를
제공하는 원근감에 따라 모든 꼭지점을 변형한다. 다음 이미지는 두 가지 투영 방법의 서로 다른 shadow 영역을 보여준다:
원근 투영은 방향 조명과 달리 실제 위치가 있는 광원에 더 적합합니다. 원근 투영은 스포트라이트 및 점 광원에 가장 많이 사용되는 반면 직교 투영은
방향 조명에 사용된다.
투시 투영 행렬을 사용하는 또 다른 미묘한 차이점은 깊이 버퍼를 시각화하면 종종 거의 완전한 흰색 결과를 얻게 된다는 것이다. 이것은 투시 투영에서
깊이가 가까운 평면에 가까운 눈에 띄는 범위의 대부분을 갖는 비선형 깊이 값으로 변환되기 때문에 발생한다. 직교 투영에서 했던 것처럼 깊이 값을 제대로
보려면 깊이 테스트 튜토리얼에서 설명한대로 비선형 깊이 값을 선형으로 변환해야한다.
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D depthMap;
uniform float near_plane;
uniform float far_plane;
float LinearizeDepth(float depth)
{
float z = depth * 2.0 - 1.0; // Back to NDC
return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}
void main()
{
float depthValue = texture(depthMap, TexCoords).r;
FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective
// FragColor = vec4(vec3(depthValue), 1.0); // orthographic
}
이것은 직교 투영에서 본 것과 비슷한 깊이 값을 보여준다. 이것은 디버깅에만 유용하다. 상대적 깊이가 변하지 않으므로 깊이 검사는 정사영 또는
투영 행렬과 동일하게 유지된다.
'Game > Graphics' 카테고리의 다른 글
Learn OpenGL - Advanced Lighting : Point Shadows (2) (0) | 2018.10.01 |
---|---|
Learn OpenGL - Advanced Lighting : Point Shadows (1) (0) | 2018.09.27 |
Learn OpenGL - Advanced Lighting : Shadow Mapping (2) (0) | 2018.09.20 |
Learn OpenGL - Advanced Lighting : Shadow Mapping (1) (0) | 2018.09.19 |
Learn OpenGL - Advanced Lighting : Gamma Correction (0) | 2018.09.17 |