(사실 이해했는지 모르겠다. 어렴풋이 느껴질 뿐... 그래도 그 느낌을 정리한다.)
1. Shadow Mapping
- 아이디어의 시작은 그림자를 지게 만드는 광원에서 뭔가를 하는 것이다.
- 광원에서 빛이 나와서 opaque한 물체에 닿으면, 그 뒤로 그림자가 생기는 것이다.
- Shadow Mapping이 이 원리를 써먹을 알고리듬이다.
- 광원에서 빛의 방향으로 바라본 장면을 렌더링 하고, 이것의 뎁스 버퍼를 이용하는 것이다. 그러면, 광원과 빛이 처음 닿는 픽셀과 거리가 담긴 텍스쳐를 얻게 된다.
- 이를 그림자를 지게할 곳과 거리를 비교해서, 해당 평면에 그림자가 드리울지, 아닐지, 혹은 얼마나 드리울지 결정하는 것이다.
2. Projective Texturing
- 예제에서는 Directional Light를 사용하기 때문에 orthographic projection을 사용한다.
- orthgraphic projection을 이용한 렌더링을 위해 Frustum이 아닌 직육면체의 viewport를 형성하도록 속성값을 정의한다.
- nearZ와 farZ의 크기가 같고, 각 모델들은 축과 평행하게 투영되어 렌더링 하게 된다.
- 이를 이용해서 평행한 directional light의 빛이 물체에 닿는 것을 표현하였다.
- 이 렌더링 과정에서 depth value를 기록하게 되는 것이다.( D3D12_RESOURCE_STATE_DEPTH_WRITE)
3. 쉐이더에서 하는 일
- Projective Texturing을 통해 만든 shadow map의 값을 sampling 해서 사용하는 것은 좌표계 변환을 이용해서 렌더링 할 픽셀에 알맞은 shadow map Texcoords를 얻게 된다.
- World space -> View space (광원 좌표계) -> Projection space -> Texture Coordinates으로 변환한다.
- 위처럼, App에서 넘겨준 변환 행렬을 이용해서 'Shadow Map sampling을 한 값'과 ' 카메라에서 해당 픽셀까지의 거리(depth)를 비교하여 그림자 판별을 하는 것이다.
4. App에서 해줘야 하는 것
- 전체 화면 렌더링을 2번 걸어준다. 1) 그림자 맵 생성 2) 본 카메라 렌더링
- 그림자 맵 생성에서는 현재 광원 정보(위치, 빛 방향 등)를 이용하여 orthogonal projection을 하도록 하는 Matrix를 Shader로 넘겨주어서, depth 정보가 담긴 Texture를 생성한다.
- 해당 Texture를 Srv로 넘겨주고, 그림자 계산을 하는 Shader로 렌더링을 한다.
- 예제에서는 광원이 계속 회전하도록 하였다. (Shadow Map은 Draw()에 같이 걸리니까 상관없지만.) Shader에서 사용하는 Texture Coordinates를 얻게 하는 행렬(리소스)을 틱마다 업데이트를 해서 넘겨주어야 한다.
5. 해상도 차이로 생기는, Aliasing 문제를 해결하는 방법
- 아무래도, 본 카메라의 해상도 보다 그림자의 해상도는 낮게 설정이 되어 있을 것이다.
- 그리고 Shadow Map에서 샘플링해서 가져오는 값은 Shadow Map 해상도를 가진 NDC으로 (정확히는 텍스쳐 좌표) 변환되어, 1대 1 대응이 아닌, 그에 가장 가까운 값을 중복해서 가져오는 현상이 발생한다.
- 이러한 이유로 안 예쁜 그림자가 나오는 shadow acne가 발생하게 된다. 교재에서는 2가지 해결 방법을 알려준다.
5.1 Slope-Scaled-Bias
- App에서 넘겨준 값으로 계산한 광원과 픽셀과의 거리(depth), 그리고 shadowmap의 값과 비교를 해서 그림자가 지는 것을 결정한다고 했는데, 거의 똑같은 위치인데도 불구하고, 이 점은 광원과의 거리가 더 멀고, 그 바로 옆에 점은 거리가 더 가까워서 그림자가 줄무늬나 점박이처럼 생기는 현상을 shadow acne (그림자 여드름?)
- 렌더링 시에 계산하는 광원과의 거리를 아주 약간 짧게 해주는 bias를 넣어준다. 그림자가 지는 곳의 광원과 가까워지도록 거리(depth)를 살짝 띄워줘서 부족한 shadow map 해상도여도 줄무늬 현상이 생기지 않게 하는 것이다.
- 그렇다고 너무 띄우면 그림자와 모델이 분리가 되어버리고 (peter panning)
- 너무 작으면 광원과 모델 삼각형과 이루는 기울기가 큰 친구들에게는 통하지 않는다. (기울기가 크면 본 렌더링에서 계산하는 광원과 삼각형 간의 거리의 차이가 심해진다.)
- 그래서 광원과 모델 삼각형의 기울기에 맞춰서 띄워줘야 한다. (slope-scaled-bias)
- 하드웨어에서 이러한 기능을 제공하고, D3D에서도 해당 속성을 정의할 수 있도록 구현해 주었다.
- Depth Bias - Win32 apps | Microsoft Learn
- 위에서 언급했듯, 예제에서도 마찬가지고 광원과 모델 삼각형들 간의 기울기를 계산하는 것이기 때문에, 본 카메라 렌더링에서 계산하는 광원과의 거리에 bias를 더해주는 것이 아니라, shadow map를 렌더링 할 때 bias를 더해줘야 한다.
- 그래서 shadow map을 그리는 PSO에 RasterizerState 값을 설정해주어야 한다.
5.2 PCF (Percentage Closer filtering)
- 이 친구도 그림자 맵과 본 카메라 렌더링의 해상도 차이로 인해 생기는 그림자 판정 문제를 해결하기 위한 방법이다.
- 그림자 한가운데에 있는 친구들은 여러 개의 픽셀을 봐도 그림자가 지는 판정을 하게 되는데
- 그림자 가생이에 있는 친구들은 깊이 판정을 했을 때, 고해상도의 모델을 따라서 예쁜 그림자가 지지 않고 계단식으로 그림자가 생기게 된다.
- 이를 해결하기 위해 본 카메라 어느 한 픽셀에 대응하는 쉐도우 맵 픽셀만 가져오는 것이 아니라, 그 주변 픽셀에 해당하는 쉐도우 맵 픽셀을 가져와서 적절히 보간한 값을 shadow 값으로 사용하는 것이다. (percentage closer filtering)
(4x4 일 수도 있고, 9x9일 수도 있다.)
6. 언급할 만한 예제에서 사용한 방법(구조)
- 위와 같은 방법으로 보간 되어서 [0, 1]의 범위를 가진 값으로, 해당 픽셀에 지는 그림자의 농도를 결정하였다. (shadow factor)
- Shadow Map을 생성하고, 관리하기 편하도록 클래스 인스턴스를 사용하였다.
7. 연습 문제
(클릭하면 커집니다.)
연습 문제 0
- 문제를 제대로 안 읽고 구현한 결과물이다. (영사기를 왜 카메라로 생각했을까)
- 2번째 광원을 따라가는 카메라를 하나 더 만들어서 렌더링 한 결과를 grid에 텍스쳐로 입혔다.
- passCB (Srv + RenderTarget), Depth-Stencil Buffer 하나씩 더 만들고, 리소스를 참조하는 View와 Shader에 넘겨줄 속성 값들도 정의했다.
- 클래스를 하나 만들어서 새로운 랜더 타겟을 관리했고, 속성값이나 view는 멤버 값으로 관리한다.
연습 문제 1
- Projective Texture Mapping를 연습하는 것이다.
- 이 친구를 투영시켜 보았다.
- .... 크기의 차이가 있긴 하다만, 어떤 특색을 가지고 있는지 잘 모르겠다.
- 생성하는 frustum 속성이나, projection 하는 방향에 따라 투영상이 달라지는데... 잘 모르겠다.
연습 문제 2
- D3D12_TEXTURE_ADDRESS_MODE를 이용해서, Texture Projection에서 veiw frustum / view cube를 벗어나면 그로부터 빛을 받을 수 없게 하라는데... 그건 이미
연습문제 1번인 것 같아서, 그냥 조명 느낌쓰로 Texture Projection를 벗어나면 조명을 아예 받지 못하도록 만들었다.
연습 문제 3
- 연습 문제 2번은 directional light로, 연습문제 3번은 point light로 하라는 건데...
- 그냥 projection을 perspective 하게 하기로 했다.
- 쉐이더 코드
연습 문제 4
- 광원은 Directional light인데, projection은 perspective로 연습해 보는 것이다.
- 기둥 그림자를 보면, 평행하지 않고 살짝 벌어지는 것을 볼 수 있다.
- 그리고 frustum이 모든 모델을 다 포함하지 못해서, shadow map에서 그려지지 않은 부분이 있다.
(아래는 비교군이다.)
연습 문제 5
- ShadowMap 해상도를 변화하면서 결과를 비교하는 연습이다.
- 확실히 비쌀수록 예쁘다.
연습 문제 6
- 직교 투영 상자(절두체)의 중앙이 시야의 중간을 지나지 않을 때, 행렬식을 구하는 연습 문제이다.
- 그니깐 좀 더 일반적인 orthogonal projection matrix를 구하는 느낌이다.
- 적당히 그 값이 나오도록 사상시켜 보자.
- x는 [left, right]에서 [-1, 1]로 가야 하니깐 1열을 먼저 생각하면,
- right와 left의 중간 점을 (right + left) / 2 빼면 일단, [-w/2, w/2]로 바뀌게 된다. (w = right - left)
(x에 right를 넣는다면 right(x) - (right + left) /2 = (right - left) / 2 이 되고, left를 넣는다면 (left - right) / 2가 되니까 맞다.)
- 그리고 다시 2/w로 나누게 된다면? [-1, 1]으로 바뀐다. (2/w 은 2 / (right - left) 이므로 맞다.)
> x' = ( 2 * x - (right + left)) / (right - left)
- 위 사상을 만족하는 열을 만들면 된다.
- y는 [top, bottom]인데, x와 똑같이 하면 된다.
- z는... 중앙 정렬된 직교투영과 똑같다.
- w는 1을 넣어야 벡터의 성분이 살아남는다.
연습 문제 7
- chap17에서 perpective projection을 이용하는 화면에서 Picking 하는 것을, off - centered orthogonal projection 일 때, 어떻게 해야 할지 생각해 보라는 연습 문제이다.
- 첫 번째 진행하는 Ray Direction을 구하는 공식을 지나면 과정은 똑같을 테니, 중앙을 벗어난 직교 투영일 때 Ray Direction을 어떻게 구할지만 생각하면 된다.
- (chap 17의 내용을 다시 보면) Projection Frustum을 NDC로 정규화하는 것이 렌더링이고, picking은 NDC를 (정확히 말하면 중간에 window client area에서 NDC로 변환하는 게 필요하지만) 다시 Projection Frustum 공간으로 바꿔서 Picking Ray를 구하는 것이다.
- Perspective Projection은 시야각이나, 종횡비 등이 있어서 Direction을 구하기가 살짝 복잡했는데,
- 이 친구의 Ray Direction은 간단하다. 바로 projection frustum(box) 박스의 축 방향이다.
- 하지만 Ray Origin이 매번 바뀐다. 연습문제 6번을 거꾸로 하자. [-1, 1]을 [left, right]으로 바꿔야 한다.
- 만족하는 식은
> x' = ((right - left) * x + (right + left)) / 2
- 위 사상을 만족하는 열을 만들면 된다.
- y 좌표는 x와 똑같이 하면 되고
- z는... 그냥 nearZ로 고정시키면 된다.
- w도 1로 고정한다.
- Perspective에서 picking은 Direction이 바뀌는 거고, Orthogonal에서는 Origin이 바뀌는 것이다.
연습 문제 8
- PCF를 사용하지 않았을 때, 그림자가 어떤지 테스트해보는 연습문제다.
- 확실히 안 예쁘다.
연습 문제 9
- slope - scaled - bias를 사용하지 않았을 때, 그림자가 어떤지 테스트 해보는 문제다.
- 줄무늬가 보인다.
연습 문제 10
- 생각보다 많이 크게 해야 그림자가 떨어진다...
연습 문제 11
- 시야각 90도인 원근 투영으로 상하전후좌우 그림자 맵을 생성하는 방법을 설명하라고 하는데...
- chap 18의 큐브 매핑을 그림자 버전으로 하면... 되지 않나? ㄹㅇㅋㅋ
8. 메모용
- 과도한 bias와 큰 pcf를 사용할 때 생기는 오차로 엉뚱한 판정이 나오는 걸 해결하는 방법에 대한 소개다.
- Chapter 17. Efficient Soft-Edged Shadows Using | NVIDIA Developer
- Shadow Mapping: GPU-based Tips and Techniques (yumpu.com)
- Lauritzen-SDSM(SIGGRAPH 2010 Advanced RealTime Rendering Course).pptx (live.com)
책 : DirectX 12를 이용한 3D 게임 프로그래밍 입문
'Programming > D3D12' 카테고리의 다른 글
[책공부] Quaternion + chap 22 연습 문제 (0) | 2024.03.12 |
---|---|
[책공부] Ambient Occlusion + chap 21 연습 문제 (0) | 2024.03.08 |
[책공부] Normal Mapping + chap 19 연습 문제 (0) | 2024.02.27 |
[책공부] Cube Mapping + chap 18 연습 문제 (0) | 2024.02.26 |
[책공부] Picking + chap 17 연습 문제 (0) | 2024.02.22 |