헛둘이 2023. 2. 18. 13:54

그림자 매핑이란?

- 빛에 의해 물체에 드리우는 그림자를 그리는 것

 

그림자가 나타나는 이유는 앞의 물체가 빛을 가리고 있기 때문이다.

그럼 그림자를 그리기 위해서는 앞에 물체가 있는지 없는지를 판별해야 한다.

 

이를 위해 빛의 위치에서 물체를 찍어서 깊이 값을 기록한 텍스쳐를 만들고,

그 텍스쳐를 이용해 실제 물체를 그릴 때 그림자를 같이 그려주는 것.

 

어떤 픽셀을 그려야 할 때 그 위치에 그림자가 있는지 없는지를 판별하려면

이 깊이 맵을 이용해야 한다.

 

그래서 라이팅이 들어가기 전 빛 위치의 카메라로 먼저 찍어서 깊이 값이 기록된 텍스쳐를 만든 후,

우리 실제 카메라로 찍어서 그 깊이값과 실제 깊이값을 비교해서 우리 물체가 더 깊이 있으면 그림자를 그려주는 것.

 

- 빛 위치의 카메라를 LC라고 부르기로 정함

 

*그림자 맵을 그리는 순서

1. LC로 찰칵

2. 클립좌표까지 구한다(WVP를 곱한 값)

3. 픽셀 쉐이더에서 그림자 맵 텍스쳐의 x값 위치에 clipPos.z / clipPos.w를 통해 실제 투영좌표에서 z값을 구해서 그걸 그려준다.

 

*그림자 맵을 이용해 그림자를 그리는 순서

1. 원래 원근 카메라로 뷰 좌표까지를 구한다.

2. 뷰 역행렬을 곱해서 월드좌표로 이동한 뒤, 그 좌표에 LC의 VP를 곱해서 LC의 클립좌표로 이동시킨다.

(LC의 VP행렬은 material의 인자로 넘겨준다 g_mat_0)

3. 그 좌표의 z값을 w값으로 나눠 LC 기준에서의 깊이값을 계산한다.

4. 그 다음 그 좌표의 xy를 w로 나눠서 실제 투영 좌표 xy를 구하고, 그 좌표를 uv좌표로 변환(xy * 0.5 + 0.5)

5. 그 좌표를 텍스쳐와 sample해서 해당 텍스쳐위치의 깊이값을 뽑아낸다.

(텍스쳐는 uv좌표이므로 uv로 변환하는 과정이 필요했던 것)

6. 그 깊이값과 아까 3번에서 구한 깊이값을 계산한 후 그림자가 드리워져야 한다면 diffuse값과 specular 값을 조작한다.

 

*참고로 그림자 맵은 우리가 그릴 화면보다 훨씬 크게 만들어줘야 한다

- 그래야 좀 더 오밀조밀한 그림자가 만들어진다.

 

 

그림자 맵을 언제 그려줄 것인가?

- 그림자는 결국 Light 연산이 시작되기 전에 그려지면 된다.

 

예제에서는 라이트 컴포넌트에서 카메라를 가지게 하고,

라이트에서 그림자를 그리는 함수를 정의하였다.

 

1. Scene의 Render에서 렌더 타겟을 초기화해준 후 RenderShadow 함수로 그림자 맵을 그려준다.

- Scene의 RenderShadow는 다시 Light의 RenderShadow를 호출한다.

- Light의 RenderShader에서는 소유한 카메라를 통해 씬의 게임오브젝트의 MeshRenderer에 접근하며,

- MeshRenderer의 RenderShadow를 호출하여 Shadow Material을 통해 물체들을 그리게 된다.

 

*그니까 어떤 쉐이더를 쓰냐에 따라 물체들이 어떤 VS, PS 과정을 거치는지가 달라지기 때문에

쉐이더의 종류도 라이트, 디퍼드, 쉐도우 등으로 나뉘는 것

 

2. 그 후 Deferred Render를 통해 텍스쳐들에 필요한 정보들을 각각의 렌더타겟에 그려넣고,

3. Render Light에서 빛에 대한 색상값을 적용할 때 1번에서 만든 쉐도우 맵을 참고해서 연산한다.

PS_OUT PS_DirLight(VS_OUT input)
{
    PS_OUT output = (PS_OUT)0;

    float3 viewPos = g_tex_0.Sample(g_sam_0, input.uv).xyz;
    if (viewPos.z <= 0.f)
        clip(-1);

    float3 viewNormal = g_tex_1.Sample(g_sam_0, input.uv).xyz;

    LightColor color = CalculateLightColor(g_int_0, viewNormal, viewPos);

    // 그림자
    if (length(color.diffuse) != 0)
    {
        matrix shadowCameraVP = g_mat_0;

        float4 worldPos = mul(float4(viewPos.xyz, 1.f), g_matViewInv);
        float4 shadowClipPos = mul(worldPos, shadowCameraVP);
        float depth = shadowClipPos.z / shadowClipPos.w;

        // x [-1 ~ 1] -> u [0 ~ 1]
        // y [1 ~ -1] -> v [0 ~ 1]
        float2 uv = shadowClipPos.xy / shadowClipPos.w;
        uv.y = -uv.y;
        uv = uv * 0.5 + 0.5;

        if (0 < uv.x && uv.x < 1 && 0 < uv.y && uv.y < 1)
        {
            float shadowDepth = g_tex_2.Sample(g_sam_0, uv).x;
            if (shadowDepth > 0 && depth > shadowDepth + 0.00001f)
            {
                color.diffuse *= 0.5f;
                color.specular = (float4) 0.f;
            }
        }
    }

    output.diffuse = color.diffuse + color.ambient;
    output.specular = color.specular;

    return output;
}

 

4. 그 후 RenderFinal에서 위에서 그린 그림들을 하나로 합쳐서 그려준다.

5. RenderForward에는 화면 가장 바깥에 그려져야 할 UI 같은 것들을 그려준다..

 

그림자 맵의 텍스쳐는 기존에 화면크기에 맞게 세팅해오던 텍스쳐들과 다르게 4096으로 세팅되므로,

이걸 IF문을 걸어서 분리하기보단 RenderTargetGroup에서 OMSetRenderTargets에서 Viewport의 크기를 세팅해주기로 했음

 

그리고 빛이 향하는 방향을 세팅할 때 카메라의 방향도 바뀌어야 하므로 그에 대한 처리도 같이 진행하였음