센로그

14. Shadow Mapping 본문

게임/게임 그래픽 프로그래밍

14. Shadow Mapping

seeyoun 2023. 6. 9. 20:18

◆ Shadow

Realistic한 scene을 만들기 위해선 Shadow가 무조건 필요하다!

 

그림을 보자. (a)는 그림자가 없기 때문에, 공중에 있는지 바닥에 있는지 알기 어렵고 위화감이 느껴진다.

반면 그림자가 있는 (b)의 경우 object가 어디에 놓여있는지를 무의식중에 굉장히 빠른 속도로 느낄 수 있다.

 

그런데.. 그림자 계산하기가 상당히 쉽지 않다고 한다. 왤까?

  • GPU 파이프라인 같은 경우, 정점을 쭉 놓고 한번에 병렬 처리한다고 했었다. 그런데 그림자라는 건, 나한테 오는 빛을 다른 누군가 막은 것임. 그러면, 나한테 오는 빛을 '누가 막았는지'에 대한 정보가 필요해짐. 오브젝트간 참조가 필요하다는 뜻!
    => 근데 GPU는 병렬 처리 한다며? 그럼 참조 못하잖아! => 특수한 방법으로 이를 처리하는 방법이 생김.
  • 또 그림자는 정지되어있지 않고 움직일 수 있다는 특성구를 가리던 큐브가 움직이면 이에 따라 그림자도 달라짐..
    따라서 미리 만들어놓기도 어려워서, 계산 비용이 꽤 듦.

 

지금부터 이 특수한 방법에 대해 알아볼 것이다.

 


◆ Shadow Mapping - two pass algorithm

two-pass algorithm

우리가 그림자 처리를 위한걸 따로 만드는 게 아니라, 기존의 렌더링 파이프라인을 그대로 이용할 것임.

대신 그림자 렌더링을 위해, two-pass 알고리즘을 사용한다. CPU → GPU로 데이터를 두번 pass한다는 뜻 (그만큼 계산 비용 비쌈)

 

 

  • CPU → GPU 렌더 콜
  • GPU → CPU depth map 데이터 전송 [NEW!]
  • CPU → GPU 다시 한번 처리 요청 [NEW!]
  • GPU → CPU 데이터 전송 

 

Pass 1.  not real rendering

그림자를 위해 필요한 정보(Shadow map)를 생성하는 단계

light source를 카메라 EYE로 취급하여, Depth 정보(Z-buffer)를 Shadow map 기록함.

카메라의 Depth map 만들 때처럼, Depth 정보를 far plane에서 near plane으로 projection해서, 광원에서 보이는 물체들이 어느 정도의 깊이값을 가지고 있는지를 기록한다.

 

 

 

Pass 2. real rendering

이번에 다시 두번째 GPU로 돌아온 것임

이제 이 Depth 정보를 가지고 물체가 실제로 가려져있는지, 즉 빛이 그 물체에 도달할 수 있는지 없는지를 판단함

도달할 수 없다면 그림자가 되는 것!

 

이번에는 원래대로 카메라 position에서 렌더링을 때림. (카메라에 보이는 pixel들을 처리해 줄거니까.)

 

그림에서 q1을 처리한다고 해보자. 우리는 q1이 빛을 받는지, 아니면 다른 물체에 가려져서 빛을 못 받는지를 알아야 함.

그런데 q1점 입장에서는 sphere에 접근할 권한이 없자나! 그럼 어카냐?

=> 아까 만들어놧던 shadow map을 이용하자!

 

우선 light와 q1사이의 거리인 d1을 계산한다. 또, Shadow map에 접근하면 light에서 sphere 표면 까지의 거리인 z1에 대한 정보를 알 수 있다. 이때 z1과 d1이 비슷하면 빛이 도달하는 것. 그러나 그림처럼 z1d1에 비해 너무 짧으면(z1<d1), q1은 뭔가에 가려지고 있다는 뜻이므로 Shadow가 되는 것이다.

 


◆ Shadow Mapping - Acne artifact

그런데 이걸 실제로 구현하려고 하면, 생각보다 복잡한 면이 생긴다.

무작정 구현하려고 하면 그림의 오른쪽 처럼 이상하게 구현됨! 

왤까?

 

문제는!

Shadow map을 만들 때 ray를 쏴서 샘플링하는 건데, 이런 방식으로는 사실 모든 점이 빽빽하게 샘플링되지는 않는다는 것. Shadow map 해상도는 정해져있을 건데, 그 안에 모든 점의 정보를 다 담지는 못함. 또, 빛은 산란(퍼져나감)하기 때문에 아무리 빛을 촘촘하게 쏴도 멀리 있는 pixel까지 가면 촘촘하게 샘플링 못 함. 

그러면 당연히 Depth 데이터가 없는 pixel들도 생길 것이다.

 

이런 pixel들의 경우, 어쩔 수 없이 nearest point sampling을 하도록 한다.

딱 맞는 데이터가 없으면, 젤 가까운 걸로 갖다써! 하는 방법.

근데, 문제는 이게 0~1 사이의 데이터이기 때문에, shadow이면 너무 어두워지고 아니면 너무 밝아져서, 위 그림처럼 못생긴 결과물 나옴

 

그림에서 q1q2에 대해 생각해보자. (q1과 q2를 가리고 있는 물체는 없다.)

예를들어 z1= 7, z2 = 10이고, q1 = 8, q2 = 9라 하자.

우선 q1이 shadow인지 판단할거야. 정확히 q1 쪽으로의 정보는 없네? 그럼 얘는 z1이랑 더 가까우니까 z1이랑 비교

=> z1 < distance(q1) 이므로 안보임 (shadow)

이번에는 q1의 바로 오른쪽에 있는 q2를 비교해보자. 얘도 정보가 없네? 그럼 얘는 z2랑 더 가까우니까 z2랑 비교

=> z2 > distance(q2) 이므로 보임 (lit)

이런식으로 바로 옆에 있는 pixel 끼리도 뭐랑 비교하느냐에 따라 어둡고 밝고 바뀌는 상황 발생

 

이런 문제를 Acne artifact 라고 한다.

 


◆ Shadow Mapping - Solution of Acne artifact

Acne artifact에 대한 굉장히 쉬운 솔루션이 있다.

=> "bias"를 주는 것. 임의의 bias를 주고, d와 z+bias를 비교한다.

아까 예시에서 z의 bias를 한 3.5로 두고 비교하면,

z1와 z2가 10.513.5이므로, 각각 p1, p2까지의 거리보다 더 커져서 둘다 보이게 됨!

 

그런데.. 너무 큰 bias를 쓰면 shadow 크기가 너무 작아지고 (세번째 그림)

너무 작은 bias를 쓰면 여전히 노이즈한 부분이 남아있다. (두번째 그림)

 

이건 사실 3~40년 전에 쓰던 기술임ㅋㅋ 그럼 어떻게 해결했을까?

 


◆ Shadow Mapping Filtering

 

또다른 문제가 있다. 그림처럼 그림자가 깨져서 보인다는 것.(aliasing artifact)

 

모든 문제의 발단은, shadow map의 해상도가 낮기 때문임. CPU ↔ GPU 사이의 데이터가 늘어날수록 처리가 느려져서 너무 높은 해상도는 쓸 수 없음. 또, 빛은 산란하기 때문에 태양이 아무리 빛을 조밀하게 쏘더라도 태양으로부터의 거리가 멀면 듬성듬성해질 수밖에 없음

 

shadow map의 해상도가 충분하지 않다면, 여러 pixel이 shadow map의 하나의 texel에 mapping되는 경우가 생길 것임.

texure mapping에서 magnification시 나타나던 문제랑 똑같음! 이를 해결하기 위해서 우리는 interpolation을 했었음.

 

 

그림에서 q점 밑에 적힌 (80)이 광원으로부터 q까지의 거리이고, 주변 texel들에 적힌 값(10, 110, 100, 100)이 texel들의 z값임. 만약 nearest point sampling을 했다면, 왼쪽 위 값(10)이 비교대상으로 뽑힐 것. 그래서 최종적으로 0(shadow)이된다.
[왼쪽 그림] 그렇다면 주변 texel들의 z값을 가지고 bilinear interpolation을 해보자. 보간한 값(64)와 q까지 거리(80)을 비교함. 그래도 최종적으로 0(shadow)이 됨. 결국 나아진 게 없음...
[오른쪽 그림] PCF를 이용한 것. 각 texel들의 visibility를 가지고 bilinear interpolation해보자. 최종적으로 0.58이 나옴. 그러면 58% 만큼 pixel을 밝혀주자! 왼쪽보단 나음.

 

shadow mapping도 마찬가지로 interpolation을 해준다.

근데 그냥 bilinear interpolation은 도움이 안됨. 일반 texture의 경우에는 주변 색을 섞어서 부드럽게 만들 수 있었는데, shadow의 경우에는 완전히 빛을 받느냐, 안받느냐 둘중 하나이기 때문에 그 사이를 보간하는 건 의미가 없음. (왼쪽 그림)

이를 해결하기 위해서, PCF(Percentage Closer Filtering)라는 방법을 사용함. (오른쪽 그림)

 

그러나 이번엔 다른 방법을 쓸거다!

아까는 주변 texel들의 z값을 이용해 interpolation 했었음

이번에는 pixel에 대한 주변 texel들의 visibility를 계산한다. 10, 110, 100, 100 이 z값들을 pixel의 d와 비교해서, 0또는 1의 visibility로 나타내준다. 그런후에 이 visibility들을 interpolation한 값을 가지고 pixel의 shadow를 표현한다.
이말은 즉슨, 단순히 밝다/아니다를 표현하는 게 아니라, 어느 정도 밝혀지고 있는지(degree of being lit)에 대한 수치를 표현하겠다는 것이다. 이를 PCF라 함.
 

왼쪽은 완전 지글재글하지만 오른쪽은 나름 덜함. 적당히 뭉갠 거니까...

근데 얘도 뭐.. 아직 여전히 지글재글함ㅎㅎ

 


◆ Shadow Mapping - Implementation

그러면 우선은 실제 구현시 PCF까지 진행되는 과정을 살펴보자.

 

[First- pass 과정]

 

Camera 구조체를 그대로 갖다쓴다고 했다.

즉 내가 별도의 시스템을 만들 필요없이, 이미 만들어진 파이프라인 위에서 동작할 수 있는 것 ㅇㅇ

 

어떻게하느냐!

 

 일단 Vertex shader에서 object space → world space까지는 똑같이 변환 해준다.

그다음에, world spacelight space(light에서 바라봤을 때 space; 사실상 이름만 다른 camera space)로 가져온다light source에다가 EYE, AT, UP을 정의하고, 기존과 똑같이 view transform 한다는 것. light space에서의 view frustum도 만듦. 그리고 마찬가지로 projection transform 해서 CLIP space로 넘김.

 

다른점은!  원래 Vertex shader는 vertex position 이외에, vertex 속성들(color, normal, ...)도 내어 줬다.

그런데 pass1 에서 Vertex shader는 그런 속성들은 굳이 안 줌. 우리는 빛이 물체까지 갔을 때의 거리, depth만 필요한 것이기 때문에, 다른 계산은 굳이 안함. 다시말해 Vertex shaderOutput position 말고는 관심없음. (실제 output position이 아니라 depth것임ㅇㅇ)

 

이에따라 당연히 vertex 속성들(color, normal, ...)을 보간하던 Rasterizer도 보간할 게 없고, (우리가 필요한건 RGB가 아니라 z값이라서) pixel의 칼라도 필요없으므로 Pixel shader도 필요없음. 

 

대신 Rasterizer에서 Screen space 좌표return되어야 함(원래  attribute들과 함께 (x, y, z)도 넘겨주는데, 명시적으로 넘겨주지는 않음. 걍 알아서 넘겨줌). Output-Merge stage에서 Z-buffering(어떤 픽셀이 더 멀리있는지 판단. 에서 진행함)해줘야하기 때문. 이렇게 최종적으로, light source로부터 보이는 표면 점들의 depth값을 저장할 수 있다.

 

 이렇게 Shadow map완성되었다!

 

더보기

+) 이제 Shadow map을 텍스처 형태로 얻어야 한다

조금 특수한 처리가 필요함. 기존에 depth map이 아닌 일반적인 렌더링을 할 때, return color 하면 자동으로 내 화면에 보였었다. 우리가 렌더 타겟 뷰에다가 렌더링해서 내 화면에 송출되게 했었기 때문

근데 지금은, depth map을 화면에 송출할 게 아니고 텍스처의 형태로 받을 것이기 때문에 특수 처리가 필요함

=> Render texture라는 것을 만들고, 이걸 렌더 타겟으로 둬야함.
 
 
이로부터 Shadow map을 texture 형식으로 얻을 수 있다.

 

[Second- pass 과정]

그담엔 second-pass, 즉 실제 렌더링 할 때의 이야기. 

앞서 언급했듯이, 점 두개를 비교해서 얘가 빛을 받는지, 그림자인지 결정함

Depth velue(Shadow map에서 가져옴)light depth value(직접 구함)를 구함.

만약 light depth valuedepth value보다 작으면Light intensity() 계산해서 실제 칼라를 구한다,

 


◆ Hard Shadow vs Soft Shadow

지금까지 배운 건 Hard shadow엿다.

  • Hard shadow : fully lit 또는 fully shadow를 결정하는것. 완전히 어둡고/완전히 밝고 두 개의 상태만 존재하는 경우
  • Soft shadow : 어느정도로 밝은지에 대한 상태가 0~1사이로 나타남.

그런데 hard shadow 모든게 해결되지 않는 경우가 존재함!

그림과 같은 Area light의 경우를 생각해보자

그림에서, 거의 모든 상황에서 umbra 부분은 빛을 안받음.

근데, penumbra 같은 경우는 오른쪽에서 온 빛은 받고, 왼쪽에서 온 빛은 못받음. 

그래서 뭔가 umbra부분보다는 좀 연해야할거같아!!

=>나한테 도달하는 빛을 확률적으로 모델링하고 이에 따라 부드럽게 쉐도우(soft shadow)를 만들어줄 필요가 생김

 
더보기

※ 이런 연구가 시작 된 이유!?

옛날엔 빛을 벡터로 생각하고 다뤘었다. "나한테 도달하냐 안하냐..."

근데 이론들이 발전하다보니, 빛이라는걸 광자량으로 다루도록 바뀌게 됨. "나한테 얼마나 도달하냐…"

내가 빛으로부터 많은 에너지를 받는 것과 적은 에너지를 받는 건 분명히 차이가 존재한다는 걸 고려하기 시작함

예를들어 전체의 총 에너지는 100이라 한정하고, 100이라는 에너지가 어떻게 분포하는게 가장 알맞을까? 에 관한 것.

Comments