ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] Dynamic Shadow (2/2) - Spot/Point/Rect light
    UE4 & UE5/Rendering 2022. 5. 7. 02:46
     

    [UE5] Dynamic Shadow (1/2)

     

     

    최초 작성 : 2022-05-07
    마지막 수정 : 2022-05-07
    최재호

     

     

    Updated 2020-12-17 : CubeMap 의 6면을 동시에 그리는 방식이 인스턴싱을 통해 처리한다는 설명을 추가했으며, 기존에 SV_RenderTargetArrayIndex 부분이 지오메트리에 쉐이더에서 하는 작업을 처리해준다는 설명은 잘못된 내용이라 제거

     

    목차

    1. 환경
    2. 목표
    3. 내용
      3.1. Light가 렌더스레드에 미러링 되는 과정
      3.2. Light가 렌더스레드에 추가되는 과정
      3.3. Light의 Transform이 업데이트되어 렌더스레드에 반영되는 과정
      3.4. Light가 렌더스레드에서 제거되는 과정
      3.5. Spot light 의 쉐도우맵 렌더링 준비
      3.6. Spot light 의 쉐도우맵 렌더링
      3.7. Point/Rect light의 쉐도우맵 렌더링 준비
      3.8. Point/Rect light의 쉐도우맵 렌더링
    4. 레퍼런스

     

    1. 환경

    Unreal Engine 5 (5.0 branch 2170f78d3d94abd7b24b7d6d5c5104cfb43245ea)

    UE4의 코드와 거의 유사하므로 UE4에 익숙하시면 보시는데 문제가 없을 것 같습니다.
    일부 코드는 줄 바꿈 되거나 접은 상태로 중요한 코드를 위주로 설명하니 보실 때 참고해주세요.
    또한 코드를 직접 붙여 넣어서 진행했기 때문에 작아서 잘 보이지 않는 코드는 클릭하여 봐주세요.

    개인적으로 분석한 내용이라 틀린 점이 있을 수 있습니다. 그런 부분은 알려주시면 감사하겠습니다.

    필요한 사전 지식
    Omnidirectional light

    아래 내용을 먼저 읽고 이해한 뒤 보면 더욱 좋습니다.
    [UE5] MeshDrawCommand (1/2)
    [UE5] MeshDrawCommand (2/2)
    [UE5] GPUScene 과 Auto-Instancing (1/2)
    [UE5] GPUScene 과 Auto-Instancing (2/2)

     

    이번 글은 한 번에 많은 길이의 코드를 분석하는 부분이 종종 등장합니다. 그래서 글을 2개 띄우고 한쪽은 설명 부분을 한쪽은 코드 이미지를 최대화해서 보는 것을 추천합니다.

     

    2. 목표

    이전 글 DynamicShadow (1/2) 에서는 Movable Directional light의 CSM 을 봤습니다.
    이번 글에서는 Movable Spot, Point, Rect light에 대해 알아봅니다.
    이 글에서는 쉐도우를 렌더링 하는 알고리즘은 다루지 않을 것이며, 언리얼의 쉐도우맵 렌더링의 코드 구조에 집중할 것입니다.

     

     

    3. 내용


    이전에 알아본 Directional light의 CSM과 Spot/Point/Rect 라이트와의 차이점을 알아보고 그 부분을 기준으로 코드를 분석해봅시다.

    첫 번째로, Spot, Point, Rect light는 쉐도우맵에 렌더링 할 프리미티브를 수집하는 방식이 Directional light와는 다릅니다. 프리미티브를 수집하는 시점은 프리미티브가 FScene에 등록되는 시점에 처리됩니다. 라이트와 라이트가 그려야 할 프리미티브들은 FLightPrimitiveInteraction 클래스를 통해 관계가 형성됩니다. 이 부분을 이해하기 위해서 Light가 렌더스레드에 어떻게 등록되는지와 자신이 렌더링 할 프리미티브들을 어떻게 등록하는지 알아볼 것입니다.

    두 번째로, 사용하는 쉐도우맵의 타입도 조금씩 달라집니다. Spot light의 경우 Directional light 처럼 특정 방향으로 향하는 라이트이므로 CSM과 동일하게 2D 텍스쳐 쉐도우맵이 사용됩니다. Point/Rect light의 경우 전방향(Omnidirectional) 로 향하는 라이트이기 때문에 큐브맵을 사용하며 렌더링 합니다.

     


    3.1. Light가 렌더스레드에 미러링 되는 과정

    라이트가 렌더스레드의 FScene에 미러링 되는 전체 과정은 프리미티브가 미러링되는 과정과 유사합니다. 하지만 FScene에 등록/수정/제거되는 과정은 조금 다르게 처리됩니다. 이 과정을 알아봅시다.

    미러링 되는 과정을 알아보기 위해서 우리는 3가지 주요 함수를 확인해야 합니다. 이 3가지 함수를 차례로 알아봅시다.
    1. CreateRenderState_Concurrent : 렌더스레드에 LightComponent를 미러링 합니다. AddLight 함수로 FScene에 등록합니다.
    2. SendRenderTransform_Concurrent : 렌더스레드에 Light의 Transform을 업데이트합니다. UpdateLightTransform 함수로 FScene에 반영합니다.
    3. DestroyRenderState_Concurrent : 렌더스레드에서 미러링된 라이트를 제거합니다. RemoveLight 함수로 FScene에서 제거합니다.

    그림1. ULightComponent가 렌더스레드의 추가/수정/제거에 사용되는 함수 3종

     

    3.2. Light가 렌더스레드에 추가되는 과정

    먼저 FScene에서 AddLight를 호출하는 경우부터 봅시다.

    1. FLightSceneProxy를 만듭니다. 프리미티브의 FPrimitiveSceneProxy 생성하는 것과 비슷합니다. 하지만 FScene에 등록되거나 관리되는 것은 조금 다릅니다.
    2. FLightSceneProxy 를 사용하여 FLightSceneInfo 를 생성합니다. Proxy와 Info 모두 앞으로 렌더스레드에서 관리됩니다.
    3. AddLightSceneInfo_RederThread 함수를 호출하여 라이트를 FScene에 등록해줍니다. 함수 이름에서 힌트를 얻을 수 있듯이 렌더스레드에서만 실행되는 함수입니다.
    4. FScene에 있는 Lights 배열에 모든 라이트들이 등록됩니다.
    5. Directional light의 경우 별도의 DirectionalLights의 배열에 등록해줍니다.
    6. Directional light이고 Static light가 아닌 경우 SimpleDirectionalLight 변수에도 라이트를 등록해줍니다.
    7. 이제 라이트를 FScene에 있는 컨테이너에 등록을 마쳤고, AddToScene 함수에서 라이트를 FScene에 있는 프리미티브들과 연결해줍니다.
    8. 우리는 DynamicShadow를 보고 있기 때문에 조건문 내부로 계속해서 들어갑니다.
    9. Directional light의 경우 FScene에 있는 전체 프리미티브에 대해서 CreateLightPrimitiveInteration 함수가 호출됩니다.
    10. 나머지 다른 라이트 타입의 경우 Octree 를 사용하여 프리미티브들을 등록합니다. 현재 라이트의 바운딩박스와 겹치는 Octree node 에 포함된 프리미티프들에 대해서만 CreateLightPrimitiveInteration 를 호출합니다. 계속해서 CreateLightPrimitiveInteration 함수 내부로 가봅시다.
    11. CreateLightPrimitiveInteration에는 라이트와 프리미티브를 쌍으로 전달받습니다. 그리고 이 프리미티브가 라이트에 영향을 받을 수 있는지 확인합니다. 라이트와 프리미티브의 바운드박스가 겹치는지 확인하거나 라이트 채널이 맞는지 확인합니다.
    12. 라이트가 영향을 미치는 프리미티브들은 링크드 리스트 형태로 라이트에 등록됩니다. 이러한 링크드리스트는 FLightPrimitiveInteraction::Create 함수를 통해 생성됩니다.
    13. 프리미티브가 Dynamic light에 영향을 받을 수 있는지 그리고 라이트에 영향을 받을지 여부를 확인합니다.
    14. Directional light인 아니거나 Light가 StaticShadow를 갖고 있는 경우만 계속해서 FLightPrimitiveInteraction 클래스를 생성하는 것을 볼 수 있습니다. 이 클래스가 프리미티브의 링크드 리스트를 만들어 줍니다. 현재는 DynamicShadow를 보고 있기 때문에 StaticShadow는 아니고, Directional light가 아닌 경우만 라이트가 영향을 줄 수 있는 프리미티브의 리스트를 소유하게 되는 것을 알 수 있습니다.
    15. FLightPrimitiveInteraction 생성 부분입니다. 생성한 객체는 따로 어디서 보관하지 않는데, 그 이유는 FLightPrimitiveInteraction 생성자에서 FLightPrimitiveInteraction을 라이트의 링크드리스트에 추가해주기 때문입니다. 이후 FLightPrimitiveInteraction의 소멸은 라이트에서 책임집니다.
    16. 라이트와 프리미티브 둘 다 DynamicShadow 를 케스팅 하는지 비교하여 쉐도우 캐스팅 여부를 확인합니다.
    17. Dynamic 라이트를 받을 수 있는 프리미티브의 경우 라이트의 링크드리스트에 자신을 등록해줍니다. 프리미티브를 등록할 링크드리스트의 타입은 2가지가 있습니다. 하나는 이동하고 있는 프리미티브, 다른 하나는 스태틱 프리미티브를 위한 리스트입니다. 이동하고 있는 객체 여부는 IsMeshShapeOftenMoving 함수를 통해 판단합니다. 프리미티브의 Mobility가 Movable이던가 쉐도우맵에 캐싱을 추천하지 않는 경우 이동하고 있는 프리미티브 리스트에 등록됩니다.
    18. 17번 과정에서 선택된 링크드리스트에 프리미티브를 등록해줍니다.

    그림2. 라이트를 렌더링 스레드에 등록하는 경우. 라이트에 영향을 받는 모든 프리미티브들을 라이트의 링크드리스트에 연결함.

     

    이렇게 라이트가 렌더스레드에 미러링되는 경우 기존에 FScene에 있던 모든 프리미티브들을 라이트에 등록해주는 것을 알 수 있습니다. 그렇다면 반대로 라이트는 이미 FScene에 있고, 프리미티브가 FScene에 등록되는 경우는 어떻게 되는지 알아봅시다.

    1. 프리미티브들이 렌더스레드에 미러링 될 때, FScene에 프리미티브를 등록 예약해두면, UpdateAllPrimitiveSceneInfos 함수에서 예약된 프리미티브를 모두 FScene에 등록해줍니다.
    2. 프리미티브를 추가하고 AddToScene 함수를 호출하며, 이 함수가 하는 일 중 자신이 영향을 받는 라이트를 찾아서 등록해주는 기능이 있습니다.
    3. 프리미티브와 라이트 간의 FLightPrimitiveInteraction 를 생성하기 위해서 CreateLightPrimitiveInteractionsForPrimitive 를 호출합니다.
    4. 라이트의 Octree 를 사용하여 프리미티브와 교차하는 모든 라이트에 대해서 CreateLightPrimitiveInteraction을 호출합니다.

    그림3. 라이트가 이미 렌더링 스레드에 등록 되어있고 프리미티브가 렌더스레드에 추가 되는 경우. 프리미티브가 영향을 받는 라이트에 프리미티브를 등록해 줌.

     

    프리미티브가 FScene에서 제거되는 경우 라이트와의 관계가 어떻게 끊어지는지 봅시다.


    1. 프리미티브가 렌더스레드에서 제거되는 과정도 UpdateAllPrimitiveSceneInfo에서 수행됩니다.
    2. RemovedLocalPrimitiveSceneInfos 에 제거 예약된 프리미티브들이 있습니다. 이 프리미티브를 순회합니다.
    3. 프리미티브의 RemoveFromScene 함수를 호출합니다. 이 과정 중 라이트와의 관계를 끊습니다.
    4. FLightPrimitiveInteraction 함수를 사용하여 자신이 등록되어있는 모든 라이트의 링크드리스트에서 자신의 프리미티브를 제거합니다.

    그림4. 렌더스레드에서 프리미티브가 제거되고 있고, 프리미티브가 렌더스레드에서 제거되는 경우. 프리미티브가 영향을 받는 라이트의 링크드리스트에서 자신을 제거함.

     

    프리미티브가 FScene에서 갱신되는 경우를 봅시다. 이 경우 Transform이 업데이트되는 경우입니다.

    1. 프리미티브가 렌더스레드에서 갱신되는 과정도 UpdateAllPrimitiveSceneInfo에서 수행됩니다.
    2. Transform이 업데이트될 프리미티브들은 UpdatedTransforms에 있습니다.
    3. 2개의 컨테이너 중 하나에 프리미티브를 담습니다. 여기서는 컨테이너가 어떤 목적인지는 중요하지 않습니다. 계속해서 코드를 봅시다.
    4. 프리미티브의 RemoveFromScene 함수를 호출합니다. 이때 프리미티브와 라이트의 관계가 끊깁니다.
    5. 3번 과정에서 담아둔 프리미티브들의 AddToScene을 호출합니다. 이때 프리미티브와 라이트의 관계가 새로 생성됩니다.

    그림5. 렌더스레드에서 프리미티브의 정보가 업데이트 되는 경우. 프리미티브를 FScene 제거했다가 다시 등록하므로 라이트와의 관계 또한 새로 만들어짐.

     

    3.3. Light의 Transform이 업데이트되어 렌더스레드에 반영되는 과정

    계속해서 라이트의 Transform이 갱신되는 경우를 확인해봅시다.

    1. 라이트의 Transform이 업데이트되어 렌더스레드에 반영되는 경우 호출되는 함수입니다.
    2. FScene의 UpdateLightTransform 를 호출하고, 계속해서 FScene의 UpdateLightTransform_RenderThread 함수를 렌더스레드에서 호출할 수 있게 렌더스레드에 전달합니다.
    3. Directional light가 아니면 bRemove 가 true가 됩니다. 즉, Directional light가 아니면 라이트를 FScene에서 제거했다가 다시 등록하는 과정을 거칩니다. 이 경우 라이트와 연관된 모든 프리미티브를 새로 등록하는 과정을 거칩니다. (Directional light가 아닌 경우 라이트를 빈번하게 이동하게 되면 퍼포먼스에 좋지 않을 수 있겠네요.) 그리고 이 라이트가 FScene에 등록된 여부가 있는지 확인하여 등록되어있다면 bHasId 를 true로 설정합니다.
    4. bRemove 가 true 인 경우 라이트를 장면에서 제거합니다.
    5. 렌더스레드에 있는 라이트의 Transform을 갱신해줍니다.
    6. bRemove 가 true 이고, 렌더스레드에 등록되어 있다면 AddToScene 함수를 호출하여 라이트에 영향을 받는 프리미티브들을 라이트의 링크드리스트에 등록해줍니다. AddToScene 호출 과정은 그림2의 7번에서 확인했습니다.

    그림6. 렌더스레드에서 라이트의 Transform이 업데이트 되는 경우, Directional light가 아니면 라이트를 Scene에서 제거했다가 다시 Scene에 추가함.

     

    3.4. Light가 렌더스레드에서 제거되는 과정

    계속해서 Light가 렌더스레드에서 제거되는 과정을 봅시다.

    1. DestroyRenderState_Concurrent 함수에서 RemoveLight 함수를 호출합니다.
    2. RemoveLight에서는 렌더스레드에서 RenderLightSceneInfo_RenderThread를 호출할 수 있도록 합니다.
    3. 라이트가 제거되기 때문에 FScene에 등록되었던 모든 정보들을 제거합니다. Directional light의 경우 DirectionalLights 배열에서 제거하고, SimpleDirectionalLight에 등록된 적이 있다면 이것 또한 제거합니다.
    4. RemoveFromScene 을 호출하여 FScene에 있는 라이트들을 모두 제거합니다. 6번에서 계속해서 이 함수의 내부를 봅시다.
    5. FScene의 Lights 배열에서 라이트를 제거합니다.
    6. RemoveFromScene 함수에서는 라이트의 옥트리에서 라이트를 제거해줍니다. 그리고 Detach 함수를 호출합니다.
    7. Detach 함수는 라이트가 영향을 주고 있는 프리미티브 리스트를 모두 제거 합니다.
    8. 라이트가 소유하는 프리미티브와의 관계를 모두 제거합니다. Moving 프리미티브 리스트와 Static 프리미티브 리스트 둘 다 제거합니다.

    그림7. 렌더스레드에서 라이트가 제거되는 경우

    지금까지 Directional light가 아닌 경우 라이트가 영향을 줄 프리미티브들을 어떻게 관리하는지 알아봤습니다. 이제 나머지 타입의 라이트가 쉐도우맵을 그리는 방법을 알아볼 준비를 마쳤습니다.

     

    3.5. Spot light 의 쉐도우맵 렌더링 준비

    Spotl light의 경우 단방향으로 라이팅을 하기 때문에 2D 쉐도우맵에 렌더링하며, 쉐도우맵에 렌더링할 프리미티브는 Spot light가 갖고 있습니다.  그리고 Spot light 경우 쉐도우맵 캐싱을 기본적으로 사용합니다. 전체 프리미티브를 다 캐싱하지 않고, Static 프리미티브만 별도의 렌더타겟에 캐싱해두고 복사해서 사용하며, Dynamic 프리미티브는 매 프레임 그리게 됩니다. 이 정보를 기반으로 코드를 분석해봅시다.

    1. 먼저 쉐도우맵 렌더링을 준비하는 과정을 봅시다. InitViews 부터 BeginInitDynamicShadows 함수까지의 진행은 이전 CSM 쉐도우 과정과 동일합니다.

    그림8. DynamicShadow 의 준비를 시작하는 BeginInitDynamicShadows 함수가 호출되는 과정

     

    1. BeginInitDynamicShadows 함수를 시작합니다. 전체적으로 중요한 부분들을 둘러봅시다.
    2. FDynamicShadowsTaskData 를 생성합니다. 하지만 이것은 CSM관련 정보를 담는 클래스였습니다. 그래서 이번에는 이 클래스를 보지 않을 것입니다.
    3. 모든 라이트를 순회합니다.
    4. 쉐도우맵을 사용하는지 레이트레이싱 쉐도우를 사용하는지 여부입니다. 현재는 쉐도우맵을 사용합니다.
    5. 라이트에 스태틱 or 다이나믹 쉐도우를 케스팅 하는지 여부를 체크합니다. 쉐도우를 캐스팅 하지 않는 라이트의 경우 라이트 케스트 체크 해제해두면 이후 과정이 무시될 것이므로 상당한 성능 향상을 생각할 수 있을 것 같습니다.
    6. 현재 순회 중인 라이트가 여러 뷰들에 대해서 어느 하나에서라도 렌더링 되고 있는지를 확인합니다. 뷰프러스텀 내부에 있는지를 확인합니다.
    7. 6번 과정에서 라이트가 뷰에 렌더링되고 있는 경우만 계속해서 처리됩니다.
    8. 현재 보고 있는 라이트가 Movable mobility를 가지기 때문에 bShouldCreateShadowForMovableLight와 bCreateShadowForMovableLight 둘 모두 true 가 됩니다. 아래에 여러 조건에 따라 몇 개의 플래그가 더 있는데 여기서는 이 두 개만 확인하는 것으로 충분합니다.
    9. 다음으로 CreateWholeSceneProjectedShadow를 호출합니다. 이 함수는 LightSceneInfo->Proxy->GetWholeSceneProjectedShadowInitializer를 호출하고 그 결과 FProjectedShadowInfo를 생성할 수 있는 FWholeSceneProjectedShadowInitializer를 생성합니다. 그런 뒤 호출이 성공하면 계속해서 FProjectedShadowInfo를 생성하는 함수입니다. 이전 글에서 Directional light의 경우 CreateWholeSceneProjectedShadow 함수가 비어있습니다. 하지만 다른 라이트는 여기에서 FProjectedShadowInfo를 생성합니다. 그림10 에서 함수 내부를 따라가 볼 예정입니다.
    10. Directional light 인경우 여기서 FProjectedShadowInfo를 생성했었습니다. 하지만 오늘은 Directional light 이외의 쉐도우맵 렌더링을 확인하므로 이 함수는 필요하지 않습니다.
    11. InitProjectedShadowVisibility 는 앞에서 만들어진 FProjectedShadowInfo의 Visibility와 Relevance를 설정합니다. 이전 글에서 다뤘던 부분이라 이전 글을 참고해주세요.
    12. Preshadow는 Staionary mobility인 라이트에서 사용합니다. 현재는 Dynamic shadow를 보고 있으므로 무시합니다.
    13. 이 함수는 Spot light에서는 아무것도 하지 않습니다. 이전 CSM에서는 이 부분에서 라이트가 쉐도우맵에서 렌더링 할 프리미티브를 수집했습니다. 하지만 Spot light는 이 과정이 9번에서 수행됩니다.

    그림9. BeginInitDynamicShadows 함수의 호출과정 FProjectedShadowInfo를 생성하고 렌더링될 프리티미티브를 모음.

     

    CreateWholeSceneProjectedShadow 에서는 FProjectedShadowInfo를 생성합니다. 또한 쉐도우맵 캐싱 전이라면 캐싱할 수 있도록 준비도 진행합니다.


    1. CreateWholeSceneProjectedShadow를 실행합니다.
    2. FProjectedShadowInfo를 생성하기 위해서 FWholeSceneProjectedShadowInitializer 를 만듭니다. 라이트에서 이 클래스를 넘겨서 라이트 타입에 맞는 적절한 값을 채웁니다.
    3. View 를 기준으로 쉐도우맵의 해상도를 결정합니다.
    4. 쉐도우 캐시에 대한 내용입니다. 여기서 사용하는 EShadowDepthCacheMode는 SDCM_MovablePrimitivesOnly, SDCM_StaticPrimitivesOnly 입니다. 캐시 되는 쉐도우맵에는 Static 프리미티브들만 렌더링됩니다. 이 경우 SDCM_StaticPrimitivesOnly를 사용합니다. 그리고 Movable 프리미티브들은 Static 프리미티브를 렌더링 해둔 쉐도우맵을 복사한 다음 그 위에 Movable 프리미티브를 렌더링 합니다. 그래서 캐시를 해야 하는 경우 CacheMode[0] 에는 SDCM_StaticPrimitivesOnly, CacheMode[1] 에는 SDCM_MovablePrimitivesOnly가 들어가게 됩니다. 그리고 NumShadowMaps 를 2로 설정됩니다. 이 값은 5번 단계의 ComputeWholeSceneShadowCacheModes 에서 설정해줍니다. 만약 캐싱이 완료된 상태라면 이후에는 NumShadowMaps 는 1로 설정되고 CacheMode[0]은 SDCM_MovablePrimitivesOnly가 사용되며 CacheMode[1]은 사용되지 않습니다.
    5. 쉐도우맵의 캐싱 여부를 결정하는 함수입니다. 캐싱이 필요하다면 NumShadowMaps 함수를 2로 설정하여 Static 프리미티브용 쉐도우맵을 캐싱합니다. 자세한 내용은 그림11 에서 계속 보겠습니다.
    6. 라이트의 뷰프러스텀을 얻습니다.
    7. AddInteractingPrimitives 람다 함수입니다. 이 함수를 사용하여 쉐도우맵에 렌더링할 프리미티브를 수집합니다. 일단 함수의 본체가 있다는 것만 보고 실제 함수가 호출될 때 내부 구조를 보겠습니다.
    8. NumShadowMaps 개수만큼 FProjectedShadowInfo를 생성합니다. 이번 프레임에 쉐도우맵 캐싱하는 경우 2개, 아닌 경우 1개 생성될 것입니다.
    9. 생성한 FProjectedShadowInfo에 FWholeSceneProjectedShadowInitializer 를 넘겨 설정합니다.
    10. CacheMode를 설정합니다.
    11. 생성한 FPrjectedShadowInfo가 SDCM_MovablePrimitivesOnly인 경우 Dynamic 프리미티브를 렌더링 할 쉐도우맵에 추가합니다. AddInteractingPrimitives 람다함수를 호출하며, 람다 함수에는 라이트가 갖고 있는 Moving 프리미티브 리스트를 전달합니다.
    12. 생성한 FProjectedShadowInfo가 SDCM_StaticPrimitivesOnly인 경우 Static 프리미티브를 렌더링할 쉐도우맵에 추가합니다. AddInteractingPrimitives 람다 함수를 호출하며, 람다함수에는 라이트가 갖고 있는 Static 프리미티브 리스트를 전달합니다.
    13. 라이트로부터 전달받은 프리미티브 리스트를 모두 순회합니다.
    14. 라이트와 프리미티브 둘 다 Dynamic 쉐도우를 캐스팅 하는지 여부를 확인합니다.
    15. 프리미티브의 바운드박스가 라이트의 뷰프러스텀에 교차되는지 확인하여 교차되는 경우만 렌더링 될 프리미티브로 등록합니다.
    16. AddSubjectPrimitive 함수를 사용하여 쉐도우맵에 렌더링 할 프리미티브를 등록합니다. 이 함수는 이전 글인 CSM 의 내용과 동일하므로 레퍼런스2 를 참고해주세요.
    17. 캐싱하는 쉐도우맵이 렌더링할 프리미티브를 갖고 있는지 확인하고, 그 값을 캐시된 쉐도우맵 데이터에 보관합니다.
    18. VisibleLightInfos의 AllProjectedShadows 배열에 생성한 FProjectedShadowInfo를 추가해줍니다. 추후 라이트로부터 렌더링 할 FProjectedShadowInfo를 얻어올 때 사용합니다.

    그림10. CreateWholeSceneProjectedShadow 는 FProjectedShadowInfo를 생성하고 라이트가 영향을 미치는 프리미티브들을 FProjectedShadowInfo에 전달함.

     

    바로 전 과정에서 본 캐시모드 관련 함수인 ComputeWholeSceneShadowCacheModes를 봅시다.


    1. 쉐도우맵 캐시를 지원하는지 확인합니다. 지원하지 않는다면, 10번으로 건너뛰어 캐시모드 SDCM_Uncached 로 설정해줍니다. 이렇게 되면 Static, Movable 프리미티브들을 모두 쉐도우맵에 매번 렌더링 하게 됩니다.
    2. 기존에 생성해둔 캐시 된 쉐도우맵이 있는지 확인합니다. 만약 없다면 9번으로 건너뛰어 바로 새로운 쉐도우맵 캐시를 생성합니다.
    3. 기존에 캐시된 쉐도우맵과 현재 렌더링할 쉐도우맵의 차이가 있는지 비교합니다. 차이가 있는 경우 8번 코드로 건너뛰어 캐시되어있던 쉐도우맵 렌더타겟을 릴리즈하고, 이번 프레임에서는 SDCM_Uncached 로 설정하여 Static, Movable 프리미티브를 모두 그려줍니다. 그리고 다음 프레임에 5번과정에서 다시 쉐도우맵 캐시를 시도할 것입니다.
    4. 이미 쉐도우맵을 캐시해뒀고, 설정도 동일하며, 쉐도우맵 사이즈 또한 동일한 경우 캐싱된 쉐도우맵을 복사해서 사용하면 되므로, SDCM_MovablePrimitivesOnly 타입의 FProjectedShadowInfo만 있으면 됩니다.
    5. 만약 캐시 된 쉐도우맵을 갱신해줘야 한다면 이 코드를 실행합니다.
    6. TryToCacheShadowMap을 호출해줘서 새로 Static 프리미티브들을 렌더링 한 쉐도우맵을 캐싱합니다. 초록색 선을 따라서 함수 구현을 보면 캐시 할 쉐도우맵의 크기가 예산 내에 들어오면 CacheModes[0] 에 Static 프리미티브를 렌더링 하여 캐싱할 수 있는 SDCM_StaticPrimitivesOnly 로 설정하고, CacheMode[1] 에는 Dynamic 프리미티브를 렌더링 하는 SDCM_MovablePrimitivesOnly 를 설정합니다. 이때 이 순서도 반드시 SDCM_StaticPrimitivesOnly -> SDCM_MovablePrimitivesOnly 순서로 돼야 하는데, SDCM_MovablePrimitivesOnly의 경우 캐시 된 쉐도우맵을 복사해오는 패스가 추가되기 때문에 SDCM_StaticPrimitivesOnly를 먼저 그려줘야 Static, Dynamic 프리미티브 모두를 렌더링 할 수 있습니다. 만약 캐시된 쉐도우맵을 사용하기에 예산이 충분하지 않다면 캐시 하지 않도록 합니다.
    7. 한 프레임에 새로 캐싱할 쉐도우맵의 수가 너무 많은 경우 그냥 이전에 캐시된 쉐도우맵을 사용하도록하고 다음에 업데이트 하도록 하는 로직입니다. bOverBudget 변수가 true 면 쉐도우맵 캐싱 수 제한을 넘은 것으로 볼 수 있습니다. 이는 이번 프레임에 모든 라이트 들을 대상으로 이번 프레임에 캐시 할 쉐도우맵의 수를 모두 더 했을때 일정값 이상 넘어가는지를 비교하는 것입니다. Spot / Point / Rect light 각각 r.Shadow.MaxNumSpotShadowCacheUpdatesPerFrame, r.Shadow.MaxNumPointShadowCacheUpdatesPerFrame 콘솔명령어로 이 부분을 제어할 수 있으며 Rect light는 Point light와 같은 콘솔 명령어를 사용합니다. 이 부분을 잘 활용하면 쉐도우를 캐스팅하는 라이트가 많은 경우 순간적으로 순간적으로 쉐도우맵 캐싱으로 인한 히칭을 방지할 수 있을 것 같습니다.
    8. 3번에서 본 코드로 캐시 된 쉐도우맵과 현재 쉐도우맵의 설정이 다른 경우 이번 프레임에서만 SDCM_Uncached 방식으로 Static, Movable 모두 렌더링하도록 합니다.
    9. 2번에서 본 코드로, 기존에 생성해둔 캐시된 쉐도우맵이 없다면 추가해주는 부분입니다. TryToCacheShadowMap 함수를 호출하게 되므로 SDCM_StaticPrimitivesOnly -> SDCM_MovablePrimitivesOnly 순서로 렌더링 될 수 있도록 각각의 CacheMode[0], CacheMode[1] 을 설정합니다.
    10. 1번에서 본 코드로, 쉐도우맵 캐시를 지원하지 않으면 매번 SDCM_Uncahed 로 설정해 Static, Dynamic 프리미티브를 매번 새로 그리게 합니다.

    그림11. 쉐도우 캐시모드를 설정함. 쉐도우 캐시를 사용하게 되면 Static 프리미티브만 캐싱하고, Dynamic 프리미티브는 매번 새로그리게 됨.

     

    1. 이제 한동안 다른 작업을 하다가 InitViewsAfterPrepass 함수가 실행됩니다. 여기서 FinishInitDynamicShadows 를 호출하면 쉐도우맵을 렌더링하기 위한 준비가 마무리 됩니다.
    2. FinishInitDynamicShadows 함수에선 쉐도우맵 렌더타겟을 할당합니다. 그리고 DynmaicRelevance인 프리미티브를 MeshBatch로 생성하고 FParallelMeshDrawCommandPass의 DispatchPassSetup 함수를 사용하여 렌더링 할 MeshDrawCommand를 준비시킵니다.
    3. FinishInitDynamicShadows에서 FinishGatherShadowPrimitives 함수는 아무것도 하지 않습니다. 이 함수는 이전 글에서 보듯 Directional light의 CSM에서 사용합니다.
    4. AllocateShadowDepthTargets 함수에서는 캐시 할 쉐도우맵과 이번 프레임에 사용할 쉐도우맵 렌더타겟을 할당합니다.
    5. GatherShadowDynamicMeshElement 는 이전 글에서 본 CSM과 동일합니다. 이 함수에서는 DynamicRelevance인 프리미티브를 MeshBatch로 만들어주고, FParallelMeshDrawCommandPass의 DispatchPassSetup 함수를 사용하여 렌더링 할 MeshDrawCommand를 준비시킵니다. 자세한 내용은 이전 글을 참고해주세요.
    6. 계속해서 AllocateShadowDepthTargets를 봅시다. Spot light의 경우 이번 프레임에 사용할 쉐도우맵 관련 FProjectedShadowInfo는 Shadows 배열에 담기게 됩니다. 그리고 캐시 될 쉐도우맵을 이번 프레임에 렌더링 한다면 CachedSpotlightShadows 배열에 FProjectedShadowInfo를 담습니다.
    7. 모든 라이트를 순회합니다.
    8. 순회 중인 라이트가 가진 FProjectedShadowInfo를 순회합니다. 여기서 만약 이번 프레임에 Static 프리미티브를 캐시 한다면 AllProjectedShadows는 SDCM_MovablePrimitivesOnly, SDCM_StaticPrimitivesOnly 로 총 2개일 것입니다.  그리고 캐시 된 쉐도우맵이 이미 있다면, AllProjectedShadows 배열은 1개의 요소를 가지며 타입은 SDCM_MovablePrimitivesOnly 입니다.
    9. 순회 중인 FProjectedShadowInfo가 특정 View 들 중 하나에 보여지고 있는지 확인합니다. 현재는 View가 1개라고 가정합니다.
    10. 캐시 된 쉐도우맵의 경우 렌더링 할 Static 프리미티브가 없다면 쉐도우맵을 렌더링 하지 않습니다. 이번 프레임에 사용할 SDCM_MovablePrimitivesOnly 쉐도우맵의 경우 이번 프레임에 렌더링 할 Dynamic 프리미티브가 없고, 캐시된 쉐도우맵에도 렌더링 했던 Static 프리미티브가 없는지 확인합니다. 만약 이번 프레임에 사용할 쉐도우맵에 그릴 Dynamic 프리미티브가 없더라도 캐시된 쉐도우맵을 복사할 수도 있습니다. 이 여부에 따라서 쉐도우맵의 렌더링 여부를 최종 결정합니다.
    11. SDCM_MovablePrimitivesOnly 인 경우 ShadowsToProject 배열에 FProjectedShadowInfo를 담습니다.
    12. 캐시 할 Spot light의 쉐도우맵(SDCM_StaticPrimitivesOnly)이면 CachedSpotlightShadows 에 담습니다.
    13. 이번 프레임에 사용할 쉐도우맵(SDCM_MovablePrimitivesOnly)이면 Shadows 에 담습니다.
    14. AllocateCachedShadowDepthTargets은 이번에 캐시 할 쉐도우맵이 있는 경우만 실행됩니다. 그림13 에서 함수 내부를 보겠습니다.
    15. AllocateAtlasedShadowDepthTargets은 이번 프레임에 사용할 쉐도우맵에 대해서 실행됩니다. 그림14에서 함수 내부를 보겠습니다.
    16. 여기서는 캐시 된 쉐도우맵이 사용된지 오래되었다면, 쉐도우맵 렌더타겟을 릴리즈 해줍니다. 캐시 된 쉐도우맵의 ShadowMapData.LastUsedTime 갱신은 ComputeWholeSceneShadowCacheModes 함수에서 합니다. ComputeWholeSceneShadowCacheModes가 수행되려면 라이트가 뷰프러스텀 내로 들어와서 쉐도우맵을 렌더링해야 되는 경우입니다. 이 부분은 InitViewsBeforePrepass -> BeginDynamicShadows 에서 bIsVisibleInAnyView 가 true 인지 여부를 확인하며 코드는 그림9의 7번을 참고해주세요.

    그림12. FProjectedShadowInfo에 대응하는 쉐도우맵의 렌더타겟을 만듭니다.

     

    AllocateCachedShadowDepthTargets 함수의 내부 구현을 봅시다.


    1. 캐시 할 FProjectedShadowInfo를 모두 순회합니다.
    2. 렌더타겟 풀에서 쉐도우맵 렌더타겟을 생성합니다.
    3. 생성한 렌더타겟을 FCachedShadowMapData 에 저장합니다. 이 데이터는 FScene에서 GetCachedShadowMapDataRef에 라이트의 아이디를 넘겨서 다시 얻어올 수 있습니다.
    4. FProjectedShadowInfo 에 이번에 만든 렌더타겟을 전달합니다.
    5. FSortedShadowMapAtlas 클래스에 FProjectedShadowInfo와 생성한 렌더타겟을 담아서 SortedShadowsForShadowDepthPass.ShadowMapAtlases 배열에 저장합니다. 이 배열에 저장된 쉐도우가 추후 쉐도우맵 렌더링에 사용됩니다.

    그림13. AllocateCachedShadowDepthTargets는 캐시할 쉐도우맵 렌더타겟을 생성하는데 사용됨. 캐시되는 쉐도우맵에는 Static 프리미티브만 렌더링함.

     

    AllocateAtlasedShadowDepthTargets 함수의 내부 구현을 봅시다. 이 함수는 이번 프레임에 최종적으로 사용될 쉐도우맵인 SDCM_MovablePrimitivesOnly 타입 FProjectedShadowInfo를 대한 처리입니다.


    1. Layouts 에는 렌더타겟과 FProjectedShadowInfo 의 연관관계를 저장하고 최종적으로 쉐도우맵 렌더타겟을 생성하는 데 사용합니다.
    2. SDCM_MovablePrimitivesOnly 타입인 FProjectedShadowInfo들을 순회합니다.
    3. 만약 SDCM_MovablePrimitivesOnly 인데 렌더링 할 Dynamic 프리미티브가 없다면 그냥 캐시 된 쉐도우맵을 그대로 사용하면 될 것입니다. 그 부분에 대한 코드이며, 캐시된 쉐도우맵을 FProjectedShadowInfo에 추가해줍니다.
    4. 그릴 Dynamic 프리미티브가 있다면 Layout의 Shadow 배열에 ProjectedShadowedInfo를 추가해줍니다.
    5. 준비한 Layouts 를 모두 순회합니다.
    6. 최종 결과를 저장할 FSortedShadowMapAtlas 를 OutAtlas 배열에 하나 추가하고 지금부터 값을 채웁니다.
    7. ShadowMapAtlas가 사용할 쉐도우맵 렌터타겟을 하나 할당합니다.
    8. Layout의 Shadows 를 모두 순회합니다. 이 배열의 요소는 FProjectedShadowInfo 인데, 4번 과정에서 그릴 Dynamic 프리미티브가 있는 경우 추가한 FProjectedShadowInfo 입니다. 만약 그릴 Dynamic 프리미티브가 없다면 쉐도우맵을 렌더링 할 필요가 없으므로 Shadows 배열 크기는 0 일 것입니다.
    9. ShadowMapAtlas.Shadows 배열에 FProjectedShadowInfo를 추가합니다. 그리고 이 FProjectedShadowInfo에 7번 과정에서 만든 쉐도우맵 렌더타겟을 전달합니다. 여기서 알 수 있는 점은 다음과 같습니다.

    • ShadowMapAtlas.Shadows 배열에 있는 FProjectedShadowInfo 만 쉐도우맵에 프리미티브를 렌더링 할 것임.
    • 그릴 Dynamic 프리미티브가 없어서 그냥 캐시 된 쉐도우맵을 사용하는 경우 ShadowMapAtlas.Shadows에 FProjectedShadowInfo가 추가되지 않기 때문에 쉐도우맵에 렌더링 될 과정이 생략될 것임.

    그림14. AllocateAtlasedShadowDepthTargets 는 이번 프레임에 사용할 최종 렌더타겟을 생성하는데 사용됨.

    3.6. Spot light 의 쉐도우맵 렌더링

    마지막 과정인 쉐도우맵 렌더링 과정을 봅시다.
    1. RenderShadowDepthMaps 를 호출합니다. 이 함수는 FDeferredShadingSceneRenderer::Render 함수에서 호출됩니다.
    2. GPUScene에 업로드할 프리미티브가 있다면 갱신합니다.
    3. FInstanceCullingContext 관련 내용입니다. 렌더링 할 인스턴스에 대한 버퍼들을 채우는 부분입니다. 이전에 다룬 적이 있으므로 자세한 내용은 레퍼런스6 를 참고하시면 됩니다.
    4. 실제 Spot light 의 쉐도우맵을 렌더링 하는 함수인 RenderShadowDepthMapAtlases 입니다.
    5. 이전 과정에서 만들어둔 SortedShadowsForShadowDepthPass.ShadowMapAtlases 를 순회합니다. 이 배열에는 FProjectedShadowInfo와 그에 대응하는 렌더타겟이 서로 짝지어 있습니다.
    6. 쉐도우를 병렬로 렌더링 할 수도 있고, 차례로 렌더링 할 수 있도 있는데, 기본 설정은 차례로 렌더링 하는 방식이므로 그 부분의 코드를 보겠습니다. 그림 14 의 3, 4번 과정에서 그릴 Dynamic 프리미티브를 가 있는 경우만 ShadowMapAtlas 의 Shadows 에 FProjectedShadowInfo를 추가하는 것을 볼 수 있었습니다. 만약 Dynamic 프리미티브를 그릴 것이 없다면 캐시 된 쉐도우맵을 사용하고, Shadows 배열에 FProjectedShadowInfo를 추가하지 않았습니다. 여기서는 Shadows 배열에 추가된 FProjectedShadowInfo만 SerialShadowPasses 배열에 담아 렌더링 할 수 있게 준비합니다.
    7. 렌더링 할 FProjectedShadowInfo의 렌더타겟을 클리어 시킵니다.
    8. 렌더링 할 FProjectedShadowInfo의 RenderDepth 함수를 사용하여 쉐도우맵에 렌더링 합니다. 계속해서 RenderDepth의 구현을 봅시다.
    9. SDCM_MovablePrimitivesOnly 인 경우 캐시 된 쉐도우맵을 복사하여 그 위에 Dynamic 프리미티브를 렌더링해줍니다. 그래서 캐시된 쉐도우맵을 먼저 복사합니다. 그 작업을 수행하는 CopyCachedShadowMap의 구현을 초록색 화살표를 따라가 구현을 알아봅시다.
    10. SDCM_MovablePrimitivesOnly 인 경우 캐시된 쉐도우맵을 복사하는 과정이 수행됩니다.
    11. 쉐도우맵 복사 렌더패스가 추가됩니다.
    12. 쉐도우맵 렌더링에 필요한 UniformBuffer를 준비합니다. 이전 글에서 알아봤으므로 함수 내부로 들어가진 않겠습니다.
    13. IntanceCullingContext를 사용한다면 여기서 Indirect draw를 위해서 렌더 커맨드를 준비하거나 인스턴싱을 위한 버퍼를 준비합니다. 이 과정은 GPUSene 에 대한 내용인 레퍼런스6 에서 자세히 다룹니다. 여기서는 최종 렌더링을 위해 준비한다고 생각하고 계속 진행하겠습니다.
    14. 이제 FParallelMeshDrawCommandPass에 쉐도우맵에 렌더링을 요청합니다.

    그림15. Spot light 의 쉐도우맵 렌더링 과정

    지금까지 Spot light의 쉐도우맵 렌더링 과정을 알아봤습니다. Spot light의 경우 CSM과 같이 2D 렌더타겟에 쉐도우를 그리는 점은 동일했습니다. 다른 점은 쉐도우맵에 렌더링할 프리미티브를 수집하는 방식과 쉐도우맵을 캐싱하는 기능이 기본적으로 사용되는 점입니다.

     

    3.7. Point/Rect light의 쉐도우맵 렌더링 준비

    Point/Rect light의 경우 여기서 하나 더 변경사항이 추가됩니다. 쉐도우맵의 렌더타겟을 큐브맵으로 렌더링 하는 것입니다. 계속해서 이 두가지 라이트 타입에 대해 알아봅시다.
     
    코드는 Spot light와 거의 유사합니다. 
     
    BeginInitDynamicShadows 함수는 거의 모두 동일합니다. 다른 점은 그림16 나오듯, FPointLightProxy 를 사용하여 FWholeSceneProjectedShadowInitializer 를 초기화 하는 부분 정도입니다. 그 외에 쉐도우맵을 캐시하는 등의 모든 작업들이 동일합니다.

    그림16. Point light가 FWholeSceneProjectedShadowInitializer를 초기화 하는 과정

    InitViewsAfterPrepass 에서 실행되는 FinishInitDynamicShadows 를 살펴봅시다. FinishGatherShadowPrimitives 는 CSM 쉐도우에서만 사용하므로 무시합니다.
    1. AllocateShadowDepthTargets 는 전방향 라이트에 맞게 Cubemap을 생성합니다. 그림18 에서 확인해봅시다.
    2. GatherShadowDynamicMeshElements 에서는 Dynamc 프리미티브의 MeshBatch를 생성하고 FParallelMeshDrawCommandPass의 DispatchPassSetup 함수를 사용하여 렌더링 할 MeshDrawCommand를 준비시킵니다. 그림19 에서 확인해봅시다.

    그림17. Point light의 FinishInitDynamicShadows 함수 호출

     

    AllocateShadowDepthTargets 함수도 Spot light와 유사한 형태로 진행됩니다. 다른 부분을 위주로 확인해봅시다.


    1. Point light는 Cubemap에 쉐도우맵을 렌더링 합니다. WholeScenePointShadows 배열에 렌더링 할 FProjectedShadowInfo를 저장합니다.
    2. 모든 라이트에 대해서 순회합니다.
    3. WholeScenePointShadows 배열에 FProjectedShadowInfo를 담습니다.
    4. WholeScenePointShadows 를 사용하여 쉐도우맵 렌더타겟을 할당합니다. 함수 내부 구현을 확인하기 위해 초록색 화살표를 따라가 봅시다.
    5. Point light 는 SM5 이상에서 동작하는 것을 확인할 수 있습니다.
    6. WholeScenePointShadows 배열의 모든 FProjectedShadowInfo를 순회합니다.
    7. Spot light와 마찬가지로, 캐시 된 쉐도우맵이 있는 상태이지만 렌더링할 Dynamic 프리미티브는 없는 상태면 그냥 캐시된 쉐도우맵을 사용할 수 있게 준비합니다. 아래 8번에서 볼 ShadowMapCubemap.Shadows 에 ProjectedShadowInfo를 추가하지 않으므로 쉐도우맵을 렌더링하지는 않고 캐시된 쉐도우맵을 그대로 사용합니다.
    8. SortedShadowsForShadowDepthPass.ShadowMapCubemaps 에 Point light의 FProjectedShadowInfo 와 쉐도우맵의 렌더타겟을 담는 것을 볼 수 있습니다. CSM과 Spot light 는 FSortedShadowMapAtlas 배열에 FProjectedShadowInfo와 렌더타겟을 담았는데 Cubemap은 ShadowMapCubemaps 배열에 이런 정보를 저장하는 것을 확인 할 수 있습니다. 그리고 SDCM_StaticPrimitivesOnly 의 경우 캐시된 쉐도우맵에 이번에 생성한 쉐도우맵을 전달합니다. Spot light의 경우 캐시할 FProjectedShadowInfo의 렌더타겟을 생성할 때, AllocateCachedShadowDepthTargets 을 호출했지만 Point light의 경우 그냥 AllocateShadowDepthTargets 함수에서 모두 처리하는 것을 알 수 있습니다.
    9. Point light 도 마찬가지로 라이트가 View에서 보여지지 않는다면 쉐도우맵 렌더타겟을 릴리즈 합니다.

    그림18. Point light의 AllocateShadowDepthTargets 함수 호출

    GatherShadowDynamicMeshElements 에서는 이전에 봤던 과정과 동일하게 Dynamic 프리미티브의 MeshBatch를 생성하고 ParallelMeshDrawCommandPass의 DispatchPassSetup 함수를 사용하여 렌더링 할 MeshDrawCommand를 준비시킵니다. 이전 쉐도우와 다른 점은 SortedShadowsForShadowDepthPass.ShadowMapAtlases 가 아닌 SortedShadowsForShadowDepthPass.ShadowMapCubemaps 에 있는 FProjectedShadowInfo의 함수를 호출하는 것입니다.

    그림19. Point light의 MeshBatch 생성 과정

     

    3.8. Point/Rect light의 쉐도우맵 렌더링

    마지막으로 Point/Rect light를 렌더링 하는 부분입니다.
    1. SortedShadowsForShadowDepthPass.ShadowMapCubemaps 배열의 모든 FProjectedShadowInfo 를 대상으로 GPUScene에 업로드할 프리미티브가 있다면 갱신합니다.
    2. 렌더링 할 FProjectedShadowInfo 들을 순회합니다.
    3. 쉐도우맵의 클리어 패스를 추가합니다. 쉐도우맵을 캐싱하는 경우나 혹은 캐싱된 쉐도우맵을 사용하지 않는 경우만 렌더타겟을 클리어 합니다.
    4. 쉐도우맵을 렌더링합니다. 이 함수의 내부는 그림21 에서 확인해봅시다.

    그림20. Point light의 쉐도우맵 렌더링을 위한 RenderShadowDepthMaps 함수

    계속해서 쉐도우맵을 렌더링하는 부분입니다.
    1. 캐싱된 쉐도우맵을 사용해야하는 경우 쉐도우맵을 복사합니다.
    2. 쉐도우맵의 UniformBuffer를 설정합니다. 초록색 화살표를 따라가 함수 내부를 확인해봅시다.
    3. bOnPassPointLightShadow 여부를 확인하고 Cubemap의 6면에 대해서 변환 매트릭스를 설정하는 부분이 있습니다. 이름 그대로 Cubemap 의 최대 6면을 한 번에 렌더링 하는 기능을 사용합니다. 이 부분은 그림22 에서 조금 더 알아볼 예정입니다.
    4. MeshDrawCommand를 실행하여 쉐도우맵에 렌더링 합니다.

    그림21. Point light의 FProjectedShadowInfo->RenderDepth 함수

    마지막으로 Point light 쉐도우맵을 렌더링 하는 쉐이더를 보고 OnePassPointLight가 어떻게 동작하는지 확인해봅시다. 다른 light의 경우 특별한 것이 없으므로 Point light만 확인해보려고 합니다.


    1. OnPassPointLight를 사용하는 경우 ONEPASS_POINTLIGHT_SHADOW Preprocessor를 추가합니다.
    2. 실제 쉐이더를 보면 ONEPASS_POINTLIGHT_SHADOW 의 경우, LayerId와 버택스 쉐이더의 출력으로 uint LayerIndex : SV_RenderTargetArrayIndex(레퍼런스7 참고)를 전달하도록 합니다. 이 LayerIndex 는 Array 타입인 RenderTarget(CubeMap 포함)의 어느 위치에 기록할 것인지를 결정합니다. 최대 6면을 동시에 렌더링 할 수 있어야 하는데, 이런 부분은 인스턴싱을 사용하여 처리합니다. 인스턴싱 데이터는 3 번과정에서 볼 ViewIndex 정보에 담겨있으면서 이 부분을 통해 LayerIndex 를 변경시켜 여러개의 Face 에 동시에 렌더링 합니다.
    3. LayerIndex 를 설정해주고, LayerIndex를 기반으로 WorldPos 을 변환시킵니다. LayerIndex 는 ViewIndex 로 부터 얻어오는데 이 부분은 VFIntermediates.SceneData.ViewIndex 로 부터 정보를 얻어 옵니다. 이 데이터는 인스턴싱 데이터이며 현재 렌더링 될 메시가 렌더링될 Face (CubeMap 0~6 사이 값) 에 대해서만 인스턴싱 데이터를 생성합니다.
    4. 렌더독의 ShadowDepths 렌더 패스의 렌더커맨드 입니다.
    5. uint LayerIndex : SV_RenderTargetArrayIndex 정보를 인스턴싱 데이터 사용하여 채우기 때문에 지오메트리 쉐이더는 활용하지 않습니다. 인스턴싱 데이터를 생성하는 부분은 InstanceCullingContext 에서 처리되며 메시 당 렌더링될 인스턴스의 수도 각 Face 의 Frustum 에 따라서 결정됩니다.

    그림22. OnPassPointLightShadow 를 사용하여 Cubemap 6면을 동시에 렌더링하는 기능 확인

     

    여기까지가 DynamicShadow 를 쉐도우맵에 렌더링 하는 과정입니다.

     

    4. 레퍼런스

    1. Unreal Engine 5 (5.0 branch 2170f78d3d94abd7b24b7d6d5c5104cfb43245ea)
    2. [UE5] Dynamic Shadow (1/2) 
    3. [UE5] MeshDrawCommand (1/2)
    4. [UE5] MeshDrawCommand (2/2)
    5. [UE5] GPUScene 과 Auto-Instancing (1/2)
    6. [UE5] GPUScene 과 Auto-Instancing (2/2)
    7. Semactics

     

     

     

     
     
     

    댓글

Designed by Tistory & scahp.