Skip to content

언리얼 렌더링 시스템 분석(06) - UE5 특별 2부(루멘 및 기타)

🌐 원문 링크: 剖析虚幻渲染体系(06)- UE5特辑Part 2(Lumen和其它) (cnblogs.com/timlly)
📅 원문 발행일: 2021-07-13 | ✍️ 저자: Timlly (00 / )

💡 시리즈 분류: Unreal Engine Rendering국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료


Lumen은 두 가지 레이 트레이싱 모드를 지원합니다.

  1. 소프트웨어 레이 트레이싱. 가장 광범위한 하드웨어 및 플랫폼에서 실행됩니다.

  2. 하드웨어 레이 트레이싱. 그래픽 카드 및 운영 체제 지원이 필요합니다.

  • 소프트웨어 레이 트레이싱

Lumen은 기본적으로 지향성 거리 필드에 의존하는 소프트웨어 레이 트레이싱을 사용합니다. 즉, SM5를 지원하는 하드웨어에서 실행할 수 있습니다.

프로젝트 설정에서 메시 디스턴스 필드 생성을 켜야 합니다. UE5에서는 기본적으로 켜져 있습니다.

렌더러는 메시의 디스턴스 필드를 글로벌 디스턴스 필드로 병합하여 추적 속도를 높입니다. 기본적으로 Lumen은 각 그리드 거리 필드의 처음 2미터의 정확도를 추적하고 다른 거리의 광선은 결합된 전역 거리 필드를 사용합니다. 프로젝트에 Lumen 소프트 레이 트레이싱의 정밀한 제어가 필요한 경우 프로젝트 설정에서 소프트웨어 레이 트레이싱 모드 방법을 사용할 수 있습니다.

세부 추적은 별도의 그리드 거리 필드를 사용하여 고품질 GI를 달성할 수 있는 기본 추적 방법입니다(처음 2미터에만 사용되며 다른 거리에는 전역 거리 필드가 사용됨). Global Tracing은 빠른 추적을 위해 전역 거리 필드를 사용하지만 특정 이미지 품질 효과를 잃습니다.

그리드 디스턴스 필드는 월드의 카메라 움직임에 따라 동적으로 스트리밍되고 로드되거나 언로드됩니다. 이는 아틀라스(Atlas)로 패키징되며, r.DistanceFields.LogAtlasStats 1 콘솔 명령을 통해 정보를 출력할 수 있습니다.

Lumen의 소프트 라이트 추적 품질은 그리드 거리 필드에 크게 의존하므로 그리드 거리 필드의 품질에 주의를 기울이면 Lumen의 GI 효과를 향상시킬 수 있습니다. 아래 그림은 실제 그리드 거리 필드와 전역 거리 필드의 메뉴입니다.

다음 두 그림은 각각 그리드 거리 필드와 전역 거리 필드를 시각화한 것입니다.

그러나 소프트웨어 레이 트레이싱에는 주로 다음과 같은 많은 제한 사항이 있습니다.

  • 기하학적 개체 제한 사항:

루멘 장면은 정적 메쉬, 인스턴스화된 정적 메쉬 및 계층적 인스턴스화된 정적 메쉬(Hierarchical Instanced Static Meshe)만 지원합니다.

  • 랜드스케이프 기하학은 지원되지 않으므로 간접광 반사가 없습니다. 앞으로도 지원될 예정입니다.

  • 자재 제한사항:

WPO(World Position Offset)는 지원되지 않습니다.

  • 투명한 객체는 지원되지 않으며 마스크된 객체는 불투명한 객체로 처리됩니다.

  • 디스턴스 필드 데이터는 오버라이드 컴포넌트가 아닌 정적 메시 자산의 머티리얼 속성을 기반으로 구성됩니다. 즉, 런타임 시 재질을 변경해도 Lumen의 GI에는 영향을 미치지 않습니다.

  • 작업흐름 제한사항:

소프트웨어 레이 트레이싱을 사용하려면 계층 구조가 모듈로 구성되어야 합니다. 벽, 바닥, 천장은 별도의 그리드여야 합니다. 더 큰 메시(예: 산)는 성능이 저하되고 자체 폐색 의사 그림자가 발생할 수 있습니다.

  • 빛샘을 방지하기 위해 벽은 10cm 이상 커야 합니다.

  • 디스턴스 필드의 해상도는 스태틱 메시를 임포트할 때의 설정에 따라 달라집니다. 압축률이 너무 높으면 고품질 거리 필드 데이터를 얻을 수 없습니다.

  • 디스턴스 필드는 매우 얇은 물체를 표현할 수 없습니다.

위에서 Lumen의 소프트웨어 Ray Tracing에 대해 설명했습니다. 하드웨어 레이 트레이싱을 계속 소개하겠습니다.

  • 하드웨어 레이 트레이싱

하드웨어 레이 트레이싱은 소프트웨어 레이 트레이싱보다 더 넓은 범위의 기하학적 객체 유형을 지원하며, 특히 스킨 메쉬 추적을 지원합니다. 하드웨어 레이 트레이싱은 더 높은 화질을 달성하는 데에도 더 좋습니다. 즉, 낮은 품질의 표면 캐시와 달리 실제 삼각형을 교차하고 광선이 닿는 지점의 조명을 선택적으로 평가합니다.

그러나 하드웨어 레이 트레이싱의 장면 설정 비용은 매우 높으며 현재 인스턴스가 100,000개가 넘는 장면으로 확장할 수 없습니다. 동적으로 변형되는 메시(예: 스킨 처리된 메시)는 매 프레임마다 레이 트레이싱 가속 구조를 업데이트하는 데 상당한 비용이 발생하며, 이 비용은 스킨 처리된 삼각형의 수에 비례합니다.

Nanite를 사용하는 정적 메쉬의 경우 하드웨어 레이 트레이싱은 렌더링 효율성을 위해 정적 메쉬 편집기 설정에서 Nanite의 Proxy Triangle Percent에 의해 생성된 프록시 메쉬(프록시 메쉬)에서만 작동할 수 있습니다. 이 프록시 메시는 r.Nanite 0|1 콘솔 명령을 통해 시각화를 위해 켜고 끌 수 있습니다:

상단: 매우 정밀한 디테일을 갖춘 삼각형 메시; 하단: 해당 Nanite 프록시 메시.

스크린 추적은 Nanite로 렌더링된 완전 정밀도 삼각형 메시와 Lumen 레이 트레이싱 프록시 메시 간의 불일치를 가리는 데 사용됩니다. 그러나 어떤 경우에는 불일치가 너무 커서 은폐할 수 없습니다. 위 두 그림은 Proxy Triangle Percent 값이 너무 작기 때문에 자체 그림자 결함으로 인해 발생합니다.

Lumen은 다음 조건이 충족되는 경우에만 하드웨어 레이 트레이싱을 활성화합니다.

  • 가능한 경우 하드웨어 레이 트레이싱 사용하드웨어 레이 트레이싱 지원이 프로젝트 설정에서 활성화됩니다.

  • 프로젝트는 지원되는 운영 체제, RHI 및 그래픽 카드에서 실행됩니다. 현재 다음 플랫폼에서만 하드웨어 Ray Tracing을 지원합니다.

DirectX 12를 사용하는 Windows 10.

  • 플레이스테이션 5.

  • Xbox 시리즈 S/X.

  • 그래픽 카드는 NVIDIA RTX-2000 시리즈 이상 또는 AMD RX 6000 시리즈 이상이어야 합니다.

6.5.1.4 Lumen에 대한 기타 지침

루멘 씬은 전체 월드가 아닌 카메라 근처의 월드에서 실행되어 대규모 월드와 스트리밍 데이터가 가능합니다. Lumen은 Nanite의 LOD 및 다중 뷰 래스터라이제이션를 사용하여 장면을 빠르게 캡처하고 표면 캐시를 유지하며 모든 작업을 제어하여 오류를 방지합니다. Lumen은 Nanite 작동을 요구하지 않지만 Nanite가 활성화되지 않은 장면에서는 Lumen의 장면 캡처 속도가 매우 느려집니다. 자산의 LOD 설정이 좋지 않은 경우 특히 그렇습니다.

Lumen의 Surface Cache는 카메라에서 200m 떨어진 위치를 커버합니다. 이 범위 이후에는 전역 조명에 대해 화면 추적만 켜집니다.

또한 Lumen에는 다음과 같은 다른 제한 사항도 있습니다.

  • 루멘 전역 조명은 라이트맵과 함께 사용할 수 없습니다. 앞으로는 Lumen의 반사가 라이트맵과 함께 전역 조명을 사용하도록 확장되어 렌더링 품질이 더욱 향상될 것입니다.

  • 다운샘플링된 렌더링 및 임시 필터에 대한 의존도가 높기 때문에 식물은 아직 제대로 지원되지 않습니다.

  • Lumen의 Final Gather는 움직이는 물체 주위에 상당한 소음을 추가하며 아직 개발 중입니다.

  • 투명 재질은 아직 루멘 반사를 지원하지 않습니다.

  • 투명한 재질에는 고품질의 동적 GI가 없습니다.

다음은 Lumen 관련 디버깅 또는 시각화 정보입니다.

상단: 일반 화면; 중간: 루멘 장면 시각화; 하단: 루멘 GI 시각화.

물론 위에 표시된 여러 시각화 옵션 외에도 Lumen에는 실제로 다른 많은 시각적 제어 명령이 있습니다.

cpp
r.Lumen.RadianceCache.Visualize    
r.Lumen.RadianceCache.VisualizeClipmapIndex
r.Lumen.RadianceCache.VisualizeProbeRadius
r.Lumen.RadianceCache.VisualizeRadiusScale

r.Lumen.ScreenProbeGather.VisualizeTraces    
r.Lumen.ScreenProbeGather.VisualizeTracesFreeze

r.Lumen.Visualize.CardInterpolateInfluenceRadius    
r.Lumen.Visualize.CardPlacement
r.Lumen.Visualize.CardPlacementDistance    
r.Lumen.Visualize.CardPlacementIndex    
r.Lumen.Visualize.CardPlacementOrientation    
r.Lumen.Visualize.ClipmapIndex    
r.Lumen.Visualize.ConeAngle    
r.Lumen.Visualize.ConeStepFactor
r.Lumen.Visualize.GridPixelSize    
r.Lumen.Visualize.HardwareRayTracing
r.Lumen.Visualize.HardwareRayTracing.DeferredMaterial    
r.Lumen.Visualize.HardwareRayTracing.DeferredMaterial.TileDimension
r.Lumen.Visualize.HardwareRayTracing.LightingMode
r.Lumen.Visualize.HardwareRayTracing.MaxTranslucentSkipCount
r.Lumen.Visualize.MaxMeshSDFTraceDistance
r.Lumen.Visualize.MaxTraceDistance    
r.Lumen.Visualize.MinTraceDistance    
r.Lumen.Visualize.Stats    
r.Lumen.Visualize.TraceMeshSDFs    
r.Lumen.Visualize.TraceRadianceCache
r.Lumen.Visualize.VoxelFaceIndex
r.Lumen.Visualize.Voxels
r.Lumen.Visualize.VoxelStepFactor    

ShowFlag.LumenGlobalIllumination
ShowFlag.LumenReflections
ShowFlag.VisualizeLumenIndirectDiffuse
ShowFlag.VisualizeLumenScene

그 외에도 많은 제어 명령이 있습니다. 다음은 일부 명령을 보여줍니다.

cpp
r.Lumen.DiffuseIndirect.Allow
r.Lumen.DiffuseIndirect.CardInterpolateInfluenceRadius
r.Lumen.DiffuseIndirect.CardTraceEndDistanceFromCamera    

r.Lumen.DirectLighting    
r.Lumen.DirectLighting.BatchSize    
r.Lumen.DirectLighting.CardUpdateFrequencyScale    

r.Lumen.HardwareRayTracing
r.Lumen.HardwareRayTracing.PullbackBias
r.Lumen.IrradianceFieldGather
r.Lumen.IrradianceFieldGather.ClipmapDistributionBase
r.Lumen.IrradianceFieldGather.ClipmapWorldExtent

r.Lumen.MaxConeSteps
r.Lumen.MaxTraceDistance
r.Lumen.ProbeHierarchy
r.Lumen.ProbeHierarchy.AdditionalSpecularRayThreshold
r.Lumen.ProbeHierarchy.AntiTileAliasing

r.Lumen.RadianceCache.DownsampleDistanceFromCamera
r.Lumen.RadianceCache.ForceFullUpdate    
r.Lumen.RadianceCache.NumFramesToKeepCachedProbes    

r.Lumen.Radiosity    
r.Lumen.Radiosity.CardUpdateFrequencyScale    
r.Lumen.Radiosity.ComputeScatter    
r.Lumen.Radiosity.ConeAngleScale

r.Lumen.Reflections.Allow
r.Lumen.Reflections.DownsampleFactor    
r.Lumen.Reflections.GGXSamplingBias    
r.Lumen.Reflections.HardwareRayTracing
r.Lumen.Reflections.HardwareRayTracing.DeferredMaterial

r.Lumen.Reflections.HierarchicalScreenTraces.UncertainTraceRelativeDepthThreshold
r.Lumen.Reflections.MaxRayIntensity
r.Lumen.Reflections.MaxRoughnessToTrace    
r.Lumen.Reflections.RoughnessFadeLength    
r.Lumen.Reflections.ScreenSpaceReconstruction

r.Lumen.Reflections.ScreenTraces
r.Lumen.Reflections.Temporal
r.Lumen.Reflections.Temporal.DistanceThreshold
r.Lumen.Reflections.Temporal.HistoryWeight
r.Lumen.Reflections.TraceMeshSDFs

r.Lumen.ScreenProbeGather
r.Lumen.ScreenProbeGather.AdaptiveProbeAllocationFraction
r.Lumen.ScreenProbeGather.AdaptiveProbeMinDownsampleFactor
r.Lumen.ScreenProbeGather.DiffuseIntegralMethod
r.Lumen.ScreenProbeGather.DownsampleFactor
r.Lumen.ScreenProbeGather.FixedJitterIndex
r.Lumen.ScreenProbeGather.FullResolutionJitterWidth
r.Lumen.ScreenProbeGather.GatherNumMips
r.Lumen.ScreenProbeGather.GatherOctahedronResolutionScale
r.Lumen.ScreenProbeGather.HardwareRayTracing

r.Lumen.ScreenProbeGather.ImportanceSample.ProbeRadianceHistory
r.Lumen.ScreenProbeGather.MaxRayIntensity
r.Lumen.ScreenProbeGather.OctahedralSolidAngleTextureSize
r.Lumen.ScreenProbeGather.RadianceCache
r.Lumen.ScreenProbeGather.RadianceCache.ClipmapDistributionBase

r.Lumen.ScreenProbeGather.ReferenceMode
r.Lumen.ScreenProbeGather.ScreenSpaceBentNormal
r.Lumen.ScreenProbeGather.ScreenTraces
r.Lumen.ScreenProbeGather.ScreenTraces.HZBTraversal

r.Lumen.ScreenProbeGather.SpatialFilterHalfKernelSize    Experimental
r.Lumen.ScreenProbeGather.SpatialFilterMaxRadianceHitAngle

r.Lumen.ScreenProbeGather.Temporal    
r.Lumen.ScreenProbeGather.Temporal.ClearHistoryEveryFrame    

r.Lumen.ScreenProbeGather.TraceMeshSDFs    
r.Lumen.ScreenProbeGather.TracingOctahedronResolution
r.Lumen.TraceMeshSDFs
r.Lumen.TraceMeshSDFs.Allow    
r.Lumen.TranslucencyVolume.ConeAngleScale    
r.Lumen.TranslucencyVolume.Enable    
r.Lumen.TranslucencyVolume.EndDistanceFromCamera    

r.LumenParallelBeginUpdate
r.LumenScene.CardAtlasAllocatorBinSize    
r.LumenScene.CardAtlasSize    
r.LumenScene.CardCameraDistanceTexelDensityScale
r.LumenScene.CardCaptureMargin

r.LumenScene.ClipmapResolution    
r.LumenScene.ClipmapWorldExtent    
r.LumenScene.ClipmapZResolutionDivisor    
r.LumenScene.DiffuseReflectivityOverride    
r.LumenScene.DistantScene
r.LumenScene.DistantScene.CardResolution    

r.LumenScene.FastCameraMode
r.LumenScene.GlobalDFClipmapExtent    
r.LumenScene.GlobalDFResolution    
r.LumenScene.HeightfieldSlopeThreshold    
r.LumenScene.MaxInstanceAddsPerFrame
r.LumenScene.MeshCardsCullFaces    
r.LumenScene.MeshCardsMaxLOD

r.LumenScene.NaniteMultiViewCapture    
r.LumenScene.NumClipmapLevels    
r.LumenScene.PrimitivesPerPacket
r.LumenScene.RecaptureEveryFrame    
r.LumenScene.Reset
r.LumenScene.UploadCardBufferEveryFrame    
r.LumenScene.VoxelLightingAverageObjectsPerVisBufferTile

r.SSGI.AllowStandaloneLumenProbeHierarchy
r.Water.SingleLayer.LumenReflections

Lumen과 관련된 수백 가지 콘솔 명령이 있는데, 이는 Lumen 렌더링이 얼마나 복잡한지 보여줍니다! !

6.5.2 루멘 렌더링 기본 사항

이 섹션에서는 루멘과 관련된 기본 개념과 종류에 대해 설명합니다.

6.5.2.1 FLumenCard

FLumenCard는 이전 섹션에서 언급한 카드이며 FLumenMeshCard의 기본 구성 요소입니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneData.h

// Lumen타입/유형 。
class FLumenCard
{
public:
    FLumenCard();
    ~FLumenCard();

    // 의 바운딩 박스(AABB).
    FBox WorldBounds;
    // 회전 정보 .
    FVector LocalToWorldRotationX;
    FVector LocalToWorldRotationY;
    FVector LocalToWorldRotationZ;
    // 위치 .
    FVector Origin;
    // 로컬 의 바운딩 박스(AABB).
    FVector LocalExtent;
    
    // 는 .
    bool bVisible = false;
    // 는 .
    bool bDistantScene = false;

    // 의 정보 .
    bool bAllocated = false;
    FIntPoint DesiredResolution;
    FIntRect AtlasAllocation;

    //
    int32 Orientation = -1;
    // 리스트 의 .
    int32 IndexInVisibleCardIndexBuffer = -1;
    // 의 FLumenMeshCards의 Card리스트 의 .
    int32 IndexInMeshCards = -1;
    // 의 FLumenMeshCards의 .
    int32 MeshCardsIndex = -1;
    // 해상도 스케일 .
    float ResolutionScale = 1.0f;

    // 초기화(Initialize)
    void Initialize(float InResolutionScale, const FMatrix& LocalToWorld, const FLumenCardBuildData& CardBuildData, int32 InIndexInMeshCards, int32 InMeshCardsIndex);

    // 설정(Set)변환(Transform)데이터
    void SetTransform(const FMatrix& LocalToWorld, FVector CardLocalCenter, FVector CardLocalExtent, int32 InOrientation);
    void SetTransform(const FMatrix& LocalToWorld, const FVector& LocalOrigin, const FVector& CardToLocalRotationX, const FVector& CardToLocalRotationY, const FVector& CardToLocalRotationZ, const FVector& InLocalExtent);

    // 로부터 () 내에서 .
    void RemoveFromAtlas(FLumenSceneData& LumenSceneData);

    int32 GetNumTexels() const
    {
        return AtlasAllocation.Area();
    }

    inline FVector TransformWorldPositionToCardLocal(FVector WorldPosition) const
    {
        FVector Offset = WorldPosition - Origin;
        return FVector(Offset | LocalToWorldRotationX, Offset | LocalToWorldRotationY, Offset | LocalToWorldRotationZ);
    }

    inline FVector TransformCardLocalPositionToWorld(FVector CardPosition) const
    {
        return Origin + CardPosition.X * LocalToWorldRotationX + CardPosition.Y * LocalToWorldRotationY + CardPosition.Z * LocalToWorldRotationZ;
    }
};

6.5.2.2 FLumenMeshCard

FLumenMeshCard는 Surface Cache를 계산하기 위한 기본 요소이자 Lumen Scene을 구성하는 기본 단위입니다. 최대 6개의 면(방향)에 대한 FLumenCard 정보를 저장할 수 있으며, 각 방향은 0~N FLumenCard 정보(NumCardsPerOrientation으로 지정)를 저장할 수 있습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenMeshCards.h

class FLumenMeshCards
{
public:
    // 초기화(Initialize).
    void Initialize(
        const FMatrix& InLocalToWorld, 
        const FBox& InBounds,
        uint32 InFirstCardIndex,
        uint32 InNumCards,
        uint32 InNumCardsPerOrientation[6],
        uint32 InCardOffsetPerOrientation[6])
    {
        Bounds = InBounds;
        SetTransform(InLocalToWorld);
        FirstCardIndex = InFirstCardIndex;
        NumCards = InNumCards;

        for (uint32 OrientationIndex = 0; OrientationIndex < 6; ++OrientationIndex)
        {
            NumCardsPerOrientation[OrientationIndex] = InNumCardsPerOrientation[OrientationIndex];
            CardOffsetPerOrientation[OrientationIndex] = InCardOffsetPerOrientation[OrientationIndex];
        }
    }

    // 설정(Set)변환(Transform)모멘트(Moment).
    void SetTransform(const FMatrix& InLocalToWorld)
    {
        LocalToWorld = InLocalToWorld;
    }

    // 로컬 까지 의 모멘트(Moment).
    FMatrix LocalToWorld;
    // 로컬 바운딩 박스(AABB).
    FBox Bounds;

    // 개 FLumenCard.
    uint32 FirstCardIndex = 0;
    // FLumenCard개수 .
    uint32 NumCards = 0;
    // 6개 의 FLumenCard개수 .
    uint32 NumCardsPerOrientation[6];
    // 6개 의 FLumenCard.
    uint32 CardOffsetPerOrientation[6];
};

6.5.2.3 FLumenSceneData

FLumenSceneData는 Lumen의 전역 조명 구현을 나타내는 장면입니다. Nanite의 고정밀 메쉬를 사용하지 않고 FLumenCard와 FLumenMeshCard를 기본 요소로 한 대략적인 장면을 사용합니다. 그 정의와 관련 유형은 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneData.h

// Lumen인스턴스
class FLumenPrimitiveInstance
{
public:
    FBox WorldSpaceBoundingBox;
    // FLumenMeshCards.
    int32 MeshCardsIndex;
    bool bValidMeshCards;
};

// Lumen
class FLumenPrimitive
{
public:
    // 바운딩 박스(AABB).
    FBox WorldSpaceBoundingBox;
    // 의 FLumenMeshCards의 바운딩 박스(AABB), 을(를) 활용하여 컬링 .
    float MaxCardExtent;

    // 인스턴스 리스트 .
    TArray<FLumenPrimitiveInstance, TInlineAllocator<1>> Instances;

    // 의 의 정보 .
    FPrimitiveSceneInfo* Primitive = nullptr;

    // 는 의 인스턴스 .
    bool bMergedInstances = false;
    // 해상도 스케일 .
    float CardResolutionScale = 1.0f;
    // FLumenMeshCards의 개수 .
    int32 NumMeshCards = 0;

    // 매핑 까지 LumenDFInstanceToDFObjectIndex.
    uint32 LumenDFInstanceOffset = UINT32_MAX;
    int32 LumenNumDFInstances = 0;

    // 페치/가져오기(Fetch)FLumenMeshCards.
    int32 GetMeshCardsIndex(int32 InstanceIndex) const
    {
        if (bMergedInstances)
        {
            return Instances[0].MeshCardsIndex;
        }

        if (InstanceIndex < Instances.Num())
        {
            return Instances[InstanceIndex].MeshCardsIndex;
        }

        return -1;
    }
};

// Lumen데이터 .
class FLumenSceneData
{
public:
    int32 Generation;

    // GPU의 버퍼 .
    FScatterUploadBuffer CardUploadBuffer;
    FScatterUploadBuffer UploadMeshCardsBuffer;
    FScatterUploadBuffer ByteBufferUploadBuffer;
    FScatterUploadBuffer UploadPrimitiveBuffer;

    FUniqueIndexList CardIndicesToUpdateInBuffer;
    FRWBufferStructured CardBuffer;

    TArray<FBox> PrimitiveModifiedBounds;

    // Lumen의 Lumen.
    TArray<FLumenPrimitive> LumenPrimitives;

    // FLumenMeshCards데이터 .
    FUniqueIndexList MeshCardsIndicesToUpdateInBuffer;
    TSparseSpanArray<FLumenMeshCards> MeshCards;
    TSparseSpanArray<FLumenCard> Cards;
    TArray<int32, TInlineAllocator<8>> DistantCardIndices;
    FRWBufferStructured MeshCardsBuffer;
    FRWByteAddressBuffer DFObjectToMeshCardsIndexBuffer;

    // 로부터 매핑 까지 LumenDFInstance.
    FUniqueIndexList PrimitivesToUpdate;
    FRWByteAddressBuffer PrimitiveToDFLumenInstanceOffsetBuffer;
    uint32 PrimitiveToLumenDFInstanceOffsetBufferSize = 0;

    // 로부터 LumenDFInstance매핑 까지 DFObjectIndex
    FUniqueIndexList DFObjectIndicesToUpdateInBuffer;
    FUniqueIndexList LumenDFInstancesToUpdate;
    TSparseSpanArray<int32> LumenDFInstanceToDFObjectIndex;
    FRWByteAddressBuffer LumenDFInstanceToDFObjectIndexBuffer;
    uint32 LumenDFInstanceToDFObjectIndexBufferSize = 0;

    // 의 FLumenMeshCards리스트 .
    TArray<int32> VisibleCardsIndices;
    TRefCountPtr<FRDGPooledBuffer> VisibleCardsIndexBuffer;

    // --- 로부터 내에서 의 데이터 ---
    TRefCountPtr<IPooledRenderTarget> AlbedoAtlas;
    TRefCountPtr<IPooledRenderTarget> NormalAtlas;
    TRefCountPtr<IPooledRenderTarget> EmissiveAtlas;

    // --- 의 데이터 ---
    TRefCountPtr<IPooledRenderTarget> DepthAtlas;
    TRefCountPtr<IPooledRenderTarget> FinalLightingAtlas;
    TRefCountPtr<IPooledRenderTarget> IrradianceAtlas;
    TRefCountPtr<IPooledRenderTarget> IndirectIrradianceAtlas;
    TRefCountPtr<IPooledRenderTarget> RadiosityAtlas;
    TRefCountPtr<IPooledRenderTarget> OpacityAtlas;

    // 데이터 .
    bool bFinalLightingAtlasContentsValid;
    FIntPoint MaxAtlasSize;
    FBinnedTextureLayout AtlasAllocator;
    int32 NumCardTexels = 0;
    int32 NumMeshCardsToAddToSurfaceCache = 0;

    // 데이터 .
    bool bTrackAllPrimitives;
    TSet<FPrimitiveSceneInfo*> PendingAddOperations;
    TSet<FPrimitiveSceneInfo*> PendingUpdateOperations;
    TArray<FLumenPrimitiveRemoveInfo> PendingRemoveOperations;

    FLumenSceneData(EShaderPlatform ShaderPlatform, EWorldType::Type WorldType);
    ~FLumenSceneData();

    // .
    void AddPrimitiveToUpdate(int32 PrimitiveIndex);
    void AddPrimitive(FPrimitiveSceneInfo* InPrimitive);
    void UpdatePrimitive(FPrimitiveSceneInfo* InPrimitive);
    void RemovePrimitive(FPrimitiveSceneInfo* InPrimitive, int32 PrimitiveIndex);

    // FLumenMeshCards.
    void AddCardToVisibleCardList(int32 CardIndex);
    void RemoveCardFromVisibleCardList(int32 CardIndex);
    void AddMeshCards(int32 LumenPrimitiveIndex, int32 LumenInstanceIndex);
    void UpdateMeshCards(const FMatrix& LocalToWorld, int32 MeshCardsIndex, const FMeshCardsBuildData& MeshCardsBuildData);
    void RemoveMeshCards(FLumenPrimitive& LumenPrimitive, FLumenPrimitiveInstance& LumenPrimitiveInstance);

    bool HasPendingOperations() const
    {
        return PendingAddOperations.Num() > 0 || PendingUpdateOperations.Num() > 0 || PendingRemoveOperations.Num() > 0;
    }

    void UpdatePrimitiveToDistanceFieldInstanceMapping(FScene& Scene, FRHICommandListImmediate& RHICmdList);

private:
    // 로부터 데이터 추가(Add)FLumenMeshCards.
    int32 AddMeshCardsFromBuildData(const FMatrix& LocalToWorld, const FMeshCardsBuildData& MeshCardsBuildData, float ResolutionScale);
};

FLumenSceneData는 FLumenMeshCard와 기본 FLumenPrimitive 및 FLumenMeshCard를 기반으로 하는 기본 인스턴스 FLumenPrimitiveInstance를 저장하는 것을 볼 수 있습니다. 각 FLumenPrimitive는 여러 개의 FLumenMeshCard를 저장하고 FPrimitiveSceneInfo 포인터도 저장하여 실제 세계에서 대략적으로 표현되는 FPrimitiveSceneInfo를 나타냅니다.

6.5.3 루멘 데이터 구성

Lumen은 렌더링하기 전에 메시 디스턴스 필드, 글로벌 디스턴스 필드 및 MeshCard 생성을 포함하여 많은 데이터 구성을 수행합니다.

처음 Lumen 프로젝트를 시작하면 그리드 거리 필드 등을 포함하여 많은 데이터가 구축됩니다.

6.5.3.1 카드 표현

그리드 카드 표현을 구축하기 위해 UE5는 MeshCardRepresentation 모듈을 독립적으로 개발했습니다. 그 핵심 개념과 종류는 다음과 같습니다.

cpp
// Engine\Source\Runtime\Engine\Public\MeshCardRepresentation.h

// FLumenCard데이터
class FLumenCardBuildData
{
public:
    // 내에서 및 바운딩 박스(AABB).
    FVector Center;
    FVector Extent;

    // : -X, +X, -Y, +Y, -Z, +Z
    int32 Orientation;
    int32 LODLevel;

    // 에 따라 회전 Extent.
    static FVector TransformFaceExtent(FVector Extent, int32 Orientation)
    {
        if (Orientation / 2 == 2) // : -Z, +Z
        {
            return FVector(Extent.Y, Extent.X, Extent.Z);
        }
        else if (Orientation / 2 == 1) // : -Y, +Y
        {
            return FVector(Extent.Z, Extent.X, Extent.Y);
        }
        else // (Orientation / 2 == 0), : -X, +X
        {
            return FVector(Extent.Y, Extent.Z, Extent.X);
        }
    }
};

// FLumenMeshCards데이터 .
class FMeshCardsBuildData
{
public:
    FBox Bounds;
    int32 MaxLODLevel;
    // FLumenCard데이터 리스트 .
    TArray<FLumenCardBuildData> CardBuildData;

    (......)
};

// 개 테이블 데이터 인스턴스 의 id。
class FCardRepresentationDataId
{
public:
    uint32 Value = 0;

    bool IsValid() const
    {
        return Value != 0;
    }

    bool operator==(FCardRepresentationDataId B) const
    {
        return Value == B.Value;
    }

    friend uint32 GetTypeHash(FCardRepresentationDataId DataId)
    {
        return GetTypeHash(DataId.Value);
    }
};

// 테이블 의 페이로드(Payload) 및 출력데이터 .
class FCardRepresentationData : public FDeferredCleanupInterface
{
public:
    // 데이터 및 ID.
    FMeshCardsBuildData MeshCardsBuildData;
    FCardRepresentationDataId CardRepresentationDataId;

    (......)

#if WITH_EDITORONLY_DATA
    // 캐싱(Cache)테이블 의 데이터 .
    void CacheDerivedData(const FString& InDDCKey, const ITargetPlatform* TargetPlatform, UStaticMesh* Mesh, UStaticMesh* GenerateSource, bool bGenerateDistanceFieldAsIfTwoSided, FSourceMeshDataForDerivedDataTask* OptionalSourceMeshData);
#endif
};

//
class FAsyncCardRepresentationTaskWorker : public FNonAbandonableTask
{
public:
    (.....)
    
    void DoWork();

private:
    FAsyncCardRepresentationTask& Task;
};

// 데이터 .
class FAsyncCardRepresentationTask
{
public:
    bool bSuccess = false;

#if WITH_EDITOR
    TArray<FSignedDistanceFieldBuildMaterialData> MaterialBlendModes;
#endif

    FSourceMeshDataForDerivedDataTask SourceMeshData;
    bool bGenerateDistanceFieldAsIfTwoSided = false;
    UStaticMesh* StaticMesh = nullptr;
    UStaticMesh* GenerateSource = nullptr;
    FString DDCKey;
    FCardRepresentationData* GeneratedCardRepresentation;
    TUniquePtr<FAsyncTask<FAsyncCardRepresentationTaskWorker>> AsyncTask = nullptr;
};

// 거리의 비동기 의 타입/유형 .
class FCardRepresentationAsyncQueue : public FGCObject
{
public:
    // 추가(Add)의 .
    ENGINE_API void AddTask(FAsyncCardRepresentationTask* Task);
    
    // 처리(Process)비동기 .
    ENGINE_API void ProcessAsyncTasks(bool bLimitExecutionTime = false);
    
    // .
    ENGINE_API void CancelBuild(UStaticMesh* StaticMesh);
    ENGINE_API void CancelAllOutstandingBuilds();

    // .
    ENGINE_API void BlockUntilBuildComplete(UStaticMesh* StaticMesh, bool bWarnIfBlocked);
    ENGINE_API void BlockUntilAllBuildsComplete();

    (......)
};

// 글로벌 .
extern ENGINE_API FCardRepresentationAsyncQueue* GCardRepresentationAsyncQueue;

extern ENGINE_API FString BuildCardRepresentationDerivedDataKey(const FString& InMeshKey);

extern ENGINE_API void BeginCacheMeshCardRepresentation(const ITargetPlatform* TargetPlatform, UStaticMesh* StaticMeshAsset, class FStaticMeshRenderData& RenderData, const FString& DistanceFieldKey, FSourceMeshDataForDerivedDataTask* OptionalSourceMeshData);

6.5.3.2 GCardRepresentationAsyncQueue

Lumen에 필요한 데이터를 구성하기 위해 UE5는 GCardRepresentationAsyncQueueGDistanceFieldAsyncQueue라는 두 개의 전역 대기열 변수를 선언합니다. 전자는 Lumen Card 데이터 구축에 사용되고, 후자는 거리 필드 데이터 구축에 사용됩니다. 생성 및 업데이트 논리는 다음과 같습니다.

cpp
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp

int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
    (......)
    
    if (!FPlatformProperties::RequiresCookedData())
    {
        (......)
        
        // 생성(Create)글로벌 비동기 .
        GDistanceFieldAsyncQueue = new FDistanceFieldAsyncQueue();
        GCardRepresentationAsyncQueue = new FCardRepresentationAsyncQueue();

        (......)
    }
    
    (......)
}

void FEngineLoop::Tick()
{
    (......)
    
    // 갱신(Update)글로벌 비동기 .
    if (GDistanceFieldAsyncQueue)
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_FEngineLoop_Tick_GDistanceFieldAsyncQueue);
        GDistanceFieldAsyncQueue->ProcessAsyncTasks();
    }
    if (GCardRepresentationAsyncQueue)
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_FEngineLoop_Tick_GCardRepresentationAsyncQueue);
        GCardRepresentationAsyncQueue->ProcessAsyncTasks();
    }
    
    (......)
}

GDistanceFieldAsyncQueue는 UE4에 존재하는 유형이므로, 이 섹션에서는 이를 무시하고 GCardRepresentationAsyncQueue에 중점을 둘 것입니다.

전역 빌드 큐 GCardRepresentationAsyncQueue에 CardRepresentation을 추가하는 시점에 대한 답변은 MeshCardRepresentation.cpp에서 찾을 수 있습니다.

cpp
FCardRepresentationAsyncQueue* GCardRepresentationAsyncQueue = NULL;

// 캐싱(Cache)테이블 .
void BeginCacheMeshCardRepresentation(const ITargetPlatform* TargetPlatform, UStaticMesh* StaticMeshAsset, FStaticMeshRenderData& RenderData, const FString& DistanceFieldKey, FSourceMeshDataForDerivedDataTask* OptionalSourceMeshData)
{
    static const auto CVarCards = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.MeshCardRepresentation"));

    if (CVarCards->GetValueOnAnyThread() != 0)
    {
        FString Key = BuildCardRepresentationDerivedDataKey(DistanceFieldKey);
        if (RenderData.LODResources.IsValidIndex(0))
        {
            // FCardRepresentationData인스턴스 .
            if (!RenderData.LODResources[0].CardRepresentationData)
            {
                RenderData.LODResources[0].CardRepresentationData = new FCardRepresentationData();
            }

            const FMeshBuildSettings& BuildSettings = StaticMeshAsset->GetSourceModel(0).BuildSettings;
            UStaticMesh* MeshToGenerateFrom = StaticMeshAsset;

            // 캐싱(Cache)FCardRepresentationData.
            RenderData.LODResources[0].CardRepresentationData->CacheDerivedData(Key, TargetPlatform, StaticMeshAsset, MeshToGenerateFrom, BuildSettings.bGenerateDistanceFieldAsIfTwoSided, OptionalSourceMeshData);
        }
    }
}

// 캐싱(Cache)FCardRepresentationData.
void FCardRepresentationData::CacheDerivedData(const FString& InDDCKey, const ITargetPlatform* TargetPlatform, UStaticMesh* Mesh, UStaticMesh* GenerateSource, bool bGenerateDistanceFieldAsIfTwoSided, FSourceMeshDataForDerivedDataTask* OptionalSourceMeshData)
{
    TArray<uint8> DerivedData;

    (......)
    {
        COOK_STAT(Timer.TrackCyclesOnly());
        
        // 생성(Create)의 FAsyncCardRepresentationTask.
        FAsyncCardRepresentationTask* NewTask = new FAsyncCardRepresentationTask;
        NewTask->DDCKey = InDDCKey;
        check(Mesh && GenerateSource);
        NewTask->StaticMesh = Mesh;
        NewTask->GenerateSource = GenerateSource;
        NewTask->GeneratedCardRepresentation = new FCardRepresentationData();
        NewTask->bGenerateDistanceFieldAsIfTwoSided = bGenerateDistanceFieldAsIfTwoSided;

        // 처리(Process)머티리얼(Material)블렌딩(Blending).
        for (int32 MaterialIndex = 0; MaterialIndex < Mesh->GetStaticMaterials().Num(); MaterialIndex++)
        {
            FSignedDistanceFieldBuildMaterialData MaterialData;
            // Default material blend mode
            MaterialData.BlendMode = BLEND_Opaque;
            MaterialData.bTwoSided = false;

            if (Mesh->GetStaticMaterials()[MaterialIndex].MaterialInterface)
            {
                MaterialData.BlendMode = Mesh->GetStaticMaterials()[MaterialIndex].MaterialInterface->GetBlendMode();
                MaterialData.bTwoSided = Mesh->GetStaticMaterials()[MaterialIndex].MaterialInterface->IsTwoSided();
            }

            NewTask->MaterialBlendModes.Add(MaterialData);
        }

        // Nanite머티리얼(Material)을(를) 활용하여 개 거침(Rough)테이블 정적 。SDF,로드 데이터 。
        if (OptionalSourceMeshData)
        {
            NewTask->SourceMeshData = *OptionalSourceMeshData;
        }
        // 생성(Create)Nanite의 거침(Rough)테이블 .
        else if (Mesh->NaniteSettings.bEnabled)
        {
            IMeshBuilderModule& MeshBuilderModule = IMeshBuilderModule::GetForPlatform(TargetPlatform);
            if (!MeshBuilderModule.BuildMeshVertexPositions(Mesh, NewTask->SourceMeshData.TriangleIndices, NewTask->SourceMeshData.VertexPositions))
            {
                UE_LOG(LogStaticMesh, Error, TEXT("Failed to build static mesh. See previous line(s) for details."));
            }
        }

        // 추가(Add)글로벌 GCardRepresentationAsyncQueue.
        GCardRepresentationAsyncQueue->AddTask(NewTask);
    }
}

6.5.3.3 카드 표현 데이터 생성

FCardRepresentationAsyncQueue의 호출 스택을 추적하면 결국 FMeshUtilities::GenerateCardRepresentationData 인터페이스에 진입한다는 것을 찾는 것이 어렵지 않습니다. 이 인터페이스는 특정 그리드 카드 구성 논리를 실행합니다.

cpp
// Engine\Source\Developer\MeshUtilities\Private\MeshCardRepresentationUtilities.cpp

bool FMeshUtilities::GenerateCardRepresentationData(
    FString MeshName,
    const FSourceMeshDataForDerivedDataTask& SourceMeshData,
    const FStaticMeshLODResources& LODModel,
    class FQueuedThreadPool& ThreadPool,
    const TArray<FSignedDistanceFieldBuildMaterialData>& MaterialBlendModes,
    const FBoxSphereBounds& Bounds,
    const FDistanceFieldVolumeData* DistanceFieldVolumeData,
    bool bGenerateAsIfTwoSided,
    FCardRepresentationData& OutData)
{
    // Embree.
    FEmbreeScene EmbreeScene;
    MeshRepresentation::SetupEmbreeScene(MeshName,
        SourceMeshData,
        LODModel,
        MaterialBlendModes,
        bGenerateAsIfTwoSided,
        EmbreeScene);

    if (!EmbreeScene.EmbreeScene)
    {
        return false;
    }

    // 처리(Process).
    FGenerateCardMeshContext Context(MeshName, EmbreeScene.EmbreeScene, EmbreeScene.EmbreeDevice, OutData);
    // .
    BuildMeshCards(DistanceFieldVolumeData ? DistanceFieldVolumeData->LocalSpaceMeshBounds : Bounds.GetBox(), Context, OutData);

    MeshRepresentation::DeleteEmbreeScene(EmbreeScene);
    
    (......)

    return true;
}

그리드 카드를 구축하는 과정에서 Embree 타사 라이브러리가 사용되는 것을 볼 수 있습니다.

엠브리 소개

Embree는 Intel에서 개발하고 유지 관리하는 오픈 소스 라이브러리입니다. 개발자가 사실적인 렌더링 애플리케이션의 성능을 향상시키는 데 도움이 되는 고성능 레이 트레이싱 커널 모음입니다. 기능에는 고급 헤어 지오메트리, 모션 블러, 동적 장면 및 다중 레벨 인스턴스가 포함됩니다.

Embree의 구현 및 기술은 다음과 같은 특징을 가지고 있습니다.

  • 커널은 SSE, AVX, AVX2 및 AVX-512 지침을 지원하는 최신 Intel 프로세서에 최적화되어 있습니다.

  • CPU 명령어 세트와 가장 잘 일치하는 순회 및 구성 알고리즘을 선택하기 위한 런타임 코드 선택을 지원합니다.

  • 인텔 SPMD 프로그램 컴파일러(ISPC)를 사용하여 작성된 애플리케이션을 지원하고 핵심 레이 트레이싱 알고리즘에 대한 ISPC 인터페이스도 제공합니다.

  • Monte Carlo 레이 트레이싱 알고리즘과 같은 비캐시 일관성 워크로드와 기본 가시성 및 하드 섀도우 광선과 같은 캐시 일관성 워크로드에 최적화된 알고리즘이 포함되어 있습니다.

간단히 말해서 Embree는 CPU 기반의 고도로 최적화된 레이 트레이싱 렌더링 가속기이지만 GPU의 하드웨어 가속을 지원하지 않습니다. 루멘의 그리드 카드 구축 시간이 주로 CPU 성능에 좌우되는 것이 바로 이 기능이다.

빌드의 핵심 로직은 BuildMeshCards에 있습니다.

cpp
void BuildMeshCards(const FBox& MeshBounds, const FGenerateCardMeshContext& Context, FCardRepresentationData& OutData)
{
    static const auto CVarMeshCardRepresentationMinSurface = IConsoleManager::Get().FindTConsoleVariableDataFloat(TEXT("r.MeshCardRepresentation.MinSurface"));
    const float MinSurfaceThreshold = CVarMeshCardRepresentationMinSurface->GetValueOnAnyThread();

    // 의 바운딩 박스(AABB)로 .
    const FVector MeshCardsBoundsCenter = MeshBounds.GetCenter();
    const FVector MeshCardsBoundsExtent = FVector::Max(MeshBounds.GetExtent() + 1.0f, FVector(5.0f));
    const FBox MeshCardsBounds(MeshCardsBoundsCenter - MeshCardsBoundsExtent, MeshCardsBoundsCenter + MeshCardsBoundsExtent);

    // 초기화(Initialize)출력데이터 .
    OutData.MeshCardsBuildData.Bounds = MeshCardsBounds;
    OutData.MeshCardsBuildData.MaxLODLevel = 1;
    OutData.MeshCardsBuildData.CardBuildData.Reset();

    // 처리(Process)샘플링 및 데이터 .
    const float SamplesPerWorldUnit = 1.0f / 10.0f;
    const int32 MinSamplesPerAxis = 4;
    const int32 MaxSamplesPerAxis = 64;
    FIntVector VolumeSizeInVoxels;
    VolumeSizeInVoxels.X = FMath::Clamp<int32>(MeshCardsBounds.GetSize().X * SamplesPerWorldUnit, MinSamplesPerAxis, MaxSamplesPerAxis);
    VolumeSizeInVoxels.Y = FMath::Clamp<int32>(MeshCardsBounds.GetSize().Y * SamplesPerWorldUnit, MinSamplesPerAxis, MaxSamplesPerAxis);
    VolumeSizeInVoxels.Z = FMath::Clamp<int32>(MeshCardsBounds.GetSize().Z * SamplesPerWorldUnit, MinSamplesPerAxis, MaxSamplesPerAxis);

    // 개 의 크기 .
    const FVector VoxelExtent = MeshCardsBounds.GetSize() / FVector(VolumeSizeInVoxels);

    // 랜덤/난수 광선(Ray).
    TArray<FVector4> RayDirectionsOverHemisphere;
    {
        FRandomStream RandomStream(0);
        MeshUtilities::GenerateStratifiedUniformHemisphereSamples(64, RandomStream, RayDirectionsOverHemisphere);
    }
    
    // 순회(Traverse/Iterate)6개 , 개 데이터 .
    for (int32 Orientation = 0; Orientation < 6; ++Orientation)
    {
        // 초기화(Initialize) 및 광선(Ray)데이터 .
        FIntPoint HeighfieldSize(0, 0);
        FVector RayDirection(0.0f, 0.0f, 0.0f);
        FVector RayOriginFrame = MeshCardsBounds.Min;
        FVector HeighfieldStepX(0.0f, 0.0f, 0.0f);
        FVector HeighfieldStepY(0.0f, 0.0f, 0.0f);
        float MaxRayT = 0.0f;
        int32 MeshSliceNum = 0;

        // 에 따라 조정(Adjust) 및 광선(Ray)데이터 .
        switch (Orientation / 2)
        {
            case 0: // : -X, +X
                MaxRayT = MeshCardsBounds.GetSize().X + 0.1f;
                MeshSliceNum = VolumeSizeInVoxels.X;
                HeighfieldSize.X = VolumeSizeInVoxels.Y;
                HeighfieldSize.Y = VolumeSizeInVoxels.Z;
                HeighfieldStepX = FVector(0.0f, MeshCardsBounds.GetSize().Y / HeighfieldSize.X, 0.0f);
                HeighfieldStepY = FVector(0.0f, 0.0f, MeshCardsBounds.GetSize().Z / HeighfieldSize.Y);
                break;

            case 1: // : -Y, +Y
                MaxRayT = MeshCardsBounds.GetSize().Y + 0.1f;
                MeshSliceNum = VolumeSizeInVoxels.Y;
                HeighfieldSize.X = VolumeSizeInVoxels.X;
                HeighfieldSize.Y = VolumeSizeInVoxels.Z;
                HeighfieldStepX = FVector(MeshCardsBounds.GetSize().X / HeighfieldSize.X, 0.0f, 0.0f);
                HeighfieldStepY = FVector(0.0f, 0.0f, MeshCardsBounds.GetSize().Z / HeighfieldSize.Y);
                break;

            case 2: // : -Z, +Z
                MaxRayT = MeshCardsBounds.GetSize().Z + 0.1f;
                MeshSliceNum = VolumeSizeInVoxels.Z;
                HeighfieldSize.X = VolumeSizeInVoxels.X;
                HeighfieldSize.Y = VolumeSizeInVoxels.Y;
                HeighfieldStepX = FVector(MeshCardsBounds.GetSize().X / HeighfieldSize.X, 0.0f, 0.0f);
                HeighfieldStepY = FVector(0.0f, MeshCardsBounds.GetSize().Y / HeighfieldSize.Y, 0.0f);
                break;
        }

        // 에 따라 조정(Adjust)광선(Ray).
        switch (Orientation)
        {
            case 0: 
                RayDirection.X = +1.0f; 
                break;

            case 1: 
                RayDirection.X = -1.0f; 
                RayOriginFrame.X = MeshCardsBounds.Max.X;
                break;

            case 2: 
                RayDirection.Y = +1.0f; 
                break;

            case 3: 
                RayDirection.Y = -1.0f; 
                RayOriginFrame.Y = MeshCardsBounds.Max.Y;
                break;

            case 4: 
                RayDirection.Z = +1.0f; 
                break;

            case 5: 
                RayDirection.Z = -1.0f; 
                RayOriginFrame.Z = MeshCardsBounds.Max.Z;
                break;

            default: 
                check(false);
        };

        TArray<TArray<FSurfacePoint, TInlineAllocator<16>>> HeightfieldLayers;
        HeightfieldLayers.SetNum(HeighfieldSize.X * HeighfieldSize.Y);

        // 패딩/채우기 테이블 포인트 의 데이터 .
        {
            TRACE_CPUPROFILER_EVENT_SCOPE(FillSurfacePoints);

            TArray<float> Heightfield;
            Heightfield.SetNum(HeighfieldSize.X * HeighfieldSize.Y);
            for (int32 HeighfieldY = 0; HeighfieldY < HeighfieldSize.Y; ++HeighfieldY)
            {
                for (int32 HeighfieldX = 0; HeighfieldX < HeighfieldSize.X; ++HeighfieldX)
                {
                    Heightfield[HeighfieldX + HeighfieldY * HeighfieldSize.X] = -1.0f;
                }
            }

            for (int32 HeighfieldY = 0; HeighfieldY < HeighfieldSize.Y; ++HeighfieldY)
            {
                for (int32 HeighfieldX = 0; HeighfieldX < HeighfieldSize.X; ++HeighfieldX)
                {
                    FVector RayOrigin = RayOriginFrame;
                    RayOrigin += (HeighfieldX + 0.5f) * HeighfieldStepX;
                    RayOrigin += (HeighfieldY + 0.5f) * HeighfieldStepY;

                    float StepTMin = 0.0f;

                    for (int32 StepIndex = 0; StepIndex < 64; ++StepIndex)
                    {
                        FEmbreeRay EmbreeRay;
                        EmbreeRay.ray.org_x = RayOrigin.X;
                        EmbreeRay.ray.org_y = RayOrigin.Y;
                        EmbreeRay.ray.org_z = RayOrigin.Z;
                        EmbreeRay.ray.dir_x = RayDirection.X;
                        EmbreeRay.ray.dir_y = RayDirection.Y;
                        EmbreeRay.ray.dir_z = RayDirection.Z;
                        EmbreeRay.ray.tnear = StepTMin;
                        EmbreeRay.ray.tfar = FLT_MAX;

                        FEmbreeIntersectionContext EmbreeContext;
                        rtcInitIntersectContext(&EmbreeContext);
                        rtcIntersect1(Context.FullMeshEmbreeScene, &EmbreeContext, &EmbreeRay);

                        if (EmbreeRay.hit.geomID != RTC_INVALID_GEOMETRY_ID && EmbreeRay.hit.primID != RTC_INVALID_GEOMETRY_ID)
                        {
                            const FVector SurfacePoint = RayOrigin + RayDirection * EmbreeRay.ray.tfar;
                            const FVector SurfaceNormal = EmbreeRay.GetHitNormal();

                            const float NdotD = FVector::DotProduct(RayDirection, SurfaceNormal);
                            const bool bPassCullTest = EmbreeContext.IsHitTwoSided() || NdotD <= 0.0f;
                            const bool bPassProjectionAngleTest = FMath::Abs(NdotD) >= FMath::Cos(75.0f * (PI / 180.0f));

                            const float MinDistanceBetweenPoints = (MaxRayT / MeshSliceNum);
                            const bool bPassDistanceToAnotherSurfaceTest = EmbreeRay.ray.tnear <= 0.0f || (EmbreeRay.ray.tfar - EmbreeRay.ray.tnear > MinDistanceBetweenPoints);

                            if (bPassCullTest && bPassProjectionAngleTest && bPassDistanceToAnotherSurfaceTest)
                            {
                                const bool bIsInsideMesh = IsSurfacePointInsideMesh(Context.FullMeshEmbreeScene, SurfacePoint, SurfaceNormal, RayDirectionsOverHemisphere);
                                if (!bIsInsideMesh)
                                {
                                    HeightfieldLayers[HeighfieldX + HeighfieldY * HeighfieldSize.X].Add(
                                        { EmbreeRay.ray.tnear, EmbreeRay.ray.tfar }
                                    );
                                }
                            }

                            StepTMin = EmbreeRay.ray.tfar + 0.01f;
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }
        }

        const int32 MinCardHits = FMath::Floor(HeighfieldSize.X * HeighfieldSize.Y * MinSurfaceThreshold);

        TArray<FPlacedCard, TInlineAllocator<16>> PlacedCards;
        int32 PlacedCardsHits = 0;

        // 개 기본값(Default).
        {
            FPlacedCard PlacedCard;
            PlacedCard.SliceMin = 0;
            PlacedCard.SliceMax = MeshSliceNum;
            PlacedCards.Add(PlacedCard);

            PlacedCardsHits = UpdatePlacedCards(PlacedCards, RayOriginFrame, RayDirection, HeighfieldStepX, HeighfieldStepY, HeighfieldSize, MeshSliceNum, MaxRayT, MinCardHits, VoxelExtent, HeightfieldLayers);

            if (PlacedCardsHits < MinCardHits)
            {
                PlacedCards.Reset();
            }
        }

        SerializePlacedCards(PlacedCards, /*LOD level*/ 0, Orientation, MinCardHits, MeshCardsBounds, OutData);

        // 의 의 .
        for (uint32 CardPlacementIteration = 0; CardPlacementIteration < 4; ++CardPlacementIteration)
        {
            TArray<FPlacedCard, TInlineAllocator<16>> BestPlacedCards;
            int32 BestPlacedCardHits = PlacedCardsHits;

            for (int32 PlacedCardIndex = 0; PlacedCardIndex < PlacedCards.Num(); ++PlacedCardIndex)
            {
                const FPlacedCard& PlacedCard = PlacedCards[PlacedCardIndex];
                for (int32 SliceIndex = PlacedCard.SliceMin + 2; SliceIndex < PlacedCard.SliceMax; ++SliceIndex)
                {
                    TArray<FPlacedCard, TInlineAllocator<16>> TempPlacedCards(PlacedCards);

                    FPlacedCard NewPlacedCard;
                    NewPlacedCard.SliceMin = SliceIndex;
                    NewPlacedCard.SliceMax = PlacedCard.SliceMax;

                    TempPlacedCards[PlacedCardIndex].SliceMax = SliceIndex - 1;
                    TempPlacedCards.Insert(NewPlacedCard, PlacedCardIndex + 1);

                    const int32 NumHits = UpdatePlacedCards(TempPlacedCards, RayOriginFrame, RayDirection, HeighfieldStepX, HeighfieldStepY, HeighfieldSize, MeshSliceNum, MaxRayT, MinCardHits, VoxelExtent, HeightfieldLayers);

                    if (NumHits > BestPlacedCardHits)
                    {
                        BestPlacedCards = TempPlacedCards;
                        BestPlacedCardHits = NumHits;
                    }
                }
            }

            if (BestPlacedCardHits >= PlacedCardsHits + MinCardHits)
            {
                PlacedCards = BestPlacedCards;
                PlacedCardsHits = BestPlacedCardHits;
            }
        }

        SerializePlacedCards(PlacedCards, /*LOD level*/ 1, Orientation, MinCardHits, MeshCardsBounds, OutData);
    } // for (int32 Orientation = 0; Orientation < 6; ++Orientation)
}

위 코드는 카드 데이터 구성 속도를 높이기 위해 Height Field Ray Tracing이 사용되며, Ray Tracing 기술은 수년 동안 존재해 왔음을 보여줍니다. 핵심 아이디어와 단계는 그리드를 동일한 크기의 3D 복셀로 분할한 다음 해상도 크기에 따라 카메라 위치에서 각 픽셀 위치로 광선을 방출하고 3D 복셀의 교차점을 테스트하여 하이트 필드의 윤곽을 렌더링하는 것입니다. 하이트 필드의 윤곽은 화면을 하이트 필드가 차지하는 영역과 하이트 필드 위의 영역 사이를 구분하는 선으로 나눕니다.

이런 방식으로 얻은 윤곽선에는 명백한 앨리어싱이 있습니다. Ray Tracing Height Fields 논문은 앨리어싱을 완화하기 위해 표면 데이터를 재구성하는 높이 필드 평면, 선형 근사 평면, 삼각형 표면, 이중선형 표면과 같은 방법을 제공합니다.

위 구성 후 아래와 같은 그리드 카드 데이터가 나타날 수 있습니다.

위: 그리드 일반 데이터; 하단: 그리드 카드 데이터 시각화.

그리드 카드 데이터에는 LOD가 있으며, 렌즈의 거리에 따라 해당 레벨의 LOD가 선택됩니다. (동영상을 보려면 클릭하세요.)

또한, UE5에서 생성된 그리드 거리 필드 데이터가 개선되어 희소 저장소를 사용하여 정확성을 향상시켰습니다(아래 그림 왼쪽). 이는 UE4(아래 그림 오른쪽)보다 훨씬 뛰어납니다.

6.5.4 루멘 렌더링 프로세스

Lumen의 주요 렌더링 프로세스는 여전히 FDeferredShadingSceneRenderer::Render에 있습니다.

cpp
void FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder)
{
    (......)
    
    bool bAnyLumenEnabled = false;
    if (!IsSimpleForwardShadingEnabled(ShaderPlatform))
    {
        (......)

        // 검사/감지(Detect)는 뷰(View)활성화(Enable)Lumen.
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
        {
            FViewInfo& View = Views[ViewIndex];
            bAnyLumenEnabled = bAnyLumenEnabled 
                || GetViewPipelineState(View).DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen
                || GetViewPipelineState(View).ReflectionsMethod == EReflectionsMethod::Lumen;
        }

        (......)
    }
    
    (......)
    
    // PrePass.
    RenderPrePass(...);
    
    (......)
    
    // 갱신(Update)Lumen.
    UpdateLumenScene(GraphBuilder);

    // 만약 BasePass실행(Execute)차폐/오클루전 컬링 , 이면 RenderBasePass렌더링(Render)Lumen라이팅 .
    // bOcclusionBeforeBasePass기본값(Default)로 false.
    if (bOcclusionBeforeBasePass)
    {
        {
            LLM_SCOPE_BYTAG(Lumen);
            RenderLumenSceneLighting(GraphBuilder, Views[0]);
        }

        ComputeVolumetricFog(GraphBuilder);
    }
    
    (......)
    
    // BasePass.
    RenderBasePass(...);
    
    (......)
    
    // BasePass의 Lumen라이팅 .
    if (!bOcclusionBeforeBasePass)
    {
        const bool bAfterBasePass = true;
        // 렌더링(Render)그림자(Shadow).
        AllocateVirtualShadowMaps(bAfterBasePass);
        RenderShadowDepthMaps(GraphBuilder, InstanceCullingManager);
        
        {
            LLM_SCOPE_BYTAG(Lumen);
            // 렌더링(Render)Lumen라이팅 .
            RenderLumenSceneLighting(GraphBuilder, Views[0]);
        }

        AddServiceLocalQueuePass(GraphBuilder);
    }
    
    (......)
    
    // 렌더링(Render)Lumen시각화(Visualization).
    RenderLumenSceneVisualization(GraphBuilder, SceneTextures);
    // 렌더링(Render)디퓨즈(Diffuse) 및 AO.
    RenderDiffuseIndirectAndAmbientOcclusion(GraphBuilder, SceneTextures, LightingChannelsTexture, true);
    
    (......)
}

아래 빨간색 상자는 RenderDoc 프레임 캡처에서 Lumen의 실행 단계입니다.

Lumen의 조명은 주로 장면 UpdateLumenScene 업데이트와 장면 조명 RenderLumenSceneLighting 계산의 두 단계로 구성됩니다.

6.5.5 루멘 장면 업데이트

6.5.5.1 LumenScene 업데이트

Lumen 장면 업데이트는 주로 UpdateLumenScene에 의해 수행됩니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneRendering.cpp

void FDeferredShadingSceneRenderer::UpdateLumenScene(FRDGBuilder& GraphBuilder)
{
    LLM_SCOPE_BYTAG(Lumen);

    FViewInfo& View = Views[0];
    const FPerViewPipelineState& ViewPipelineState = GetViewPipelineState(View);
    const bool bAnyLumenActive = ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen || ViewPipelineState.ReflectionsMethod == EReflectionsMethod::Lumen;

    if (bAnyLumenActive
        // 뷰(View)갱신(Update)
        && !View.bIsPlanarReflection 
        && !View.bIsSceneCapture
        && !View.bIsReflectionCapture
        && View.ViewState)
    {
        const double StartTime = FPlatformTime::Seconds();

        // 페치/가져오기(Fetch)Lumen 및 데이터 .
        FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;
        TArray<FCardRenderData, SceneRenderingAllocator>& CardsToRender = LumenCardRenderer.CardsToRender;

        RDG_EVENT_SCOPE(GraphBuilder, "UpdateLumenScene: %u card captures %.3fM texels", CardsToRender.Num(), LumenCardRenderer.NumCardTexelsToCapture / 1e6f);

        // 갱신(Update)버퍼 .
        UpdateCardSceneBuffer(GraphBuilder.RHICmdList, ViewFamily, Scene);

        // 이므로 갱신(Update)Lumen의 매핑 버퍼 , 따라서 생성(Create)뷰(View)버퍼 .
        Lumen::SetupViewUniformBufferParameters(Scene, *View.CachedViewUniformShaderParameters);
        View.ViewUniformBuffer = TUniformBufferRef<FViewUniformShaderParameters>::CreateUniformBufferImmediate(*View.CachedViewUniformShaderParameters, UniformBuffer_SingleFrame);
        
        LumenCardRenderer.CardIdsToRender.Empty(CardsToRender.Num());

        // 의 뎁스 버퍼(Depth Buffer).
        const FRDGTextureDesc DepthStencilAtlasDesc = FRDGTextureDesc::Create2D(LumenSceneData.MaxAtlasSize, PF_DepthStencil, FClearValueBinding::DepthZero, TexCreate_ShaderResource | TexCreate_DepthStencilTargetable | TexCreate_NoFastClear);
        FRDGTextureRef DepthStencilAtlasTexture = GraphBuilder.CreateTexture(DepthStencilAtlasDesc, TEXT("Lumen.DepthStencilAtlas"));

        if (CardsToRender.Num() > 0)
        {
            FRHIBuffer* PrimitiveIdVertexBuffer = nullptr;
            FInstanceCullingResult InstanceCullingResult;
            // 클리핑/클램핑(Clipping), 지원 GPU 및 GPU클리핑/클램핑(Clipping).
#if GPUCULL_TODO
            if (Scene->GPUScene.IsEnabled())
            {
                int32 MaxInstances = 0;
                int32 VisibleMeshDrawCommandsNum = 0;
                int32 NewPassVisibleMeshDrawCommandsNum = 0;

                FInstanceCullingContext InstanceCullingContext(nullptr, TArrayView<const int32>(&View.GPUSceneViewId, 1));

                SetupGPUInstancedDraws(InstanceCullingContext, LumenCardRenderer.MeshDrawCommands, false, MaxInstances, VisibleMeshDrawCommandsNum, NewPassVisibleMeshDrawCommandsNum);
                // Not supposed to do any compaction here.
                ensure(VisibleMeshDrawCommandsNum == LumenCardRenderer.MeshDrawCommands.Num());

                InstanceCullingContext.BuildRenderingCommands(GraphBuilder, Scene->GPUScene, View.DynamicPrimitiveCollector.GetPrimitiveIdRange(), InstanceCullingResult);
            }
            else
#endif // GPUCULL_TODO
            {
                // Prepare primitive Id VB for rendering mesh draw commands.
                if (LumenCardRenderer.MeshDrawPrimitiveIds.Num() > 0)
                {
                    const uint32 PrimitiveIdBufferDataSize = LumenCardRenderer.MeshDrawPrimitiveIds.Num() * sizeof(int32);

                    FPrimitiveIdVertexBufferPoolEntry Entry = GPrimitiveIdVertexBufferPool.Allocate(PrimitiveIdBufferDataSize);
                    PrimitiveIdVertexBuffer = Entry.BufferRHI;

                    void* RESTRICT Data = RHILockBuffer(PrimitiveIdVertexBuffer, 0, PrimitiveIdBufferDataSize, RLM_WriteOnly);
                    FMemory::Memcpy(Data, LumenCardRenderer.MeshDrawPrimitiveIds.GetData(), PrimitiveIdBufferDataSize);
                    RHIUnlockBuffer(PrimitiveIdVertexBuffer);

                    GPrimitiveIdVertexBufferPool.ReturnToFreeList(Entry);
                }
        }
            FRDGTextureRef AlbedoAtlasTexture = GraphBuilder.RegisterExternalTexture(LumenSceneData.AlbedoAtlas);
            FRDGTextureRef NormalAtlasTexture = GraphBuilder.RegisterExternalTexture(LumenSceneData.NormalAtlas);
            FRDGTextureRef EmissiveAtlasTexture = GraphBuilder.RegisterExternalTexture(LumenSceneData.EmissiveAtlas);

            uint32 NumRects = 0;
            FRDGBufferRef RectMinMaxBuffer = nullptr;
            {
                // id,을(를) 활용하여 렌더링(Render)의 드로우/렌더(Draw)。
                TArray<FUintVector4, SceneRenderingAllocator> RectMinMaxToRender;
                RectMinMaxToRender.Reserve(CardsToRender.Num());
                for (const FCardRenderData& CardRenderData : CardsToRender)
                {
                    FIntRect AtlasRect = CardRenderData.AtlasAllocation;

                    FUintVector4 Rect;
                    Rect.X = FMath::Max(AtlasRect.Min.X, 0);
                    Rect.Y = FMath::Max(AtlasRect.Min.Y, 0);
                    Rect.Z = FMath::Max(AtlasRect.Max.X, 0);
                    Rect.W = FMath::Max(AtlasRect.Max.Y, 0);
                    RectMinMaxToRender.Add(Rect);
                }

                NumRects = CardsToRender.Num();
                RectMinMaxBuffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateUploadDesc(sizeof(FUintVector4), FMath::RoundUpToPowerOfTwo(NumRects)), TEXT("Lumen.RectMinMaxBuffer"));

                FPixelShaderUtils::UploadRectMinMaxBuffer(GraphBuilder, RectMinMaxToRender, RectMinMaxBuffer);

                FRDGBufferSRVRef RectMinMaxBufferSRV = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(RectMinMaxBuffer, PF_R32G32B32A32_UINT));
                ClearLumenCards(GraphBuilder, View, AlbedoAtlasTexture, NormalAtlasTexture, EmissiveAtlasTexture, DepthStencilAtlasTexture, LumenSceneData.MaxAtlasSize, RectMinMaxBufferSRV, NumRects);
            }

            // 캐싱(Cache)뷰(View)정보 .
            FViewInfo* SharedView = View.CreateSnapshot();
            {
                SharedView->DynamicPrimitiveCollector = FGPUScenePrimitiveCollector(&GetGPUSceneDynamicContext());
                SharedView->StereoPass = eSSP_FULL;
                SharedView->DrawDynamicFlags = EDrawDynamicFlags::ForceLowestLOD;

                // Don't do material texture mip biasing in proxy card rendering
                SharedView->MaterialTextureMipBias = 0;

                TRefCountPtr<IPooledRenderTarget> NullRef;
                FPlatformMemory::Memcpy(&SharedView->PrevViewInfo.HZB, &NullRef, sizeof(SharedView->PrevViewInfo.HZB));

                SharedView->CachedViewUniformShaderParameters = MakeUnique<FViewUniformShaderParameters>();
                SharedView->CachedViewUniformShaderParameters->PrimitiveSceneData = Scene->GPUScene.PrimitiveBuffer.SRV;
                SharedView->CachedViewUniformShaderParameters->InstanceSceneData = Scene->GPUScene.InstanceDataBuffer.SRV;
                SharedView->CachedViewUniformShaderParameters->LightmapSceneData = Scene->GPUScene.LightmapDataBuffer.SRV;
                SharedView->ViewUniformBuffer = TUniformBufferRef<FViewUniformShaderParameters>::CreateUniformBufferImmediate(*SharedView->CachedViewUniformShaderParameters, UniformBuffer_SingleFrame);
            }

            // 설정(Set)의 텍스처(Texture)캐싱(Cache).
            FLumenCardPassUniformParameters* PassUniformParameters = GraphBuilder.AllocParameters<FLumenCardPassUniformParameters>();
            SetupSceneTextureUniformParameters(GraphBuilder, Scene->GetFeatureLevel(), /*SceneTextureSetupMode*/ ESceneTextureSetupMode::None, PassUniformParameters->SceneTextures);

            // .
            {
                FLumenCardPassParameters* PassParameters = GraphBuilder.AllocParameters<FLumenCardPassParameters>();
                PassParameters->View = Scene->UniformBuffers.LumenCardCaptureViewUniformBuffer;
                PassParameters->CardPass = GraphBuilder.CreateUniformBuffer(PassUniformParameters);
                PassParameters->RenderTargets[0] = FRenderTargetBinding(AlbedoAtlasTexture, ERenderTargetLoadAction::ELoad);
                PassParameters->RenderTargets[1] = FRenderTargetBinding(NormalAtlasTexture, ERenderTargetLoadAction::ELoad);
                PassParameters->RenderTargets[2] = FRenderTargetBinding(EmissiveAtlasTexture, ERenderTargetLoadAction::ELoad);
                PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(DepthStencilAtlasTexture, ERenderTargetLoadAction::ELoad, FExclusiveDepthStencil::DepthWrite_StencilNop);

                InstanceCullingResult.GetDrawParameters(PassParameters->InstanceCullingDrawParams);

                // Pass.
                GraphBuilder.AddPass(
                    RDG_EVENT_NAME("MeshCardCapture"),
                    PassParameters,
                    ERDGPassFlags::Raster,
                    [this, Scene = Scene, PrimitiveIdVertexBuffer, SharedView, &CardsToRender, PassParameters](FRHICommandList& RHICmdList)
                    {
                        QUICK_SCOPE_CYCLE_COUNTER(MeshPass);

                        // 렌더링(Render)의 데이터 드로우/렌더(Draw).
                        for (FCardRenderData& CardRenderData : CardsToRender)
                        {
                            if (CardRenderData.NumMeshDrawCommands > 0)
                            {
                                FIntRect AtlasRect = CardRenderData.AtlasAllocation;
                                RHICmdList.SetViewport(AtlasRect.Min.X, AtlasRect.Min.Y, 0.0f, AtlasRect.Max.X, AtlasRect.Max.Y, 1.0f);

                                CardRenderData.PatchView(RHICmdList, Scene, SharedView);
                                Scene->UniformBuffers.LumenCardCaptureViewUniformBuffer.UpdateUniformBufferImmediate(*SharedView->CachedViewUniformShaderParameters);

                                FGraphicsMinimalPipelineStateSet GraphicsMinimalPipelineStateSet;
#if GPUCULL_TODO
                                if (Scene->GPUScene.IsEnabled())
                                {
                                    FRHIBuffer* DrawIndirectArgsBuffer = nullptr;
                                    FRHIBuffer* InstanceIdOffsetBuffer = nullptr;
                                    FInstanceCullingDrawParams& InstanceCullingDrawParams = PassParameters->InstanceCullingDrawParams;
                                    if (InstanceCullingDrawParams.DrawIndirectArgsBuffer != nullptr && InstanceCullingDrawParams.InstanceIdOffsetBuffer != nullptr)
                                    {
                                        DrawIndirectArgsBuffer = InstanceCullingDrawParams.DrawIndirectArgsBuffer->GetRHI();
                                        InstanceIdOffsetBuffer = InstanceCullingDrawParams.InstanceIdOffsetBuffer->GetRHI();
                                    }

                                    // GPU클리핑/클램핑(Clipping)호출(Call)GPUInstanced인터페이스 .
                                    SubmitGPUInstancedMeshDrawCommandsRange(
                                        LumenCardRenderer.MeshDrawCommands,
                                        GraphicsMinimalPipelineStateSet,
                                        CardRenderData.StartMeshDrawCommandIndex,
                                        CardRenderData.NumMeshDrawCommands,
                                        1,
                                        InstanceIdOffsetBuffer,
                                        DrawIndirectArgsBuffer,
                                        RHICmdList);
                                }
                                else
#endif // GPUCULL_TODO
                                {
                                    // GPU클리핑/클램핑(Clipping)호출(Call)드로우/렌더(Draw)인터페이스 .
                                    SubmitMeshDrawCommandsRange(
                                        LumenCardRenderer.MeshDrawCommands,
                                        GraphicsMinimalPipelineStateSet,
                                        PrimitiveIdVertexBuffer,
                                        0,
                                        false,
                                        CardRenderData.StartMeshDrawCommandIndex,
                                        CardRenderData.NumMeshDrawCommands,
                                        1,
                                        RHICmdList);
                                }
                            }
                        }
                    }
                );
            }

            // 렌더링(Render)의 id 및 검사/감지(Detect)는 렌더링(Render)Nanite의 플래그 .
            bool bAnyNaniteMeshes = false;
            for (FCardRenderData& CardRenderData : CardsToRender)
            {
                bAnyNaniteMeshes = bAnyNaniteMeshes || CardRenderData.NaniteInstanceIds.Num() > 0 || CardRenderData.bDistantScene;
                LumenCardRenderer.CardIdsToRender.Add(CardRenderData.CardIndex);
            }

            // 렌더링(Render)Lumen의 Nanite.
            if (UseNanite(ShaderPlatform) && ViewFamily.EngineShowFlags.NaniteMeshes && bAnyNaniteMeshes)
            {
                TRACE_CPUPROFILER_EVENT_SCOPE(NaniteMeshPass);
                QUICK_SCOPE_CYCLE_COUNTER(NaniteMeshPass);

                const FIntPoint DepthStencilAtlasSize = DepthStencilAtlasDesc.Extent;
                const FIntRect DepthAtlasRect = FIntRect(0, 0, DepthStencilAtlasSize.X, DepthStencilAtlasSize.Y);
                FRDGBufferSRVRef RectMinMaxBufferSRV = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(RectMinMaxBuffer, PF_R32G32B32A32_UINT));

                // 래스터라이제이션 .
                Nanite::FRasterContext RasterContext = Nanite::InitRasterContext(
                    GraphBuilder,
                    FeatureLevel,
                    DepthStencilAtlasSize,
                    Nanite::EOutputBufferMode::VisBuffer,
                    true,
                    RectMinMaxBufferSRV,
                    NumRects);

                const bool bUpdateStreaming = false;
                const bool bSupportsMultiplePasses = true;
                const bool bForceHWRaster = RasterContext.RasterScheduling == Nanite::ERasterScheduling::HardwareOnly;
                // ( 및 Nanite의 Pass)
                const bool bPrimaryContext = false;

                // 클리핑/클램핑(Clipping)
                Nanite::FCullingContext CullingContext = Nanite::InitCullingContext(
                    GraphBuilder,
                    *Scene,
                    nullptr,
                    FIntRect(),
                    false,
                    bUpdateStreaming,
                    bSupportsMultiplePasses,
                    bForceHWRaster,
                    bPrimaryContext);

                // 뷰(View)렌더링(Render).
                if (GLumenSceneNaniteMultiViewCapture)
                {
                    const uint32 NumCardsToRender = CardsToRender.Num();

                    // 레벨 while루프 는 를 위해 개수 , 개 회 의 MAX_VIEWS_PER_CULL_RASTERIZE_PASS.
                    uint32 NextCardIndex = 0;
                    while(NextCardIndex < NumCardsToRender)
                    {
                        TArray<Nanite::FPackedView, SceneRenderingAllocator> NaniteViews;
                        TArray<Nanite::FInstanceDraw, SceneRenderingAllocator> NaniteInstanceDraws;

                        // 개 렌더링(Render)개 FPackedViewParams인스턴스 , 추가(Add)까지 NaniteViews, 까지 NaniteViews까지 뷰(View)개수 .
                        while(NextCardIndex < NumCardsToRender && NaniteViews.Num() < MAX_VIEWS_PER_CULL_RASTERIZE_PASS)
                        {
                            const FCardRenderData& CardRenderData = CardsToRender[NextCardIndex];

                            if(CardRenderData.NaniteInstanceIds.Num() > 0)
                            {
                                for(uint32 InstanceID : CardRenderData.NaniteInstanceIds)
                                {
                                    NaniteInstanceDraws.Add(Nanite::FInstanceDraw { InstanceID, (uint32)NaniteViews.Num() });
                                }

                                Nanite::FPackedViewParams Params;
                                Params.ViewMatrices = CardRenderData.ViewMatrices;
                                Params.PrevViewMatrices = CardRenderData.ViewMatrices;
                                Params.ViewRect = CardRenderData.AtlasAllocation;
                                Params.RasterContextSize = DepthStencilAtlasSize;
                                Params.LODScaleFactor = CardRenderData.NaniteLODScaleFactor;
                                NaniteViews.Add(Nanite::CreatePackedView(Params));
                            }

                            NextCardIndex++;
                        }

                        // 래스터라이제이션 .
                        if (NaniteInstanceDraws.Num() > 0)
                        {
                            RDG_EVENT_SCOPE(GraphBuilder, "Nanite::RasterizeLumenCards");

                            Nanite::FRasterState RasterState;
                            Nanite::CullRasterize(
                                GraphBuilder,
                                *Scene,
                                NaniteViews,
                                CullingContext,
                                RasterContext,
                                RasterState,
                                &NaniteInstanceDraws
                            );
                        }
                    }
                }
                else // 뷰(View)렌더링(Render)
                {
                    RDG_EVENT_SCOPE(GraphBuilder, "RenderLumenCardsWithNanite");

                    // 뷰(View)렌더링(Render)비교 , 선형(Linear)순회(Traverse/Iterate)렌더링(Render), 개 개 view호출(Call)회 드로우/렌더(Draw).
                    for(FCardRenderData& CardRenderData : CardsToRender)
                    {
                        if(CardRenderData.NaniteInstanceIds.Num() > 0)
                        {                        
                            TArray<Nanite::FInstanceDraw, SceneRenderingAllocator> NaniteInstanceDraws;
                            for( uint32 InstanceID : CardRenderData.NaniteInstanceIds )
                            {
                                NaniteInstanceDraws.Add( Nanite::FInstanceDraw { InstanceID, 0u } );
                            }
                        
                            CardRenderData.PatchView(GraphBuilder.RHICmdList, Scene, SharedView);
                            Nanite::FPackedView PackedView = Nanite::CreatePackedViewFromViewInfo(*SharedView, DepthStencilAtlasSize, 0);

                            Nanite::CullRasterize(
                                GraphBuilder,
                                *Scene,
                                { PackedView },
                                CullingContext,
                                RasterContext,
                                Nanite::FRasterState(),
                                &NaniteInstanceDraws
                            );
                        }
                    }
                }

                extern float GLumenDistantSceneMinInstanceBoundsRadius;

                // 로 의 렌더링(Render)개 .
                for (FCardRenderData& CardRenderData : CardsToRender)
                {
                    // bDistantScene플래그 는 의 .
                    if (CardRenderData.bDistantScene)
                    {
                        Nanite::FRasterState RasterState;
                        RasterState.bNearClip = false;

                        CardRenderData.PatchView(GraphBuilder.RHICmdList, Scene, SharedView);
                        Nanite::FPackedView PackedView = Nanite::CreatePackedViewFromViewInfo(
                            *SharedView,
                            DepthStencilAtlasSize,
                            /*Flags*/ 0,
                            /*StreamingPriorityCategory*/ 0,
                            GLumenDistantSceneMinInstanceBoundsRadius,
                            Lumen::GetDistanceSceneNaniteLODScaleFactor());

                        Nanite::CullRasterize(
                            GraphBuilder,
                            *Scene,
                            { PackedView },
                            CullingContext,
                            RasterContext,
                            RasterState);
                    }
                }

                // LumenPass.
                Nanite::DrawLumenMeshCapturePass(
                    GraphBuilder,
                    *Scene,
                    SharedView,
                    CardsToRender,
                    CullingContext,
                    RasterContext,
                    PassUniformParameters,
                    RectMinMaxBufferSRV,
                    NumRects,
                    LumenSceneData.MaxAtlasSize,
                    AlbedoAtlasTexture,
                    NormalAtlasTexture,
                    EmissiveAtlasTexture,
                    DepthStencilAtlasTexture
                );
            }

            ConvertToExternalTexture(GraphBuilder, AlbedoAtlasTexture, LumenSceneData.AlbedoAtlas);
            ConvertToExternalTexture(GraphBuilder, NormalAtlasTexture, LumenSceneData.NormalAtlas);
            ConvertToExternalTexture(GraphBuilder, EmissiveAtlasTexture, LumenSceneData.EmissiveAtlas);
        }

        // 데이터 .
        {
            QUICK_SCOPE_CYCLE_COUNTER(UploadCardIndexBuffers);

            // 버퍼 .
            {
                FRDGBufferRef CardIndexBuffer = GraphBuilder.CreateBuffer(
                    FRDGBufferDesc::CreateUploadDesc(sizeof(uint32), FMath::Max(LumenCardRenderer.CardIdsToRender.Num(), 1)),
                    TEXT("Lumen.CardsToRenderIndexBuffer"));

                FLumenCardIdUpload* PassParameters = GraphBuilder.AllocParameters<FLumenCardIdUpload>();
                PassParameters->CardIds = CardIndexBuffer;

                const uint32 CardIdBytes = LumenCardRenderer.CardIdsToRender.GetTypeSize() * LumenCardRenderer.CardIdsToRender.Num();
                const void* CardIdPtr = LumenCardRenderer.CardIdsToRender.GetData();

                GraphBuilder.AddPass(
                    RDG_EVENT_NAME("Upload CardsToRenderIndexBuffer NumIndices=%d", LumenCardRenderer.CardIdsToRender.Num()),
                    PassParameters,
                    ERDGPassFlags::Copy,
                    [PassParameters, CardIdBytes, CardIdPtr](FRHICommandListImmediate& RHICmdList)
                    {
                        if (CardIdBytes > 0)
                        {
                            void* DestCardIdPtr = RHILockBuffer(PassParameters->CardIds->GetRHI(), 0, CardIdBytes, RLM_WriteOnly);
                            FPlatformMemory::Memcpy(DestCardIdPtr, CardIdPtr, CardIdBytes);
                            RHIUnlockBuffer(PassParameters->CardIds->GetRHI());
                        }
                    });

                ConvertToExternalBuffer(GraphBuilder, CardIndexBuffer, LumenCardRenderer.CardsToRenderIndexBuffer);
            }

            // 매핑 테이블 버퍼 .
            {
                const uint32 NumHashMapUInt32 = FLumenCardRenderer::NumCardsToRenderHashMapBucketUInt32;
                const uint32 NumHashMapBytes = 4 * NumHashMapUInt32;
                const uint32 NumHashMapBuckets = 32 * NumHashMapUInt32;

                FRDGBufferRef CardHashMapBuffer = GraphBuilder.CreateBuffer(
                    FRDGBufferDesc::CreateUploadDesc(sizeof(uint32), NumHashMapUInt32),
                    TEXT("Lumen.CardsToRenderHashMapBuffer"));

                LumenCardRenderer.CardsToRenderHashMap.Init(0, NumHashMapBuckets);

                for (int32 CardIndex : LumenCardRenderer.CardIdsToRender)
                {
                    LumenCardRenderer.CardsToRenderHashMap[CardIndex % NumHashMapBuckets] = 1;
                }

                FLumenCardIdUpload* PassParameters = GraphBuilder.AllocParameters<FLumenCardIdUpload>();
                PassParameters->CardIds = CardHashMapBuffer;

                const void* HashMapDataPtr = LumenCardRenderer.CardsToRenderHashMap.GetData();

                GraphBuilder.AddPass(
                    RDG_EVENT_NAME("Upload CardsToRenderHashMapBuffer NumUInt32=%d", NumHashMapUInt32),
                    PassParameters,
                    ERDGPassFlags::Copy,
                    [PassParameters, NumHashMapBytes, HashMapDataPtr](FRHICommandListImmediate& RHICmdList)
                    {
                        if (NumHashMapBytes > 0)
                        {
                            void* DestCardIdPtr = RHILockBuffer(PassParameters->CardIds->GetRHI(), 0, NumHashMapBytes, RLM_WriteOnly);
                            FPlatformMemory::Memcpy(DestCardIdPtr, HashMapDataPtr, NumHashMapBytes);
                            RHIUnlockBuffer(PassParameters->CardIds->GetRHI());
                        }
                    });

                ConvertToExternalBuffer(GraphBuilder, CardHashMapBuffer, LumenCardRenderer.CardsToRenderHashMapBuffer);
            }

            // 버퍼 .
            {
                FRDGBufferRef VisibleCardsIndexBuffer = GraphBuilder.CreateBuffer(
                    FRDGBufferDesc::CreateUploadDesc(sizeof(uint32), FMath::Max(LumenSceneData.VisibleCardsIndices.Num(), 1)),
                    TEXT("Lumen.VisibleCardsIndexBuffer"));

                FLumenCardIdUpload* PassParameters = GraphBuilder.AllocParameters<FLumenCardIdUpload>();
                PassParameters->CardIds = VisibleCardsIndexBuffer;

                const uint32 CardIdBytes = sizeof(uint32) * LumenSceneData.VisibleCardsIndices.Num();
                const void* CardIdPtr = LumenSceneData.VisibleCardsIndices.GetData();

                GraphBuilder.AddPass(
                    RDG_EVENT_NAME("Upload VisibleCardIndices NumIndices=%d", LumenSceneData.VisibleCardsIndices.Num()),
                    PassParameters,
                    ERDGPassFlags::Copy,
                    [PassParameters, CardIdBytes, CardIdPtr](FRHICommandListImmediate& RHICmdList)
                    {
                        if (CardIdBytes > 0)
                        {
                            void* DestCardIdPtr = RHILockBuffer(PassParameters->CardIds->GetRHI(), 0, CardIdBytes, RLM_WriteOnly);
                            FPlatformMemory::Memcpy(DestCardIdPtr, CardIdPtr, CardIdBytes);
                            RHIUnlockBuffer(PassParameters->CardIds->GetRHI());
                        }
                    });

                ConvertToExternalBuffer(GraphBuilder, VisibleCardsIndexBuffer, LumenSceneData.VisibleCardsIndexBuffer);
            }
        }

        // 필터링 Lumen씬 뎁스(SceneDepth).
        if (LumenCardRenderer.CardIdsToRender.Num() > 0)
        {
            TRDGUniformBufferRef<FLumenCardScene> LumenCardSceneUniformBuffer;
            {
                FLumenCardScene* LumenCardSceneParameters = GraphBuilder.AllocParameters<FLumenCardScene>();
                SetupLumenCardSceneParameters(GraphBuilder, Scene, *LumenCardSceneParameters);
                LumenCardSceneUniformBuffer = GraphBuilder.CreateUniformBuffer(LumenCardSceneParameters);
            }

            PrefilterLumenSceneDepth(GraphBuilder, LumenCardSceneUniformBuffer, DepthStencilAtlasTexture, LumenCardRenderer.CardIdsToRender, View);
        }
    }

    FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;
    LumenSceneData.CardIndicesToUpdateInBuffer.Reset();
    LumenSceneData.MeshCardsIndicesToUpdateInBuffer.Reset();
    LumenSceneData.DFObjectIndicesToUpdateInBuffer.Reset();
}

Lumen 장면을 업데이트하는 프로세스에는 주로 카드 자르기, 카드 ID 업로드, 뷰 및 장면 텍스처 캐싱, 그리드 카드 캡처, 카드를 뷰로 사용하여 Lumen 장면 래스터라이제이션, 원거리 카드 렌더링, 그리드 캡처 그리기, 카드 데이터 및 표시 데이터 업로드 단계가 포함됩니다.

위의 과정은 너무 많기 때문에 모든 과정을 자세히 설명하는 것은 불가능합니다. 이 섹션에서는 그리드 카드 캡처 및 그리드 카드 래스터라이제이션와 관련된 단계에 중점을 둘 것입니다.

6.5.5.2 CardsToRender

그리드 카드 캡처 및 래스터라이제이션 그리드 카드 단계를 설명하려면 LumenCardRenderer.CardsToRender의 추가 프로세스를 명확히 할 필요가 있습니다. Lumen 장면에서 어떤 카드를 캡처하고 렌더링해야 하는지 명확히 하겠습니다. 해당 프로세서는 InitView 단계의 BeginUpdateLumenSceneTasks입니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneRendering.cpp

void FDeferredShadingSceneRenderer::BeginUpdateLumenSceneTasks(FRDGBuilder& GraphBuilder)
{
    LLM_SCOPE_BYTAG(Lumen);

    const FViewInfo& MainView = Views[0];
    const bool bAnyLumenActive = ShouldRenderLumenDiffuseGI(Scene, MainView, true)
        || ShouldRenderLumenReflections(MainView, true);

    if (bAnyLumenActive
        && !ViewFamily.EngineShowFlags.HitProxies)
    {
        SCOPED_NAMED_EVENT(FDeferredShadingSceneRenderer_BeginUpdateLumenSceneTasks, FColor::Emerald);
        QUICK_SCOPE_CYCLE_COUNTER(BeginUpdateLumenSceneTasks);
        const double StartTime = FPlatformTime::Seconds();

        FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;
        // 페치/가져오기(Fetch)렌더링(Render)리스트 리셋/초기화(Reset).
        TArray<FCardRenderData, SceneRenderingAllocator>& CardsToRender = LumenCardRenderer.CardsToRender;
        LumenCardRenderer.Reset();

        const int32 LocalLumenSceneGeneration = GLumenSceneGeneration;
        const bool bRecaptureLumenSceneOnce = LumenSceneData.Generation != LocalLumenSceneGeneration;
        LumenSceneData.Generation = LocalLumenSceneGeneration;
        const bool bReallocateAtlas = LumenSceneData.MaxAtlasSize != GetDesiredAtlasSize() 
            || (LumenSceneData.RadiosityAtlas && LumenSceneData.RadiosityAtlas->GetDesc().Extent != GetRadiosityAtlasSize(LumenSceneData.MaxAtlasSize))
            || GLumenSceneReset;

        if (GLumenSceneReset != 2)
        {
            GLumenSceneReset = 0;
        }

        LumenSceneData.NumMeshCardsToAddToSurfaceCache = 0;

        // 갱신(Update).
        UpdateDirtyCards(Scene, bReallocateAtlas, bRecaptureLumenSceneOnce);
        // 갱신(Update)Lumen의 정보 .
        UpdateLumenScenePrimitives(Scene);
        // 갱신(Update).
        UpdateDistantScene(Scene, Views[0]);

        const FVector LumenSceneCameraOrigin = GetLumenSceneViewOrigin(MainView, GetNumLumenVoxelClipmaps() - 1);
        const float MaxCardUpdateDistanceFromCamera = ComputeMaxCardUpdateDistanceFromCamera();

        // 할당(Allocate)Atlas.
        if (bReallocateAtlas)
        {
            LumenSceneData.MaxAtlasSize = GetDesiredAtlasSize();
            // 생성(Create)Atlas,해제(Release)
            ensure(LumenSceneData.NumCardTexels == 0);

            LumenSceneData.AtlasAllocator = FBinnedTextureLayout(LumenSceneData.MaxAtlasSize, GLumenSceneCardAtlasAllocatorBinSize);
        }

        // 및 갱신(Update)로써 및 의 개수 , 는 갱신(Update)에 의해 GLumenSceneRecaptureLumenSceneEveryFrame(r.LumenScene.RecaptureEveryFrame).
        const int32 CardCapturesPerFrame = GLumenSceneRecaptureLumenSceneEveryFrame != 0 ? INT_MAX : GetMaxLumenSceneCardCapturesPerFrame();
        const int32 CardTexelsToCapturePerFrame = GLumenSceneRecaptureLumenSceneEveryFrame != 0 ? INT_MAX : GetLumenSceneCardResToCapturePerFrame() * GetLumenSceneCardResToCapturePerFrame();

        if (CardCapturesPerFrame > 0 && CardTexelsToCapturePerFrame > 0)
        {
            QUICK_SCOPE_CYCLE_COUNTER(FillCardsToRender);

            TArray<FLumenSurfaceCacheUpdatePacket, SceneRenderingAllocator> Packets;
            TArray<FMeshCardsAdd, SceneRenderingAllocator> MeshCardsAddsSortedByPriority;

            // 테이블 캐싱(Cache)갱신(Update).
            {
                TRACE_CPUPROFILER_EVENT_SCOPE(PrepareSurfaceCacheUpdate);

                const int32 NumPrimitivesPerPacket = FMath::Max(GLumenScenePrimitivesPerPacket, 1);
                const int32 NumPackets = FMath::DivideAndRoundUp(LumenSceneData.LumenPrimitives.Num(), NumPrimitivesPerPacket);

                CardsToRender.Reset(GetMaxLumenSceneCardCapturesPerFrame());
                Packets.Reserve(NumPackets);

                for (int32 PacketIndex = 0; PacketIndex < NumPackets; ++PacketIndex)
                {
                    Packets.Emplace(
                        LumenSceneData.LumenPrimitives,
                        LumenSceneData.MeshCards,
                        LumenSceneData.Cards,
                        LumenSceneCameraOrigin,
                        MaxCardUpdateDistanceFromCamera,
                        PacketIndex * NumPrimitivesPerPacket,
                        NumPrimitivesPerPacket);
                }
            }

            // 실행(Execute)캐싱(Cache)갱신(Update).
            {
                TRACE_CPUPROFILER_EVENT_SCOPE(RunPrepareSurfaceCacheUpdate);
                const bool bExecuteInParallel = FApp::ShouldUseThreadingForPerformance();

                ParallelFor(Packets.Num(),
                    [&Packets](int32 Index)
                    {
                        Packets[Index].AnyThreadTask();
                    },
                    !bExecuteInParallel
                );
            }

            // 의 결과 .
            {
                TRACE_CPUPROFILER_EVENT_SCOPE(PacketResults);

                const float CARD_DISTANCE_BUCKET_SIZE = 100.0f;
                uint32 NumMeshCardsAddsPerBucket[MAX_ADD_PRIMITIVE_PRIORITY + 1];

                for (int32 BucketIndex = 0; BucketIndex < UE_ARRAY_COUNT(NumMeshCardsAddsPerBucket); ++BucketIndex)
                {
                    NumMeshCardsAddsPerBucket[BucketIndex] = 0;
                }

                // Count how many cards fall into each bucket
                for (int32 PacketIndex = 0; PacketIndex < Packets.Num(); ++PacketIndex)
                {
                    const FLumenSurfaceCacheUpdatePacket& Packet = Packets[PacketIndex];
                    LumenSceneData.NumMeshCardsToAddToSurfaceCache += Packet.MeshCardsAdds.Num();

                    for (int32 CardIndex = 0; CardIndex < Packet.MeshCardsAdds.Num(); ++CardIndex)
                    {
                        const FMeshCardsAdd& MeshCardsAdd = Packet.MeshCardsAdds[CardIndex];
                        ++NumMeshCardsAddsPerBucket[MeshCardsAdd.Priority];
                    }
                }

                int32 NumMeshCardsInBucketsUpToMaxBucket = 0;
                int32 MaxBucketIndexToAdd = 0;

                // N개 할당(Allocate)
                for (int32 BucketIndex = 0; BucketIndex < UE_ARRAY_COUNT(NumMeshCardsAddsPerBucket); ++BucketIndex)
                {
                    NumMeshCardsInBucketsUpToMaxBucket += NumMeshCardsAddsPerBucket[BucketIndex];
                    MaxBucketIndexToAdd = BucketIndex;

                    if (NumMeshCardsInBucketsUpToMaxBucket > CardCapturesPerFrame)
                    {
                        break;
                    }
                }

                MeshCardsAddsSortedByPriority.Reserve(GetMaxLumenSceneCardCapturesPerFrame());

                // N개 까지 CardsToAllocateSortedByDistance
                for (int32 PacketIndex = 0; PacketIndex < Packets.Num(); ++PacketIndex)
                {
                    const FLumenSurfaceCacheUpdatePacket& Packet = Packets[PacketIndex];

                    for (int32 CardIndex = 0; CardIndex < Packet.MeshCardsAdds.Num() && MeshCardsAddsSortedByPriority.Num() < CardCapturesPerFrame; ++CardIndex)
                    {
                        const FMeshCardsAdd& MeshCardsAdd = Packet.MeshCardsAdds[CardIndex];

                        if (MeshCardsAdd.Priority <= MaxBucketIndexToAdd)
                        {
                            MeshCardsAddsSortedByPriority.Add(MeshCardsAdd);
                        }
                    }
                }

                // 의 .
                for (int32 PacketIndex = 0; PacketIndex < Packets.Num(); ++PacketIndex)
                {
                    const FLumenSurfaceCacheUpdatePacket& Packet = Packets[PacketIndex];

                    for (int32 MeshCardsToRemoveIndex = 0; MeshCardsToRemoveIndex < Packet.MeshCardsRemoves.Num(); ++MeshCardsToRemoveIndex)
                    {
                        const FMeshCardsRemove& MeshCardsRemove = Packet.MeshCardsRemoves[MeshCardsToRemoveIndex];
                        FLumenPrimitive& LumenPrimitive = LumenSceneData.LumenPrimitives[MeshCardsRemove.LumenPrimitiveIndex];
                        FLumenPrimitiveInstance& LumenPrimitiveInstance = LumenPrimitive.Instances[MeshCardsRemove.LumenInstanceIndex];

                        LumenSceneData.RemoveMeshCards(LumenPrimitive, LumenPrimitiveInstance);
                    }
                }
            }

            // 할당(Allocate).
            extern int32 GLumenUpdateDistantSceneCaptures;
            if (GLumenUpdateDistantSceneCaptures)
            {
                for (int32 DistantCardIndex : LumenSceneData.DistantCardIndices)
                {
                    FLumenCard& DistantCard = LumenSceneData.Cards[DistantCardIndex];

                    extern int32 GLumenDistantSceneCardResolution;
                    DistantCard.DesiredResolution = FIntPoint(GLumenDistantSceneCardResolution, GLumenDistantSceneCardResolution);

                    if (!DistantCard.bVisible)
                    {
                        LumenSceneData.AddCardToVisibleCardList(DistantCardIndex);
                        DistantCard.bVisible = true;
                    }

                    DistantCard.RemoveFromAtlas(LumenSceneData);
                    LumenSceneData.CardIndicesToUpdateInBuffer.Add(DistantCardIndex);

                    // 추가(Add)까지 CardsToRender리스트 .
                    CardsToRender.Add(FCardRenderData(
                        DistantCard,
                        nullptr,
                        -1,
                        FeatureLevel,
                        DistantCardIndex));
                }
            }

            // 할당(Allocate)의 .
            for (int32 SortedCardIndex = 0; SortedCardIndex < MeshCardsAddsSortedByPriority.Num(); ++SortedCardIndex)
            {
                const FMeshCardsAdd& MeshCardsAdd = MeshCardsAddsSortedByPriority[SortedCardIndex];
                FLumenPrimitive& LumenPrimitive = LumenSceneData.LumenPrimitives[MeshCardsAdd.LumenPrimitiveIndex];
                FLumenPrimitiveInstance& LumenPrimitiveInstance = LumenPrimitive.Instances[MeshCardsAdd.LumenInstanceIndex];

                LumenSceneData.AddMeshCards(MeshCardsAdd.LumenPrimitiveIndex, MeshCardsAdd.LumenInstanceIndex);

                if (LumenPrimitiveInstance.MeshCardsIndex >= 0)
                {
                    // 페치/가져오기(Fetch)인스턴스 의 .
                    const FLumenMeshCards& MeshCards = LumenSceneData.MeshCards[LumenPrimitiveInstance.MeshCardsIndex];

                    // 순회(Traverse/Iterate)의 , 추가(Add)유효(Valid)의 까지 CardsToRender리스트 .
                    for (uint32 CardIndex = MeshCards.FirstCardIndex; CardIndex < MeshCards.FirstCardIndex + MeshCards.NumCards; ++CardIndex)
                    {
                        FLumenCard& LumenCard = LumenSceneData.Cards[CardIndex];

                        // 할당(Allocate).
                        FCardAllocationOutput CardAllocation;
                        ComputeCardAllocation(LumenCard, LumenSceneCameraOrigin, MaxCardUpdateDistanceFromCamera, CardAllocation);

                        LumenCard.DesiredResolution = CardAllocation.TextureAllocationSize;

                        if (LumenCard.bVisible != CardAllocation.bVisible)
                        {
                            LumenCard.bVisible = CardAllocation.bVisible;
                            if (LumenCard.bVisible)
                            {
                                LumenSceneData.AddCardToVisibleCardList(CardIndex);
                            }
                            else
                            {
                                LumenCard.RemoveFromAtlas(LumenSceneData);
                                LumenSceneData.RemoveCardFromVisibleCardList(CardIndex);
                            }
                            LumenSceneData.CardIndicesToUpdateInBuffer.Add(CardIndex);
                        }

                        // 만약 해상도 및 , 추가(Add)까지 CardsToRender.
                        if (LumenCard.bVisible && LumenCard.AtlasAllocation.Width() != LumenCard.DesiredResolution.X && LumenCard.AtlasAllocation.Height() != LumenCard.DesiredResolution.Y)
                        {
                            LumenCard.RemoveFromAtlas(LumenSceneData);
                            LumenSceneData.CardIndicesToUpdateInBuffer.Add(CardIndex);

                            // 추가(Add)까지 CardsToRender리스트 .
                            CardsToRender.Add(FCardRenderData(
                                LumenCard,
                                LumenPrimitive.Primitive,
                                LumenPrimitive.bMergedInstances ? -1 : MeshCardsAdd.LumenInstanceIndex,
                                FeatureLevel,
                                CardIndex));

                            LumenCardRenderer.NumCardTexelsToCapture += LumenCard.AtlasAllocation.Area();
                        }
                    } // for

                    // 만약 또는 , 루프 .
                    if (CardsToRender.Num() >= CardCapturesPerFrame
                        || LumenCardRenderer.NumCardTexelsToCapture >= CardTexelsToCapturePerFrame)
                    {
                        break;
                    }
                }
            }
        }

        // 할당(Allocate) 및 갱신(Update)Atlas.
        AllocateOptionalCardAtlases(GraphBuilder, LumenSceneData, MainView, bReallocateAtlas);
        UpdateLumenCardAtlasAllocation(GraphBuilder, MainView, bReallocateAtlas, bRecaptureLumenSceneOnce);

         // 처리(Process)렌더링(Render).
        if (CardsToRender.Num() > 0)
        {
            // 설정(Set)채널/패스(Pass).
            {
                QUICK_SCOPE_CYCLE_COUNTER(MeshPassSetup);

                // 렌더링(Render),의 렌더링(Render)데이터 .
                {
                    QUICK_SCOPE_CYCLE_COUNTER(PrepareStaticMeshData);

                    // Set of unique primitives requiring static mesh update
                    TSet<FPrimitiveSceneInfo*> PrimitivesToUpdateStaticMeshes;

                    for (FCardRenderData& CardRenderData : CardsToRender)
                    {
                        FPrimitiveSceneInfo* PrimitiveSceneInfo = CardRenderData.PrimitiveSceneInfo;

                        if (PrimitiveSceneInfo && PrimitiveSceneInfo->Proxy->AffectsDynamicIndirectLighting())
                        {
                            if (PrimitiveSceneInfo->NeedsUniformBufferUpdate())
                            {
                                PrimitiveSceneInfo->UpdateUniformBuffer(GraphBuilder.RHICmdList);
                            }

                            if (PrimitiveSceneInfo->NeedsUpdateStaticMeshes())
                            {
                                PrimitivesToUpdateStaticMeshes.Add(PrimitiveSceneInfo);
                            }
                        }
                    }

                    if (PrimitivesToUpdateStaticMeshes.Num() > 0)
                    {
                        TArray<FPrimitiveSceneInfo*> UpdatedSceneInfos;
                        UpdatedSceneInfos.Reserve(PrimitivesToUpdateStaticMeshes.Num());
                        for (FPrimitiveSceneInfo* PrimitiveSceneInfo : PrimitivesToUpdateStaticMeshes)
                        {
                            UpdatedSceneInfos.Add(PrimitiveSceneInfo);
                        }

                        FPrimitiveSceneInfo::UpdateStaticMeshes(GraphBuilder.RHICmdList, Scene, UpdatedSceneInfos, true);
                    }
                }

                // 추가(Add)드로우/렌더(Draw).
                for (FCardRenderData& CardRenderData : CardsToRender)
                {
                    CardRenderData.StartMeshDrawCommandIndex = LumenCardRenderer.MeshDrawCommands.Num();
                    CardRenderData.NumMeshDrawCommands = 0;
                    int32 NumNanitePrimitives = 0;

                    const FLumenCard& Card = LumenSceneData.Cards[CardRenderData.CardIndex];
                    checkSlow(Card.bVisible && Card.bAllocated);

                    // 생성(Create) 또는 처리(Process)의 FVisibleMeshDrawCommand.
                    AddCardCaptureDraws(Scene, 
                        GraphBuilder.RHICmdList, 
                        CardRenderData, 
                        LumenCardRenderer.MeshDrawCommands, 
                        LumenCardRenderer.MeshDrawPrimitiveIds);

                    CardRenderData.NumMeshDrawCommands = LumenCardRenderer.MeshDrawCommands.Num() - CardRenderData.StartMeshDrawCommandIndex;
                }
            }

            (.....)
        }
    }
}

위에서 볼 수 있듯이 그리드 카드는 매 프레임마다 업데이트되지 않습니다. GLumenSceneRecaptureLumenSceneEveryFrame(콘솔 명령 r.LumenScene.RecaptureEveryFrame)이 켜지고 그리드 카드의 해상도가 변경되어 표시되면 렌더링할 목록에 추가되며, 각 프레임에는 한 프레임에 업데이트하고 그려야 하는 카드가 너무 많아 성능 병목 현상이 발생하는 것을 방지하기 위한 상한이 있습니다.

6.5.5.3 메시카드캡처

렌더링할 목록에 그리드 카드를 추가하는 방법을 분석한 후 카드를 캡처하는 특정 프로세스를 계속 분석할 수 있습니다.

cpp
// .
{
    FLumenCardPassParameters* PassParameters = GraphBuilder.AllocParameters<FLumenCardPassParameters>();
    // 뷰(View)정보 .
    PassParameters->View = Scene->UniformBuffers.LumenCardCaptureViewUniformBuffer;
    PassParameters->CardPass = GraphBuilder.CreateUniformBuffer(PassUniformParameters);
    // Atlas렌더링(Render)3개 : , 노멀(Normal), 발광(Emissive) .
    PassParameters->RenderTargets[0] = FRenderTargetBinding(AlbedoAtlasTexture, ERenderTargetLoadAction::ELoad);
    PassParameters->RenderTargets[1] = FRenderTargetBinding(NormalAtlasTexture, ERenderTargetLoadAction::ELoad);
    PassParameters->RenderTargets[2] = FRenderTargetBinding(EmissiveAtlasTexture, ERenderTargetLoadAction::ELoad);
    // 뎁스 버퍼 .
    PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(DepthStencilAtlasTexture, ERenderTargetLoadAction::ELoad, FExclusiveDepthStencil::DepthWrite_StencilNop);

    InstanceCullingResult.GetDrawParameters(PassParameters->InstanceCullingDrawParams);

    // Pass.
    GraphBuilder.AddPass(
        RDG_EVENT_NAME("MeshCardCapture"),
        PassParameters,
        ERDGPassFlags::Raster,
        [this, Scene = Scene, PrimitiveIdVertexBuffer, SharedView, &CardsToRender, PassParameters](FRHICommandList& RHICmdList)
        {
            QUICK_SCOPE_CYCLE_COUNTER(MeshPass);

            // 렌더링(Render)의 데이터 드로우/렌더(Draw).
            for (FCardRenderData& CardRenderData : CardsToRender)
            {
                if (CardRenderData.NumMeshDrawCommands > 0)
                {
                    FIntRect AtlasRect = CardRenderData.AtlasAllocation;
                    // 설정(Set)뷰포트(Viewport).
                    RHICmdList.SetViewport(AtlasRect.Min.X, AtlasRect.Min.Y, 0.0f, AtlasRect.Max.X, AtlasRect.Max.Y, 1.0f);

                    // 처리(Process)뷰(View)데이터 .
                    CardRenderData.PatchView(RHICmdList, Scene, SharedView);
                    Scene->UniformBuffers.LumenCardCaptureViewUniformBuffer.UpdateUniformBufferImmediate(*SharedView->CachedViewUniformShaderParameters);

                    FGraphicsMinimalPipelineStateSet GraphicsMinimalPipelineStateSet;
                #if GPUCULL_TODO
                    if (Scene->GPUScene.IsEnabled())
                    {
                        FRHIBuffer* DrawIndirectArgsBuffer = nullptr;
                        FRHIBuffer* InstanceIdOffsetBuffer = nullptr;
                        FInstanceCullingDrawParams& InstanceCullingDrawParams = PassParameters->InstanceCullingDrawParams;
                        if (InstanceCullingDrawParams.DrawIndirectArgsBuffer != nullptr && InstanceCullingDrawParams.InstanceIdOffsetBuffer != nullptr)
                        {
                            DrawIndirectArgsBuffer = InstanceCullingDrawParams.DrawIndirectArgsBuffer->GetRHI();
                            InstanceIdOffsetBuffer = InstanceCullingDrawParams.InstanceIdOffsetBuffer->GetRHI();
                        }

                        // GPU클리핑/클램핑(Clipping)호출(Call)GPUInstanced인터페이스 .
                        SubmitGPUInstancedMeshDrawCommandsRange(
                            LumenCardRenderer.MeshDrawCommands,
                            GraphicsMinimalPipelineStateSet,
                            CardRenderData.StartMeshDrawCommandIndex,
                            CardRenderData.NumMeshDrawCommands,
                            1,
                            InstanceIdOffsetBuffer,
                            DrawIndirectArgsBuffer,
                            RHICmdList);
                    }
                #endif // GPUCULL_TODO
                    (......)
                }
            }
        }
    );
}

카드 드로잉 단계에서 그리드 카드를 렌더링할 때 그리드 표면 속성의 투영은 각 그리드 카드에 대해 낮은 해상도로 서로 다른 방향에서 얻어집니다. 이러한 투영된 그리드 속성은 텍스처 아틀라스에 저장되지만 기존 렌더링 파이프라인과 달리 카드 뷰 범위 내 Nanite 그리드의 세 가지 속성(기본 색상, 일반 및 자체 조명)만 래스터라이제이션됩니다. (아래 사진)

카드 캡처 단계에서는 그리드 카드의 그리드 속성 아틀라스를 투영합니다. 위: 기본 컬러 아틀라스, 아래: 일반 아틀라스.

캡처 그리드 카드에서 사용되는 VS 및 PS는 다음과 같습니다.

cpp
// Engine\Shaders\Private\Lumen\LumenCardVertexShader.usf

struct FLumenCardInterpolantsVSToPS
{
};

struct FLumenCardVSToPS
{
    FVertexFactoryInterpolantsVSToPS FactoryInterpolants;
    FLumenCardInterpolantsVSToPS PassInterpolants;
    float4 Position : SV_POSITION;
};

// VS.
void Main(
    FVertexFactoryInput Input,
    OPTIONAL_VertexID
    out FLumenCardVSToPS Output
    )
{    
    uint EyeIndex = 0;
    ResolvedView = ResolveView();

    FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
    float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
    float4 WorldPosition = WorldPositionExcludingWPO;
    float4 ClipSpacePosition;

    float3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);    
    FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPosition.xyz, TangentToLocal);

    ISOLATE
    {
        // 머티리얼(Material)의 위치 .
        WorldPosition.xyz += GetMaterialWorldPositionOffset(VertexParameters);
        // 래스터라이제이션 의 위치 .
        float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPosition);
        // 위치 변환(Transform)까지 클리핑/클램핑(Clipping).
        ClipSpacePosition = INVARIANT(mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip));
        Output.Position = INVARIANT(ClipSpacePosition);
    }

    bool bClampToNearPlane = false;// GetPrimitiveData(Input.PrimitiveId).ObjectWorldPositionAndRadius.w < .5f * max();

    if (bClampToNearPlane && Output.Position.z < 0)
    {
        Output.Position.z = 0.01f;
        Output.Position.w = 1.0f;
    }

    Output.FactoryInterpolants = VertexFactoryGetInterpolantsVSToPS(Input, VFIntermediates, VertexParameters);
}

// Engine\Shaders\Private\Lumen\LumenCardPixelShader.usf

struct FLumenCardInterpolantsVSToPS
{
};

// PS.
void Main(
    FVertexFactoryInterpolantsVSToPS Interpolants,
    FLumenCardInterpolantsVSToPS PassInterpolants,
    in INPUT_POSITION_QUALIFIERS float4 SvPosition : SV_Position        // after all interpolators
    OPTIONAL_IsFrontFace,
    out float4 OutTarget0 : SV_Target0,
    out float4 OutTarget1 : SV_Target1,
    out float4 OutTarget2 : SV_Target2)
{
    ResolvedView = ResolveView();

    // 페치/가져오기(Fetch)머티리얼(Material)의 .
    FMaterialPixelParameters MaterialParameters = GetMaterialPixelParameters(Interpolants, SvPosition);
    FPixelMaterialInputs PixelMaterialInputs;
    
    // 계산/산출(Calculate)머티리얼(Material)의 .
    {
        float4 ScreenPosition = SvPositionToResolvedScreenPosition(SvPosition);
        float3 TranslatedWorldPosition = SvPositionToResolvedTranslatedWorld(SvPosition);
        CalcMaterialParametersEx(MaterialParameters, PixelMaterialInputs, SvPosition, ScreenPosition, bIsFrontFace, TranslatedWorldPosition, TranslatedWorldPosition);
    }

    // 페치/가져오기(Fetch)머티리얼(Material) 및 클리핑/클램핑(Clipping)데이터 .
    GetMaterialCoverageAndClipping(MaterialParameters, PixelMaterialInputs);

    float3 BaseColor = GetMaterialBaseColor(PixelMaterialInputs);
    float  Metallic = GetMaterialMetallic(PixelMaterialInputs);
    float  Specular = GetMaterialSpecular(PixelMaterialInputs);

    float Roughness = GetMaterialRoughness(PixelMaterialInputs);
    float Opacity = GetMaterialOpacity(PixelMaterialInputs);

    float3 DiffuseColor = BaseColor - BaseColor * Metallic;
    float3 SpecularColor = lerp(0.08 * Specular.xxx, BaseColor, Metallic.xxx);

    // 계산/산출(Calculate)의 .
    EnvBRDFApproxFullyRough(DiffuseColor, SpecularColor);

    // 저장(Store), 노멀(Normal), 발광(Emissive) .
    //@todo DynamicGI better encoding for low precision, hemispherical normal encoding
    OutTarget0 = float4(sqrt(DiffuseColor), Opacity);
    OutTarget1 = float4(MaterialParameters.WorldNormal * .5f + .5f, 0);
    OutTarget2 = float4(GetMaterialEmissive(PixelMaterialInputs), 0);
}

VS의 입력은 로컬 공간의 직육면체이고, VS의 출력은 클리핑 공간의 직육면체입니다.

PS 렌더링 후 데이터는 기본 색상, 일반 및 자체 조명의 세 가지 RT 아틀라스의 해당 위치에 저장됩니다. 여기서 VS 및 PS 로직은 기존 BasePass의 로직보다 훨씬 덜 복잡하다는 점에 유의해야 합니다. 이는 Lumen이 실시간으로 렌더링하는 중요한 최적화 조치 중 하나이기도 합니다.

또한, 새로운 카드를 Atlas 아틀라스에 렌더링하는 위치는 Bin Packing 문제로 해결할 수 있습니다. 렌더링할 때 시작점, 너비, 높이를 ViewPort로 설정하면 됩니다. 해당 유형은 FBinnedTextureLayout입니다. 다른 관련 유형으로는 FTextureLayout 및 FTextureLayout3d가 있습니다. 예를 들어 다음 프레임 샷에서 카드 ViewPort의 위치는 (0, 0)이고 너비와 높이가 (64, 64)입니다. 이는 너비와 높이가 64인 아틀라스의 전면 영역에 렌더링된다는 의미입니다.

그런데 그리드 카드의 그리기 지침은 FLumenCardMeshProcessor에서 처리됩니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneRendering.cpp

void FLumenCardMeshProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
    LLM_SCOPE_BYTAG(Lumen);

    if (MeshBatch.bUseForMaterial && DoesPlatformSupportLumenGI(GetFeatureLevelShaderPlatform(FeatureLevel)))
    {
        // 처리(Process)머티리얼(Material).
        const FMaterialRenderProxy* FallbackMaterialRenderProxyPtr = nullptr;
        const FMaterial& Material = MeshBatch.MaterialRenderProxy->GetMaterialWithFallback(FeatureLevel, FallbackMaterialRenderProxyPtr);

        const FMaterialRenderProxy& MaterialRenderProxy = FallbackMaterialRenderProxyPtr ? *FallbackMaterialRenderProxyPtr : *MeshBatch.MaterialRenderProxy;

        // 처리(Process)렌더링(Render)상태 .
        const EBlendMode BlendMode = Material.GetBlendMode();
        const FMaterialShadingModelField ShadingModels = Material.GetShadingModels();
        const bool bIsTranslucent = IsTranslucentBlendMode(BlendMode);
        const FMeshDrawingPolicyOverrideSettings OverrideSettings = ComputeMeshOverrideSettings(MeshBatch);
        const ERasterizerFillMode MeshFillMode = ComputeMeshFillMode(MeshBatch, Material, OverrideSettings);
        const ERasterizerCullMode MeshCullMode = ComputeMeshCullMode(MeshBatch, Material, OverrideSettings);

        if (!bIsTranslucent
            && (PrimitiveSceneProxy && PrimitiveSceneProxy->ShouldRenderInMainPass() && PrimitiveSceneProxy->AffectsDynamicIndirectLighting())
            && ShouldIncludeDomainInMeshPass(Material.GetMaterialDomain()))
        {
            // VS 및 PSshader
            const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;
            FVertexFactoryType* VertexFactoryType = VertexFactory->GetType();

            TMeshProcessorShaders<FLumenCardVS, FLumenCardPS> PassShaders;

            PassShaders.VertexShader = Material.GetShader<FLumenCardVS>(VertexFactoryType);
            PassShaders.PixelShader = Material.GetShader<FLumenCardPS>(VertexFactoryType);

            FMeshMaterialShaderElementData ShaderElementData;
            ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand, PrimitiveSceneProxy, MeshBatch, StaticMeshId, false);

            const FMeshDrawCommandSortKey SortKey = CalculateMeshStaticSortKey(PassShaders.VertexShader, PassShaders.PixelShader);

            // 드로우/렌더(Draw)
            BuildMeshDrawCommands(
                MeshBatch,
                BatchElementMask,
                PrimitiveSceneProxy,
                MaterialRenderProxy,
                Material,
                PassDrawRenderState,
                PassShaders,
                MeshFillMode,
                MeshCullMode,
                SortKey,
                EMeshPassFeatures::Default,
                ShaderElementData);
        }
    }
}

6.5.5.4 RasterizeLumenCard

Lumen 카드를 래스터라이제이션하는 논리는 다음과 같습니다.

cpp
if (UseNanite(ShaderPlatform) && ViewFamily.EngineShowFlags.NaniteMeshes && bAnyNaniteMeshes)
{
    (......)

    Nanite::FRasterContext RasterContext = Nanite::InitRasterContext(...);

    (......)

    Nanite::FCullingContext CullingContext = Nanite::InitCullingContext(...);

    if (GLumenSceneNaniteMultiViewCapture) // 뷰(View)드로우/렌더(Draw)모델
    {
        const uint32 NumCardsToRender = CardsToRender.Num();

        // 뷰(View), 회 의 개수 .
        uint32 NextCardIndex = 0;
        while(NextCardIndex < NumCardsToRender)
        {
            TArray<Nanite::FPackedView, SceneRenderingAllocator> NaniteViews;
            TArray<Nanite::FInstanceDraw, SceneRenderingAllocator> NaniteInstanceDraws;

            while(NextCardIndex < NumCardsToRender && NaniteViews.Num() < MAX_VIEWS_PER_CULL_RASTERIZE_PASS)
            {
                const FCardRenderData& CardRenderData = CardsToRender[NextCardIndex];

                if(CardRenderData.NaniteInstanceIds.Num() > 0)
                {
                    for(uint32 InstanceID : CardRenderData.NaniteInstanceIds)
                    {
                        NaniteInstanceDraws.Add(Nanite::FInstanceDraw { InstanceID, (uint32)NaniteViews.Num() });
                    }

                    Nanite::FPackedViewParams Params;
                    Params.ViewMatrices = CardRenderData.ViewMatrices;
                    Params.PrevViewMatrices = CardRenderData.ViewMatrices;
                    Params.ViewRect = CardRenderData.AtlasAllocation;
                    Params.RasterContextSize = DepthStencilAtlasSize;
                    Params.LODScaleFactor = CardRenderData.NaniteLODScaleFactor;
                    NaniteViews.Add(Nanite::CreatePackedView(Params));
                }

                NextCardIndex++;
            }

            // 인스턴스 드로우/렌더(Draw).
            if (NaniteInstanceDraws.Num() > 0)
            {
                RDG_EVENT_SCOPE(GraphBuilder, "Nanite::RasterizeLumenCards");

                Nanite::FRasterState RasterState;
                Nanite::CullRasterize(
                    GraphBuilder,
                    *Scene,
                    NaniteViews,
                    CullingContext,
                    RasterContext,
                    RasterState,
                    &NaniteInstanceDraws
                );
            }
        }
    }
    else // 뷰(View).
    {
        (......)
    }
    
    extern float GLumenDistantSceneMinInstanceBoundsRadius;

    // 렌더링(Render)의 .
    for (FCardRenderData& CardRenderData : CardsToRender)
    {
        if (CardRenderData.bDistantScene)
        {
            (......)
        }
    }

    // 드로우/렌더(Draw)Lumen의 .
    Nanite::DrawLumenMeshCapturePass(
        GraphBuilder,
        *Scene,
        SharedView,
        CardsToRender,
        CullingContext,
        RasterContext,
        PassUniformParameters,
        RectMinMaxBufferSRV,
        NumRects,
        LumenSceneData.MaxAtlasSize,
        AlbedoAtlasTexture,
        NormalAtlasTexture,
        EmissiveAtlasTexture,
        DepthStencilAtlasTexture
    );
}

카드 래스터라이제이션 단계는 기본적으로 Nanite 프로세스와 동일합니다.

가시성, 깊이 템플릿 버퍼, 삼각형 ID 및 기타 정보를 포함하여 래스터라이제이션 후의 출력 결과도 일관됩니다.

다음 단계는 그리드 카드를 그리는 것입니다. 이 단계는 기본적으로 Nanite와 동일합니다.

출력 GBuffer는 여전히 위에서 언급한 기본 색상, 일반 및 자체 조명의 세 가지 아틀라스이지만 빈 영역에 연결됩니다.

6.5.6 루멘 장면 조명

6.5.6.1 복셀 콘 추적

다음 섹션에서는 **복셀 원뿔 추적(Voxel Cone Tracing)**에 대한 추가 지식을 포함합니다. 이 섹션에서는 먼저 관련 지식을 추가합니다. 이 논문은 실시간 전역 조명을 위한 복셀 원뿔 추적 및 복셀 원뿔 추적 및 희소 복셀 옥트리를 사용한 대화형 간접 조명을 기반으로 합니다.

장면에서 Voxel Cone Tracing을 수행하는 첫 번째 단계는 장면 개체의 희소 복셀 옥트리(Sparse Voxel Octree)를 구성하는 것입니다. UE5는 희소 HLOD 그리드 거리 필드를 사용합니다.

다음 그림은 복셀화 후 Sponza 장면을 보여줍니다.

UE와 같은 렌더링 엔진은 일반적으로 하이브리드 렌더링 파이프라인을 사용합니다. 직접광(1차 광선)은 전통적인 래스터라이제이션를 사용하여 얻고 2차 광선은 원뿔 추적을 사용하여 얻습니다.

복셀 콘 추적 전에 형상이 사전 필터링된 다음 참조 매체로 추적됩니다(볼륨 레이 캐스팅 사용 가능). 복셀은 불투명 필드 + 입사 광도를 사용하여 장면 개체를 나타내므로 Quadrilinear 보간 샘플링을 사용하여 원뿔 광선으로 덮힌 발자국을 시뮬레이션할 수 있습니다.

위 단계의 단일 척추 레이 트레이싱에는 MIP 매핑을 사용해야 합니다. MIP 매핑 생성에서는 가우스 가중치를 사용합니다. 즉, 복셀 중심의 가중치가 가장 크고, 복셀 중심에서 멀어질수록 가중치는 작아집니다.

가우시안 가중치를 사용하여 생성된 MIP 맵의 수준이 높을수록 더 흐려지고 이는 원뿔의 모양과 일치할 수 있습니다. 원뿔 광선이 시작점에서 멀수록 범위가 넓어지고 받는 빛이 더 흐려집니다. 이 전제 하에서 레벨에 해당하는 MIP 맵은 원뿔 몸체 광선의 교차점과 시작점 사이의 거리를 기반으로 4선형으로 샘플링되어 원뿔 몸체 광선의 교차점의 방사율을 빠르게 얻을 수 있습니다.

Voxel의 렌더링 프로세스는 세 가지 패스로 나눌 수 있습니다. 첫 번째 패스는 조명 및 구운 방사조도(반사 그림자 맵, RSM)입니다. 두 번째 패스는 희소 옥트리를 사용하여 광도를 다운샘플링하는 사전 필터링입니다. 세 번째 패스는 눈에 보이는 각 조각(픽셀)의 방사조도를 수집하는 카메라 패스입니다. (아래 사진)

마찬가지로 복셀 추적은 스페큘러 반사, AO 및 부드러운 그림자에도 사용할 수 있습니다. 스페큘러 반사의 경우 생성된 스페큘러 반사 원뿔의 수가 적고 범위가 작은 점을 제외하면 유사한 추적 방법을 사용할 수 있습니다.

실제로 원뿔 추적에서는 거칠기가 다른 표면에서 추적을 위해 다양한 수와 크기의 원뿔을 구성할 수 있습니다.

왼쪽: 거칠기가 높은 표면, 즉 디퓨즈에는 다중 원뿔 추적이 필요합니다. 중간: 거친 스페큘러 반사, 추적하는 데 더 큰 각도를 가진 원뿔 하나만 필요합니다. 오른쪽: 낮은 러프니스 스페큘러 반사, 추적하는 데 더 작은 각도를 가진 원뿔 하나만 필요합니다.

AO의 경우 근거리 다중 샘플링 콘 추적 + 원거리 AO + 오프라인 폐색의 포괄적인 방법이 사용됩니다.

부드러운 그림자의 경우 원뿔 하나당 하나의 픽셀을 샘플링하여 더 부드럽고 효율적인 계산 효과를 얻을 수 있습니다.

또한 이 논문에서는 복셀화를 달성하기 위해 하나의 패스만 사용하는 기술과 컴퓨트 셰이더를 사용하여 희소 옥트리를 구축하는 기술 및 프로세스에 대해 언급합니다.

6.5.6.2 RenderLumenSceneLighting

Lumen의 장면 조명은 RenderLumenSceneLighting에 의해 처리됩니다. 해당 코드는 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneLighting.cpp

void FDeferredShadingSceneRenderer::RenderLumenSceneLighting(
    FRDGBuilder& GraphBuilder,
    FViewInfo& View)
{
    FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;
    // 검사/감지(Detect)는 활성화(Enable)Lumen: 디퓨즈(Diffuse) 또는 반사(Reflection)의 내에서 개 는 Lumen즉, .
    const bool bAnyLumenEnabled = GetViewPipelineState(Views[0]).DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen 
        || GetViewPipelineState(Views[0]).ReflectionsMethod == EReflectionsMethod::Lumen;

    if (bAnyLumenEnabled)
    {
        RDG_EVENT_SCOPE(GraphBuilder, "LumenSceneLighting");

        FGlobalShaderMap* GlobalShaderMap = View.ShaderMap;
        FLumenCardTracingInputs TracingInputs(GraphBuilder, Scene, Views[0]);

        if (LumenSceneData.VisibleCardsIndices.Num() > 0)
        {
            FRDGTextureRef RadiosityAtlas = GraphBuilder.RegisterExternalTexture(LumenSceneData.RadiosityAtlas, TEXT("Lumen.RadiosityAtlas"));

            // 렌더링(Render).
            RenderRadiosityForLumenScene(GraphBuilder, TracingInputs, GlobalShaderMap, RadiosityAtlas);

            ConvertToExternalTexture(GraphBuilder, RadiosityAtlas, LumenSceneData.RadiosityAtlas);

            FLumenCardScatterContext DirectLightingCardScatterContext;
            extern float GLumenSceneCardDirectLightingUpdateFrequencyScale;

            // 파라미터 기록/쓰기(Write)의 ,을(를) 활용하여 갱신(Update)의 직접광(Direct Lighting).
            DirectLightingCardScatterContext.Init(
                GraphBuilder,
                View,
                LumenSceneData,
                LumenCardRenderer,
                ECullCardsMode::OperateOnSceneForceUpdateForCardsToRender,
                1);

            // 클리핑/클램핑(Clipping)까지 .
            DirectLightingCardScatterContext.CullCardsToShape(
                GraphBuilder,
                View,
                LumenSceneData,
                LumenCardRenderer,
                TracingInputs.LumenCardSceneUniformBuffer,
                ECullCardsShapeType::None,
                FCullCardsShapeParameters(),
                GLumenSceneCardDirectLightingUpdateFrequencyScale,
                0);

            // 산란 파라미터 .
            DirectLightingCardScatterContext.BuildScatterIndirectArgs(
                GraphBuilder,
                View);

            extern int32 GLumenSceneRecaptureLumenSceneEveryFrame;

            // 라이팅 의 : , , .
            if (GLumenSceneRecaptureLumenSceneEveryFrame)
            {
                ClearAtlasRDG(GraphBuilder, TracingInputs.FinalLightingAtlas);
                if (Lumen::UseIrradianceAtlas(View))
                {
                    ClearAtlasRDG(GraphBuilder, TracingInputs.IrradianceAtlas);
                }
                if (Lumen::UseIndirectIrradianceAtlas(View))
                {
                    ClearAtlasRDG(GraphBuilder, TracingInputs.IndirectIrradianceAtlas);
                }
            }

            // 라이팅 .
            CombineLumenSceneLighting(
                Scene,
                View,
                GraphBuilder,
                TracingInputs.LumenCardSceneUniformBuffer,
                TracingInputs.FinalLightingAtlas,
                TracingInputs.OpacityAtlas,
                RadiosityAtlas,
                GlobalShaderMap, 
                DirectLightingCardScatterContext);

            // TracingInputs.FinalLightingAtlas의 데이터 까지 TracingInputs.IndirectIrradianceAtlas.
            if (Lumen::UseIndirectIrradianceAtlas(View))
            {
                CopyLumenCardAtlas(
                    Scene,
                    View,
                    GraphBuilder,
                    TracingInputs.LumenCardSceneUniformBuffer,
                    TracingInputs.FinalLightingAtlas,
                    TracingInputs.IndirectIrradianceAtlas,
                    GlobalShaderMap,
                    DirectLightingCardScatterContext);
            }

            // 렌더링(Render)Lumen의 라이팅 .
            RenderDirectLightingForLumenScene(
                GraphBuilder,
                TracingInputs.LumenCardSceneUniformBuffer,
                TracingInputs.FinalLightingAtlas,
                TracingInputs.OpacityAtlas,
                GlobalShaderMap,
                DirectLightingCardScatterContext);

            if (Lumen::UseIrradianceAtlas(View))
            {
                CopyLumenCardAtlas(
                    Scene,
                    View,
                    GraphBuilder,
                    TracingInputs.LumenCardSceneUniformBuffer,
                    TracingInputs.FinalLightingAtlas,
                    TracingInputs.IrradianceAtlas,
                    GlobalShaderMap,
                    DirectLightingCardScatterContext);
            }

            FRDGTextureRef AlbedoAtlas = GraphBuilder.RegisterExternalTexture(LumenSceneData.AlbedoAtlas, TEXT("Lumen.AlbedoAtlas"));
            FRDGTextureRef EmissiveAtlas = GraphBuilder.RegisterExternalTexture(LumenSceneData.EmissiveAtlas, TEXT("Lumen.EmissiveAtlas"));
            // 적용 Lumen의 .
            ApplyLumenCardAlbedo(
                Scene,
                View,
                GraphBuilder,
                TracingInputs.LumenCardSceneUniformBuffer,
                TracingInputs.FinalLightingAtlas,
                AlbedoAtlas,
                EmissiveAtlas,
                GlobalShaderMap,
                DirectLightingCardScatterContext);

            LumenSceneData.bFinalLightingAtlasContentsValid = true;

            // 필터링 라이팅 .
            PrefilterLumenSceneLighting(GraphBuilder, View, TracingInputs, GlobalShaderMap, DirectLightingCardScatterContext);

            ConvertToExternalTexture(GraphBuilder, TracingInputs.FinalLightingAtlas, LumenSceneData.FinalLightingAtlas);
            if (Lumen::UseIrradianceAtlas(View))
            {
                ConvertToExternalTexture(GraphBuilder, TracingInputs.IrradianceAtlas, LumenSceneData.IrradianceAtlas);
            }
            if (Lumen::UseIndirectIrradianceAtlas(View))
            {
                ConvertToExternalTexture(GraphBuilder, TracingInputs.IndirectIrradianceAtlas, LumenSceneData.IndirectIrradianceAtlas);
            }
        }

        // 계산/산출(Calculate)Voxel라이팅 .
        ComputeLumenSceneVoxelLighting(GraphBuilder, TracingInputs, GlobalShaderMap);

        // 반투명(Translucent)GI.
        ComputeLumenTranslucencyGIVolume(GraphBuilder, TracingInputs, GlobalShaderMap);
    }
}

RenderDoc의 스크린샷은 위 프로세스를 한눈에 보여줍니다.

다음 하위 섹션에서는 일부 주요 단계에 대한 분석을 수행합니다.

6.5.6.3 RenderRadiosityForLumenScene

RenderRadiosityForLumenScene의 논리는 루멘 장면의 빛을 렌더링하는 것입니다. 코드는 다음과 같습니다:

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenRadiosity.cpp

void FDeferredShadingSceneRenderer::RenderRadiosityForLumenScene(
    FRDGBuilder& GraphBuilder, 
    const FLumenCardTracingInputs& TracingInputs, 
    FGlobalShaderMap* GlobalShaderMap, 
    FRDGTextureRef RadiosityAtlas)
{
    LLM_SCOPE_BYTAG(Lumen);

    const FViewInfo& MainView = Views[0];
    FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;

    extern int32 GLumenSceneRecaptureLumenSceneEveryFrame;

    if (IsRadiosityEnabled() 
        && !GLumenSceneRecaptureLumenSceneEveryFrame
        && LumenSceneData.bFinalLightingAtlasContentsValid
        && TracingInputs.NumClipmapLevels > 0)
    {
        RDG_EVENT_SCOPE(GraphBuilder, "Radiosity");

        FLumenCardScatterContext VisibleCardScatterContext;

        // 파라미터 기록/쓰기(Write)의 ,을(를) 활용하여 갱신(Update)의 직접광(Direct Lighting).
        VisibleCardScatterContext.Init(
            GraphBuilder,
            MainView,
            LumenSceneData,
            LumenCardRenderer,
            ECullCardsMode::OperateOnSceneForceUpdateForCardsToRender);

        VisibleCardScatterContext.CullCardsToShape(
            GraphBuilder,
            MainView,
            LumenSceneData,
            LumenCardRenderer,
            TracingInputs.LumenCardSceneUniformBuffer,
            ECullCardsShapeType::None,
            FCullCardsShapeParameters(),
            GLumenSceneCardRadiosityUpdateFrequencyScale,
            0);

        // 산란 파라미터 .
        VisibleCardScatterContext.BuildScatterIndirectArgs(
            GraphBuilder,
            MainView);

        // 샘플링 포인트.
        RadiosityDirections.GenerateSamples(
            FMath::Clamp(GLumenRadiosityNumTargetCones, 1, (int32)MaxRadiosityConeDirections),
            1,
            GLumenRadiosityNumTargetCones,
            false,
            true /* Cosine distribution */);

        const bool bRenderSkylight = Lumen::ShouldHandleSkyLight(Scene, ViewFamily);

        // 렌더링(Render)의 산란 .
        if (GLumenRadiosityComputeTraceBlocksScatter) // CS
        {
            RenderRadiosityComputeScatter(
                GraphBuilder,
                Scene,
                Views[0],
                bRenderSkylight,
                LumenSceneData,
                RadiosityAtlas,
                TracingInputs,
                VisibleCardScatterContext.Parameters,
                GlobalShaderMap);
        }
        else // PS
        {
            FLumenCardRadiosity* PassParameters = GraphBuilder.AllocParameters<FLumenCardRadiosity>();

            PassParameters->RenderTargets[0] = FRenderTargetBinding(RadiosityAtlas, ERenderTargetLoadAction::ENoAction);

            PassParameters->VS.LumenCardScene = TracingInputs.LumenCardSceneUniformBuffer;
            PassParameters->VS.CardScatterParameters = VisibleCardScatterContext.Parameters;
            PassParameters->VS.ScatterInstanceIndex = 0;
            PassParameters->VS.CardUVSamplingOffset = FVector2D::ZeroVector;

            SetupTraceFromTexelParameters(Views[0], TracingInputs, LumenSceneData, PassParameters->PS.TraceFromTexelParameters);

            FLumenCardRadiosityPS::FPermutationDomain PermutationVector;
            PermutationVector.Set<FLumenCardRadiosityPS::FDynamicSkyLight>(bRenderSkylight);
            auto PixelShader = GlobalShaderMap->GetShader<FLumenCardRadiosityPS>(PermutationVector);

            FScene* LocalScene = Scene;
            const int32 RadiosityDownsampleArea = GLumenRadiosityDownsampleFactor * GLumenRadiosityDownsampleFactor;

            // 로부터 내에서 .
            GraphBuilder.AddPass(
                RDG_EVENT_NAME("TraceFromAtlasTexels: %u Cones", RadiosityDirections.SampleDirections.Num()),
                PassParameters,
                ERDGPassFlags::Raster,
                [LocalScene, PixelShader, PassParameters, GlobalShaderMap](FRHICommandListImmediate& RHICmdList)
            {
                FIntPoint ViewRect = FIntPoint::DivideAndRoundDown(LocalScene->LumenSceneData->MaxAtlasSize, GLumenRadiosityDownsampleFactor);
                DrawQuadsToAtlas(ViewRect, PixelShader, PassParameters, GlobalShaderMap, TStaticBlendState<>::GetRHI(), RHICmdList);
            });
        }
    }
    else
    {
        ClearAtlasRDG(GraphBuilder, RadiosityAtlas);
    }
}

위 코드의 마지막 단계는 방사능을 계산하는 것입니다. 일반적으로 CS 모드 RenderRadiosityComputeScatter로 들어갑니다. 코드 분석을 입력해 보겠습니다.

cpp
void RenderRadiosityComputeScatter(
    FRDGBuilder& GraphBuilder,
    const FScene* Scene,
    const FViewInfo& View,
    bool bRenderSkylight, 
    const FLumenSceneData& LumenSceneData,
    FRDGTextureRef RadiosityAtlas,
    const FLumenCardTracingInputs& TracingInputs,
    const FLumenCardScatterParameters& CardScatterParameters,
    FGlobalShaderMap* GlobalShaderMap)
{
    const bool bUseIrradianceCache = GLumenRadiosityUseIrradianceCache != 0;

    // 의 파라미터 .
    FRDGBufferRef SetupCardTraceBlocksIndirectArgsBuffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateIndirectDesc<FRHIDispatchIndirectParameters>(1), TEXT("SetupCardTraceBlocksIndirectArgsBuffer"));
    {
        FRDGBufferUAVRef SetupCardTraceBlocksIndirectArgsBufferUAV = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(SetupCardTraceBlocksIndirectArgsBuffer));

        FPlaceProbeIndirectArgsCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FPlaceProbeIndirectArgsCS::FParameters>();
        PassParameters->RWIndirectArgs = SetupCardTraceBlocksIndirectArgsBufferUAV;
        PassParameters->QuadAllocator = CardScatterParameters.QuadAllocator;

        auto ComputeShader = GlobalShaderMap->GetShader< FPlaceProbeIndirectArgsCS >(0);

        ensure(GSetupCardTraceBlocksGroupSize == GPlaceRadiosityProbeGroupSize);
        const FIntVector GroupSize(1, 1, 1);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("SetupCardTraceBlocksIndirectArgsCS"),
            ComputeShader,
            PassParameters,
            GroupSize);
    }

    const int32 TraceBlockMaxSize = 2;
    extern int32 GLumenSceneCardLightingForceFullUpdate;
    const int32 Divisor = TraceBlockMaxSize * GLumenRadiosityDownsampleFactor * (GLumenSceneCardLightingForceFullUpdate ? 1 : GLumenRadiosityTraceBlocksAllocationDivisor);
    const int32 NumTraceBlocksToAllocate = (LumenSceneData.MaxAtlasSize.X / Divisor) 
        * (LumenSceneData.MaxAtlasSize.Y / Divisor);

    FRDGBufferRef CardTraceBlockAllocator = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(sizeof(uint32), 1), TEXT("CardTraceBlockAllocator"));
    FRDGBufferRef CardTraceBlockData = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(sizeof(FIntVector4), NumTraceBlocksToAllocate), TEXT("CardTraceBlockData"));
    FRDGBufferUAVRef CardTraceBlockAllocatorUAV = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(CardTraceBlockAllocator, PF_R32_UINT));
    FRDGBufferUAVRef CardTraceBlockDataUAV = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(CardTraceBlockData, PF_R32G32B32A32_UINT));

    FComputeShaderUtils::ClearUAV(GraphBuilder, View.ShaderMap, CardTraceBlockAllocatorUAV, 0);

    // .
    {
        FSetupCardTraceBlocksCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FSetupCardTraceBlocksCS::FParameters>();
        PassParameters->RWCardTraceBlockAllocator = CardTraceBlockAllocatorUAV;
        PassParameters->RWCardTraceBlockData = CardTraceBlockDataUAV;
        PassParameters->QuadAllocator = CardScatterParameters.QuadAllocator;
        PassParameters->QuadData = CardScatterParameters.QuadData;
        PassParameters->CardBuffer = LumenSceneData.CardBuffer.SRV;
        PassParameters->RadiosityAtlasSize = FIntPoint::DivideAndRoundDown(LumenSceneData.MaxAtlasSize, GLumenRadiosityDownsampleFactor);
        PassParameters->IndirectArgs = SetupCardTraceBlocksIndirectArgsBuffer;

        auto ComputeShader = GlobalShaderMap->GetShader<FSetupCardTraceBlocksCS>();

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("SetupCardTraceBlocksCS"),
            ComputeShader,
            PassParameters,
            SetupCardTraceBlocksIndirectArgsBuffer,
            0);
    }

    // 파라미터 .
    FRDGBufferRef TraceBlocksIndirectArgsBuffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateIndirectDesc<FRHIDispatchIndirectParameters>(1), TEXT("TraceBlocksIndirectArgsBuffer"));
    {
        FRDGBufferUAVRef TraceBlocksIndirectArgsBufferUAV = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(TraceBlocksIndirectArgsBuffer));

        FTraceBlocksIndirectArgsCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTraceBlocksIndirectArgsCS::FParameters>();
        PassParameters->RWIndirectArgs = TraceBlocksIndirectArgsBufferUAV;
        PassParameters->CardTraceBlockAllocator = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(CardTraceBlockAllocator, PF_R32_UINT));

        FTraceBlocksIndirectArgsCS::FPermutationDomain PermutationVector;
        PermutationVector.Set<FTraceBlocksIndirectArgsCS::FIrradianceCache>(bUseIrradianceCache);
        auto ComputeShader = GlobalShaderMap->GetShader< FTraceBlocksIndirectArgsCS >(PermutationVector);

        const FIntVector GroupSize(1, 1, 1);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("TraceBlocksIndirectArgsCS"),
            ComputeShader,
            PassParameters,
            GroupSize);
    }

    LumenRadianceCache::FRadianceCacheInterpolationParameters RadianceCacheParameters;

    // 렌더링(Render)캐싱(Cache).
    if (bUseIrradianceCache)
    {
        const LumenRadianceCache::FRadianceCacheInputs RadianceCacheInputs = LumenRadiosity::SetupRadianceCacheInputs();

        FRadiosityMarkUsedProbesData MarkUsedProbesData;
        MarkUsedProbesData.Parameters.View = View.ViewUniformBuffer;
        MarkUsedProbesData.Parameters.DepthAtlas = LumenSceneData.DepthAtlas->GetRenderTargetItem().ShaderResourceTexture;
        MarkUsedProbesData.Parameters.CurrentOpacityAtlas = LumenSceneData.OpacityAtlas->GetRenderTargetItem().ShaderResourceTexture;
        MarkUsedProbesData.Parameters.CardTraceBlockAllocator = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(CardTraceBlockAllocator, PF_R32_UINT));
        MarkUsedProbesData.Parameters.CardTraceBlockData = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(CardTraceBlockData, PF_R32G32B32A32_UINT));
        MarkUsedProbesData.Parameters.CardBuffer = LumenSceneData.CardBuffer.SRV;
        MarkUsedProbesData.Parameters.RadiosityAtlasSize = FIntPoint::DivideAndRoundDown(LumenSceneData.MaxAtlasSize, GLumenRadiosityDownsampleFactor);
        MarkUsedProbesData.Parameters.IndirectArgs = TraceBlocksIndirectArgsBuffer;

        RenderRadianceCache(
            GraphBuilder, 
            TracingInputs, 
            RadianceCacheInputs, 
            Scene,
            View, 
            nullptr, 
            nullptr, 
            FMarkUsedRadianceCacheProbes::CreateStatic(&RadianceCacheMarkUsedProbes), 
            &MarkUsedProbesData, 
            View.ViewState->RadiosityRadianceCacheState, 
            RadianceCacheParameters);
    }

    // 로부터 내에서 의 .
    {
        FLumenCardRadiosityTraceBlocksCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FLumenCardRadiosityTraceBlocksCS::FParameters>();
        PassParameters->RWRadiosityAtlas = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(RadiosityAtlas));
        PassParameters->RadianceCacheParameters = RadianceCacheParameters;
        PassParameters->CardTraceBlockAllocator = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(CardTraceBlockAllocator, PF_R32_UINT));
        PassParameters->CardTraceBlockData = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(CardTraceBlockData, PF_R32G32B32A32_UINT));
        PassParameters->ProbeOcclusionNormalBias = GLumenRadiosityIrradianceCacheProbeOcclusionNormalBias;
        PassParameters->IndirectArgs = TraceBlocksIndirectArgsBuffer;

        SetupTraceFromTexelParameters(View, TracingInputs, LumenSceneData, PassParameters->TraceFromTexelParameters);

        FLumenCardRadiosityTraceBlocksCS::FPermutationDomain PermutationVector;
        PermutationVector.Set<FLumenCardRadiosityTraceBlocksCS::FDynamicSkyLight>(bRenderSkylight);
        PermutationVector.Set<FLumenCardRadiosityTraceBlocksCS::FIrradianceCache>(bUseIrradianceCache);
        auto ComputeShader = GlobalShaderMap->GetShader< FLumenCardRadiosityTraceBlocksCS >(PermutationVector);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("TraceFromAtlasTexels: %u Cones", RadiosityDirections.SampleDirections.Num()),
            ComputeShader,
            PassParameters,
            TraceBlocksIndirectArgsBuffer,
            0);
    }
}

자르기, 추적 매개변수 구축, 아틀라스 텍셀 추적 등을 포함하여 방사능을 계산하는 많은 프로세스가 있음을 알 수 있습니다.

텍셀 추적의 마지막 단계는 주로 샘플링 방향을 구성하는 것입니다. 각 샘플링 방향은 근처의 빛을 추적하기 위해 원뿔(Cone)을 구성합니다. 입력 매개변수에는 주로 전역 거리 필드 아틀라스, 장면 깊이, 장면 투명도, 장면 노멀, VoxelLighting 및 기타 데이터가 포함됩니다.

카드 텍셀을 추적하는 데 필요한 데이터: 왼쪽 위의 전역 거리 필드 아틀라스, 오른쪽 위의 장면 깊이 아틀라스, 왼쪽 아래의 장면 투명도, 오른쪽 아래의 장면 노멀.

출력은 장면 복사 아틀라스입니다.

해당 CS 셰이더 코드는 다음과 같습니다.

hlsl
// Engine\Shaders\Private\Lumen\LumenRadiosity.usf

float ProbeOcclusionNormalBias;
// 을(를) 활용하여 스레드 그룹(Thread Group)의 라이팅 결과 , ※ 주의: 는 groupshared의 .
groupshared float3 ThreadLighting[THREADGROUP_SIZE];

[numthreads(THREADGROUP_SIZE, 1, 1)]
void LumenCardRadiosityTraceBlocksCS(
    uint3 DispatchThreadId : SV_DispatchThreadID,
    uint3 GroupThreadId : SV_GroupThreadID)
{
#if IRRADIANCE_CACHE // 캐싱(Cache)
    uint ThreadIndex = DispatchThreadId.x;

    uint GlobalBlockIndex = ThreadIndex / (CARD_TRACE_BLOCK_SIZE * CARD_TRACE_BLOCK_SIZE);

    if (GlobalBlockIndex < CardTraceBlockAllocator[0])
    {
        // 계산/산출(Calculate).
        uint TexelIndexInBlock = ThreadIndex % (CARD_TRACE_BLOCK_SIZE * CARD_TRACE_BLOCK_SIZE);
        uint2 TexelOffsetInBlock = uint2(TexelIndexInBlock % CARD_TRACE_BLOCK_SIZE, TexelIndexInBlock / CARD_TRACE_BLOCK_SIZE);

        // 페치/가져오기(Fetch)데이터 .
        uint4 TraceBlockData = CardTraceBlockData[GlobalBlockIndex];
        uint CardId = TraceBlockData.x;
        uint ProbeIndex = TraceBlockData.y;
        uint BlockIndex = TraceBlockData.z;

        // 페치/가져오기(Fetch)데이터 .
        FLumenCardData CardData = GetLumenCardData(CardId, CardBuffer);

        float2 CardSizeTexels = abs(CardData.LocalExtent.xy * 2 * CardData.LocalPositionToAtlasUVScale * RadiosityAtlasSize);
        uint2 NumBlocksXY = ((uint2)CardSizeTexels + CARD_TRACE_BLOCK_SIZE - 1) / CARD_TRACE_BLOCK_SIZE;
        uint2 BlockOffset = uint2(BlockIndex % NumBlocksXY.x, BlockIndex / NumBlocksXY.x);
        float2 TexelCoord = BlockOffset * CARD_TRACE_BLOCK_SIZE + TexelOffsetInBlock;

        if (all(TexelCoord < CardSizeTexels))
        {
            // 계산/산출(Calculate)UV.
            float2 CardUV = (TexelCoord + .5f) / (float2)CardSizeTexels;
            float2 CardUVToAtlasScale = GetCardUVToAtlasScale(CardData.LocalPositionToAtlasUVScale, CardData.LocalExtent);
            float2 CardUVToAtlasBias = GetCardUVToAtlasBias(CardUVToAtlasScale, CardData.LocalPositionToAtlasUVBias);
            float2 AtlasUV = CardUV * CardUVToAtlasScale + CardUVToAtlasBias;

            float Opacity = Texture2DSampleLevel(CurrentOpacityAtlas, GlobalBilinearClampedSampler, AtlasUV, 0).x;

            float3 DiffuseLighting = 0;

            // 반투명(Translucent)0의 .
            if (Opacity > 0)
            {
                float Depth = 1.0f - Texture2DSampleLevel(DepthAtlas, GlobalBilinearClampedSampler, AtlasUV, 0).x;

                float3 LocalPosition;
                LocalPosition.xy = (AtlasUV - CardData.LocalPositionToAtlasUVBias) / CardData.LocalPositionToAtlasUVScale;
                LocalPosition.z = -CardData.LocalExtent.z + Depth * 2 * CardData.LocalExtent.z;

                // 계산/산출(Calculate)의 위치 및 노멀(Normal).
                float3 WorldPosition = mul(CardData.WorldToLocalRotation, LocalPosition) + CardData.Origin;
                float3 WorldNormal = normalize(Texture2DSampleLevel(NormalAtlas, GlobalBilinearClampedSampler, AtlasUV, 0).xyz * 2 - 1);
                uint ClipmapIndex = GetRadianceProbeClipmap(WorldPosition);

                // 계산/산출(Calculate)디퓨즈(Diffuse)라이팅 . 만약 클리핑/클램핑(Clipping)유효(Valid), 이면 로부터 내에서 보간(Interpolation).
                if (ClipmapIndex < NumRadianceProbeClipmaps)
                {
                    float3 BiasOffset = WorldNormal * ProbeOcclusionNormalBias;
                    // 로부터 RadianceProbeIndirectionTexture샘플링계산/산출(Calculate)디퓨즈(Diffuse).
                    DiffuseLighting = SampleIrradianceCacheInterpolated(WorldPosition, WorldNormal, BiasOffset, ClipmapIndex);
                }
                else // 유효(Valid)클리핑/클램핑(Clipping), 로부터 스카이라이트(Skylight)의 내에서 계산/산출(Calculate)디퓨즈(Diffuse).
                {
                    DiffuseLighting = GetSkySHDiffuse(WorldNormal) * View.SkyLightColor.rgb;
                }
            }

            // 저장(Store).
            uint2 AtlasCoord = uint2(AtlasUV * RadiosityAtlasSize);
            RWRadiosityAtlas[AtlasCoord] = float4(DiffuseLighting * PI, 0);
        }
    }
#else // 캐싱(Cache)
    ThreadLighting[GroupThreadId.x] = 0;

    uint ThreadIndex = DispatchThreadId.x;
    uint GlobalBlockIndex = ThreadIndex / (CARD_TRACE_BLOCK_SIZE * CARD_TRACE_BLOCK_SIZE * THREADS_PER_RADIOSITY_TEXEL);
    int2 AtlasCoord = -1;

    if (GlobalBlockIndex < CardTraceBlockAllocator[0])
    {
        uint TexelIndexInBlock = (ThreadIndex / THREADS_PER_RADIOSITY_TEXEL) % (CARD_TRACE_BLOCK_SIZE * CARD_TRACE_BLOCK_SIZE);
        uint2 TexelOffsetInBlock = uint2(TexelIndexInBlock % CARD_TRACE_BLOCK_SIZE, TexelIndexInBlock / CARD_TRACE_BLOCK_SIZE);

        uint4 TraceBlockData = CardTraceBlockData[GlobalBlockIndex];
        uint CardId = TraceBlockData.x;
        uint ProbeIndex = TraceBlockData.y;
        uint BlockIndex = TraceBlockData.z;

        FLumenCardData CardData = GetLumenCardData(CardId, CardBuffer);

        float2 CardSizeTexels = abs(CardData.LocalExtent.xy * 2 * CardData.LocalPositionToAtlasUVScale * RadiosityAtlasSize);
        uint2 NumBlocksXY = ((uint2)CardSizeTexels + CARD_TRACE_BLOCK_SIZE - 1) / CARD_TRACE_BLOCK_SIZE;
        uint2 BlockOffset = uint2(BlockIndex % NumBlocksXY.x, BlockIndex / NumBlocksXY.x);
        float2 TexelCoord = BlockOffset * CARD_TRACE_BLOCK_SIZE + TexelOffsetInBlock;

        if (all(TexelCoord < CardSizeTexels))
        {
            uint TraceThreadIndex = ThreadIndex % THREADS_PER_RADIOSITY_TEXEL;

            float2 CardUV = (TexelCoord + .5f) / (float2)CardSizeTexels;
            float2 CardUVToAtlasScale = GetCardUVToAtlasScale(CardData.LocalPositionToAtlasUVScale, CardData.LocalExtent);
            float2 CardUVToAtlasBias = GetCardUVToAtlasBias(CardUVToAtlasScale, CardData.LocalPositionToAtlasUVBias);
            float2 AtlasUV = CardUV * CardUVToAtlasScale + CardUVToAtlasBias;

            uint NumTracesPerThread = NumCones / THREADS_PER_RADIOSITY_TEXEL;
            uint ConeStartIndex = TraceThreadIndex * NumTracesPerThread;
            AtlasCoord = int2(AtlasUV * RadiosityAtlasSize);
            // 로부터 .
            float3 Lighting = RadiosityTraceFromTexel(AtlasUV, AtlasCoord, ProbeIndex, CardData, ConeStartIndex, ConeStartIndex + NumTracesPerThread);
            ThreadLighting[GroupThreadId.x] = Lighting;
        }
    }

    // 스레드 그룹(Thread Group)의 스레드(Thread)계산/산출(Calculate).
    GroupMemoryBarrierWithGroupSync();

    uint TraceThreadIndex = ThreadIndex % THREADS_PER_RADIOSITY_TEXEL;

    // 스레드 그룹(Thread Group)스레드(Thread)의 라이팅 결과 저장(Save). TraceThreadIndex == 0테이블 오직 개 스레드 그룹(Thread Group)의 개 스레드(Thread)실행(Execute).
    if (TraceThreadIndex == 0 && all(AtlasCoord >= 0))
    {
        float3 Lighting = 0;

        for (uint OtherThreadIndex = GroupThreadId.x; OtherThreadIndex < GroupThreadId.x + THREADS_PER_RADIOSITY_TEXEL; OtherThreadIndex += 1)
        {
            Lighting += ThreadLighting[OtherThreadIndex];
        }

        RWRadiosityAtlas[AtlasCoord] = float4(Lighting, 0);
    }
#endif
}

조사량을 추적할 때 조사량 캐시 모드와 비조사량 캐시 모드의 두 가지 모드가 지원되는 것을 볼 수 있습니다. 방사조도 캐시 모드는 3D RadianceProbeIndirectionTexture 샘플링 및 보간 계산에서 방사조를 얻는 반면, 비조도 캐시 모드는 카드 텍셀 근처의 방사조를 실시간으로 추적한 다음 그 결과를 중첩합니다. RadiosityTraceFromTexel의 논리는 다음과 같이 사용됩니다.

hlsl
float3 RadiosityTraceFromTexel(float2 AtlasUV, int2 AtlasCoord, uint ProbeIndex, FLumenCardData LumenCardData, uint ConeStartIndex, uint ConeEndIndex)
{
    float Opacity = Texture2DSampleLevel(CurrentOpacityAtlas, GlobalBilinearClampedSampler, AtlasUV, 0).x;

    float3 Lighting = 0;

    if (Opacity > 0)
    {
        float Depth = 1.0f - Texture2DSampleLevel(DepthAtlas, GlobalBilinearClampedSampler, AtlasUV, 0).x;

        // 로컬 위치
        float3 LocalPosition;
        LocalPosition.xy = (AtlasUV - LumenCardData.LocalPositionToAtlasUVBias) / LumenCardData.LocalPositionToAtlasUVScale;
        LocalPosition.z = -LumenCardData.LocalExtent.z + Depth * 2 * LumenCardData.LocalExtent.z;

        // 의 위치 및 노멀(Normal).
        float3 WorldPosition = mul(LumenCardData.WorldToLocalRotation, LocalPosition) + LumenCardData.Origin;
        float3 WorldNormal = normalize(Texture2DSampleLevel(NormalAtlas, GlobalBilinearClampedSampler, AtlasUV, 0).xyz * 2 - 1);

        //@todo - derive bias from texel world size
        WorldPosition += WorldNormal * SurfaceBias;

        // 포인트 .
        float VoxelTraceStartDistance = CalculateVoxelTraceStartDistance(MinTraceDistance, MaxTraceDistance, MaxMeshSDFTraceDistance, false);

        // 순회(Traverse/Iterate)의 , 의 결과 .
        for (uint ConeIndex = ConeStartIndex; ConeIndex < ConeEndIndex; ConeIndex++)
        {
            //uint ConeIndex = ConeStartIndex;
            float3x3 TangentBasis = GetTangentBasisFrisvad(WorldNormal);

            // 계산/산출(Calculate).
            #define PRECOMPUTED_SAMPLE_DIRECTIONS 1
            #if PRECOMPUTED_SAMPLE_DIRECTIONS // 계산/산출(Calculate)의 .
                float3 LocalConeDirection = RadiosityConeDirections[ConeIndex].xyz;
                float3 WorldConeDirection = mul(LocalConeDirection, TangentBasis);
            #else // 계산/산출(Calculate), 저불일치 준난수 시퀀스.
                uint2 Seed0 = Rand3DPCG16(int3(AtlasCoord + 17, 0)).xy;
                float2 E = Hammersley16(ConeIndex, NumCones, Seed0);
                float2 DiskE = UniformSampleDiskConcentric(E.xy);
                float TangentZ = sqrt(1 - length2(DiskE));
                float3 WorldConeDirection = mul(float3(DiskE, TangentZ), TangentBasis);
            #endif

            //@todo - derive bias from texel world size
            // 샘플링위치 .
            float3 SamplePosition = WorldPosition + SurfaceBias * WorldConeDirection;

            // 입력데이터 .
            FConeTraceInput TraceInput;
            TraceInput.Setup(SamplePosition, WorldConeDirection, DiffuseConeHalfAngle, MinSampleRadius, MinTraceDistance, MaxTraceDistance, StepFactor);
            TraceInput.VoxelStepFactor = VoxelStepFactor;
            TraceInput.VoxelTraceStartDistance = VoxelTraceStartDistance;
            TraceInput.SDFStepFactor = 1;

            // 실행(Execute), 저장(Save)결과 .
            FConeTraceResult TraceResult;
            ConeTraceVoxels(TraceInput, TraceResult);

            // 을(를) 활용하여 계산/산출(Calculate)스카이라이트(Skylight)의 .
            EvaluateSkyRadianceForCone(WorldConeDirection, TraceInput.TanConeAngle, TraceResult);

            // 샘플링의 라이팅 결과 .
            Lighting += TraceResult.Lighting;
        }
    }

    // 스케일 샘플링결과 , .
    Lighting *= PI / (float)NumCones;
    return Lighting;
}

척추체 추적 장면에 포함된 ConeTraceVoxels 인터페이스는 6.5.6.1 Voxel Cone Tracing에서 언급한 방법입니다. 코드는 다음과 같습니다:

hlsl
// Engine\Shaders\Private\Lumen\LumenTracingCommon.ush

void ConeTraceVoxels(
    FConeTraceInput TraceInput,
    inout FConeTraceResult OutResult)
{
    FGlobalSDFTraceResult SDFTraceResult;

    // SDF광선(Ray)
    {
        FGlobalSDFTraceInput SDFTraceInput = SetupGlobalSDFTraceInput(TraceInput.ConeOrigin, TraceInput.ConeDirection, TraceInput.MinTraceDistance, TraceInput.MaxTraceDistance, TraceInput.SDFStepFactor, TraceInput.VoxelStepFactor);
        SDFTraceInput.bExpandSurfaceUsingRayTimeInsteadOfMaxDistance = TraceInput.bExpandSurfaceUsingRayTimeInsteadOfMaxDistance;
        SDFTraceInput.InitialMaxDistance = TraceInput.InitialMaxDistance;

        // 글로벌 거리.
        SDFTraceResult = RayTraceGlobalDistanceField(SDFTraceInput);
    }

    float4 LightingAndAlpha = float4(0, 0, 0, 1);

    // 오직 글로벌 거리히트(Hit)실행(Execute)의 로직 .
    if (GlobalSDFTraceResultIsHit(SDFTraceResult))
    {
        float3 SampleWorldPosition = TraceInput.ConeOrigin + TraceInput.ConeDirection * SDFTraceResult.HitTime;

        uint VoxelClipmapIndex = 0;
        float3 VoxelClipmapCenter = ClipmapWorldCenter[VoxelClipmapIndex].xyz;
        float3 VoxelClipmapExtent = ClipmapWorldSamplingExtent[VoxelClipmapIndex].xyz;

        bool bOutsideValidRegion = any(SampleWorldPosition > VoxelClipmapCenter + VoxelClipmapExtent || SampleWorldPosition < VoxelClipmapCenter - VoxelClipmapExtent);

        // 탐색/조회(Find/Lookup)매칭 현재(Current)의 의 voxel clipmap.
        while (bOutsideValidRegion && VoxelClipmapIndex + 1 < NumClipmapLevels)
        {
            VoxelClipmapIndex++;
            VoxelClipmapCenter = ClipmapWorldCenter[VoxelClipmapIndex].xyz;
            VoxelClipmapExtent = ClipmapWorldSamplingExtent[VoxelClipmapIndex].xyz;
            bOutsideValidRegion = any(SampleWorldPosition > VoxelClipmapCenter + VoxelClipmapExtent || SampleWorldPosition < VoxelClipmapCenter - VoxelClipmapExtent);
        }

        LightingAndAlpha.xyzw = 0.0f;

        // 만약 유효(Valid)범위 , 이면 계산/산출(Calculate)Voxel라이팅 .
        if (!bOutsideValidRegion)
        {
            float3 DistanceFieldGradient = -TraceInput.ConeDirection;

            float3 ClipmapVolumeUV = ComputeGlobalUV(SampleWorldPosition, SDFTraceResult.HitClipmapIndex);
            uint PageIndex = GetGlobalDistanceFieldPage(ClipmapVolumeUV, SDFTraceResult.HitClipmapIndex);

            if (PageIndex < GLOBAL_DISTANCE_FIELD_INVALID_PAGE_ID)
            {
                float3 PageUV = ComputeGlobalDistanceFieldPageUV(ClipmapVolumeUV, PageIndex);
                DistanceFieldGradient = GlobalDistanceFieldPageCentralDiff(PageUV);
            }

            float DistanceFieldGradientLength = length(DistanceFieldGradient);
            float3 SampleNormal = DistanceFieldGradientLength > 0.001 ? DistanceFieldGradient / DistanceFieldGradientLength : -TraceInput.ConeDirection;

            // 샘플링3D텍스처(Texture)VoxelLighting, 라이팅 .
            float4 StepLighting = SampleVoxelLighting(SampleWorldPosition, -SampleNormal, VoxelClipmapIndex);

            StepLighting.xyz = StepLighting.xyz * (1.0f / max(StepLighting.w, 0.1));

            // 계산/산출(Calculate)차폐/오클루전 .
            float VoxelSelfLightingBias = 1.0f;
            if (TraceInput.bExpandSurfaceUsingRayTimeInsteadOfMaxDistance)
            {
                // ,는 차폐/오클루전 , .
                VoxelSelfLightingBias = smoothstep(1.5 * ClipmapVoxelSizeAndRadius[VoxelClipmapIndex].w, 2.0 * ClipmapVoxelSizeAndRadius[VoxelClipmapIndex].w, SDFTraceResult.HitTime);
            }

            // 차폐/오클루전 의 라이팅 결과 .
            LightingAndAlpha.xyz = StepLighting.xyz * VoxelSelfLightingBias;
        }
    }

    // 에 따라 Opacity라이팅 결과 .
    LightingAndAlpha = FadeOutVoxelConeTraceMinTransparency(LightingAndAlpha);

    // 저장(Save)결과 .
    OutResult = (FConeTraceResult)0;
    #if !VISIBILITY_ONLY_TRACE
        OutResult.Lighting = LightingAndAlpha.rgb;
    #endif
    OutResult.Transparency = LightingAndAlpha.a;
    OutResult.NumSteps = SDFTraceResult.TotalStepsTaken;
    OutResult.OpaqueHitDistance = GlobalSDFTraceResultIsHit(SDFTraceResult) ? SDFTraceResult.HitTime : TraceInput.MaxTraceDistance;
}

VoxelLighting의 3D 텍스처는 위의 원뿔 추적에 사용됩니다. 이 텍스처는 클립맵이기도 합니다. 작성자가 가로채는 데이터에 따르면 크기는 64x256x384이고 많은 조각이 검은색이며 소수에만 픽셀이 있고 영역이 매우 작습니다.

6.5.6.4 CombineLumenSceneLighting

CombineLumenSceneLighting은 조명의 조합이며 구체적인 논리는 다음과 같습니다.

cpp
void CombineLumenSceneLighting(
    FScene* Scene, 
    FViewInfo& View,
    FRDGBuilder& GraphBuilder,
    TRDGUniformBufferRef<FLumenCardScene> LumenCardSceneUniformBuffer,
    FRDGTextureRef FinalLightingAtlas, 
    FRDGTextureRef OpacityAtlas, 
    FRDGTextureRef RadiosityAtlas, 
    FGlobalShaderMap* GlobalShaderMap,
    const FLumenCardScatterContext& VisibleCardScatterContext)
{
    LLM_SCOPE_BYTAG(Lumen);

    FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;

    {
        FLumenCardLightingEmissive* PassParameters = GraphBuilder.AllocParameters<FLumenCardLightingEmissive>();
        
        extern int32 GLumenRadiosityDownsampleFactor;
        FVector2D CardUVSamplingOffset = FVector2D::ZeroVector;
        if (GLumenRadiosityDownsampleFactor > 1)
        {
            // Offset bilinear samples in order to not sample outside of the lower res radiosity card bounds
            CardUVSamplingOffset.X = (GLumenRadiosityDownsampleFactor * 0.25f) / LumenSceneData.MaxAtlasSize.X;
            CardUVSamplingOffset.Y = (GLumenRadiosityDownsampleFactor * 0.25f) / LumenSceneData.MaxAtlasSize.Y;
        }

        PassParameters->RenderTargets[0] = FRenderTargetBinding(FinalLightingAtlas, ERenderTargetLoadAction::ENoAction);
        PassParameters->VS.LumenCardScene = LumenCardSceneUniformBuffer;
        PassParameters->VS.CardScatterParameters = VisibleCardScatterContext.Parameters;
        PassParameters->VS.ScatterInstanceIndex = 0;
        PassParameters->VS.CardUVSamplingOffset = CardUVSamplingOffset;
        PassParameters->PS.View = View.ViewUniformBuffer;
        PassParameters->PS.LumenCardScene = LumenCardSceneUniformBuffer;
        PassParameters->PS.RadiosityAtlas = RadiosityAtlas;
        PassParameters->PS.OpacityAtlas = OpacityAtlas;

        // 추가(Add)라이팅 Pass, 을(를) 활용하여 의 는 의 래스터라이제이션 프로세스 .
        GraphBuilder.AddPass(
            RDG_EVENT_NAME("LightingCombine"),
            PassParameters,
            ERDGPassFlags::Raster,
            [MaxAtlasSize = Scene->LumenSceneData->MaxAtlasSize, PassParameters, GlobalShaderMap](FRHICommandListImmediate& RHICmdList)
        {
            FLumenCardLightingInitializePS::FPermutationDomain PermutationVector;
            auto PixelShader = GlobalShaderMap->GetShader< FLumenCardLightingInitializePS >(PermutationVector);

            DrawQuadsToAtlas(MaxAtlasSize, PixelShader, PassParameters, GlobalShaderMap, TStaticBlendState<>::GetRHI(), RHICmdList);
        });
    }
}

이 단계에서는 이전 섹션의 장면 방사성 아틀라스를 입력으로 가져온 다음 방사성 색상을 SceneFinalLighting으로 출력합니다.

6.5.6.5 RenderDirectLightingForLumenScene

RenderDirectLightingForLumenScene은 루멘 장면의 직접 조명을 계산합니다. 이 과정은 기존 조명과 다소 유사합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneDirectLighting.cpp

void FDeferredShadingSceneRenderer::RenderDirectLightingForLumenScene(
    FRDGBuilder& GraphBuilder,
    TRDGUniformBufferRef<FLumenCardScene> LumenCardSceneUniformBuffer,
    FRDGTextureRef FinalLightingAtlas,
    FRDGTextureRef OpacityAtlas,
    FGlobalShaderMap* GlobalShaderMap,
    const FLumenCardScatterContext& VisibleCardScatterContext)
{
    LLM_SCOPE_BYTAG(Lumen);

    if (GLumenDirectLighting)
    {
        RDG_EVENT_SCOPE(GraphBuilder, "DirectLighting");
        QUICK_SCOPE_CYCLE_COUNTER(RenderDirectLightingForLumenScene);

        const FViewInfo& MainView = Views[0];
        FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;
        const bool bLumenUseHardwareRayTracedShadow = Lumen::UseHardwareRayTracedShadows(MainView);
        FLumenDirectLightingHardwareRayTracingData LumenDirectLightingHardwareRayTracingData;
        
        if(bLumenUseHardwareRayTracedShadow)
        {
            LumenDirectLightingHardwareRayTracingData.Initialize(GraphBuilder, Scene);
        }

        TArray<const FLightSceneInfo*, TInlineAllocator<64>> GatheredLocalLights;

        // 순회(Traverse/Iterate)의 라이트(Light).
        for (TSparseArray<FLightSceneInfoCompact>::TConstIterator LightIt(Scene->Lights); LightIt; ++LightIt)
        {
            const FLightSceneInfoCompact& LightSceneInfoCompact = *LightIt;
            const FLightSceneInfo* LightSceneInfo = LightSceneInfoCompact.LightSceneInfo;

            if (LightSceneInfo->ShouldRenderLightViewIndependent()
                && LightSceneInfo->ShouldRenderLight(MainView, true)
                && LightSceneInfo->Proxy->GetIndirectLightingScale() > 0.0f)
            {
                const ELightComponentType LightType = (ELightComponentType)LightSceneInfo->Proxy->GetLightType();

                //
                if (LightType == LightType_Directional)
                {
                    // 클리핑/클램핑(Clipping), 드로우/렌더(Draw).

                    FString LightNameWithLevel;
                    FSceneRenderer::GetLightNameForDrawEvent(LightSceneInfo->Proxy, LightNameWithLevel);

                    // 렌더링(Render)까지 Lumen.
                    RenderDirectLightIntoLumenCards(
                        GraphBuilder,
                        Scene,
                        MainView,
                        ViewFamily.EngineShowFlags,
                        VisibleLightInfos,
                        LumenCardSceneUniformBuffer,
                        FinalLightingAtlas,
                        OpacityAtlas,
                        LightSceneInfo,
                        LightNameWithLevel,
                        VisibleCardScatterContext,
                        0,
                        LumenDirectLightingHardwareRayTracingData,
                        VirtualShadowMapArray);
                }
                else // , 까지 GatheredLocalLights.
                {
                    GatheredLocalLights.Add(LightSceneInfo);
                }
            }
        }

        const int32 LightBatchSize = FMath::Clamp(GLumenDirectLightingBatchSize, 1, 256);

        // 의 라이팅 클리핑/클램핑(Clipping) 및
        for (int32 LightBatchIndex = 0; LightBatchIndex * LightBatchSize < GatheredLocalLights.Num(); ++LightBatchIndex)
        {
            const int32 FirstLightIndex = LightBatchIndex * LightBatchSize;
            const int32 LastLightIndex = FMath::Min((LightBatchIndex + 1) * LightBatchSize, GatheredLocalLights.Num());

            FLumenCardScatterContext CardScatterContext;

            {
                RDG_EVENT_SCOPE(GraphBuilder, "Cull Cards %d Lights", LastLightIndex - FirstLightIndex);

                // 초기화(Initialize).
                CardScatterContext.Init(
                    GraphBuilder,
                    MainView,
                    LumenSceneData,
                    LumenCardRenderer,
                    ECullCardsMode::OperateOnSceneForceUpdateForCardsToRender,
                    LightBatchSize);

                // 클리핑/클램핑(Clipping)까지 라이트(Light)의 .
                for (int32 LightIndex = FirstLightIndex; LightIndex < LastLightIndex; ++LightIndex)
                {
                    const int32 ScatterInstanceIndex = LightIndex - FirstLightIndex;
                    const FLightSceneInfo* LightSceneInfo = GatheredLocalLights[LightIndex];
                    const ELightComponentType LightType = (ELightComponentType)LightSceneInfo->Proxy->GetLightType();
                    const FSphere LightBounds = LightSceneInfo->Proxy->GetBoundingSphere();

                    ECullCardsShapeType ShapeType = ECullCardsShapeType::None;

                    if (LightType == LightType_Point)
                    {
                        ShapeType = ECullCardsShapeType::PointLight;
                    }
                    else if (LightType == LightType_Spot)
                    {
                        ShapeType = ECullCardsShapeType::SpotLight;
                    }
                    else if (LightType == LightType_Rect)
                    {
                        ShapeType = ECullCardsShapeType::RectLight;
                    }
                    else
                    {
                        ensureMsgf(false, TEXT("Need Lumen card culling for new light type"));
                    }

                    FCullCardsShapeParameters ShapeParameters;
                    ShapeParameters.InfluenceSphere = FVector4(LightBounds.Center, LightBounds.W);
                    ShapeParameters.LightPosition = LightSceneInfo->Proxy->GetPosition();
                    ShapeParameters.LightDirection = LightSceneInfo->Proxy->GetDirection();
                    ShapeParameters.LightRadius = LightSceneInfo->Proxy->GetRadius();
                    ShapeParameters.CosConeAngle = FMath::Cos(LightSceneInfo->Proxy->GetOuterConeAngle());
                    ShapeParameters.SinConeAngle = FMath::Sin(LightSceneInfo->Proxy->GetOuterConeAngle());

                    // 에 따라 라이트(Light)클리핑/클램핑(Clipping)
                    CardScatterContext.CullCardsToShape(
                        GraphBuilder,
                        MainView,
                        LumenSceneData,
                        LumenCardRenderer,
                        LumenCardSceneUniformBuffer,
                        ShapeType,
                        ShapeParameters,
                        GLumenSceneCardDirectLightingUpdateFrequencyScale,
                        ScatterInstanceIndex);
                }

                // 산란 파라미터 .
                CardScatterContext.BuildScatterIndirectArgs(
                    GraphBuilder,
                    MainView);
            }

            // 드로우/렌더(Draw)의 라이트(Light).
            {
                RDG_EVENT_SCOPE(GraphBuilder, "Draw %d Lights", LastLightIndex - FirstLightIndex);

                for (int32 LightIndex = FirstLightIndex; LightIndex < LastLightIndex; ++LightIndex)
                {
                    const int32 ScatterInstanceIndex = LightIndex - FirstLightIndex;
                    const FLightSceneInfo* LightSceneInfo = GatheredLocalLights[LightIndex];

                    FString LightNameWithLevel;
                    FSceneRenderer::GetLightNameForDrawEvent(LightSceneInfo->Proxy, LightNameWithLevel);

                    // 드로우/렌더(Draw)의 라이트(Light)까지 Lumen.
                    RenderDirectLightIntoLumenCards(
                        GraphBuilder,
                        Scene,
                        MainView,
                        ViewFamily.EngineShowFlags,
                        VisibleLightInfos,
                        LumenCardSceneUniformBuffer,
                        FinalLightingAtlas,
                        OpacityAtlas,
                        LightSceneInfo,
                        LightNameWithLevel,
                        CardScatterContext,
                        ScatterInstanceIndex,
                        LumenDirectLightingHardwareRayTracingData,
                        VirtualShadowMapArray);
                }
            }
        }
    }
}

다음은 단일 광원 RenderDirectLightIntoLumenCards를 그리는 코드입니다.

cpp
void RenderDirectLightIntoLumenCards(
    FRDGBuilder& GraphBuilder,
    const FScene* Scene,
    const FViewInfo& View,
    const FEngineShowFlags& EngineShowFlags,
    TArray<FVisibleLightInfo, SceneRenderingAllocator>& VisibleLightInfos,
    TRDGUniformBufferRef<FLumenCardScene> LumenCardSceneUniformBuffer,
    FRDGTextureRef FinalLightingAtlas,
    FRDGTextureRef OpacityAtlas,
    const FLightSceneInfo* LightSceneInfo,
    const FString& LightName,
    const FLumenCardScatterContext& CardScatterContext,
    int32 ScatterInstanceIndex,
    FLumenDirectLightingHardwareRayTracingData& LumenDirectLightingHardwareRayTracingData,
    const FVirtualShadowMapArray& VirtualShadowMapArray)
{
    FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;
    const FSphere LightBounds = LightSceneInfo->Proxy->GetBoundingSphere();
    const ELightComponentType LightType = (ELightComponentType)LightSceneInfo->Proxy->GetLightType();
    bool bShadowed = LightSceneInfo->Proxy->CastsDynamicShadow();

    // 변환(Transform/Convert)라이트(Light)타입/유형 .
    ELumenLightType LumenLightType = ELumenLightType::MAX;
    {
        switch (LightType)
        {
        case LightType_Directional: LumenLightType = ELumenLightType::Directional;    break;
        case LightType_Point:        LumenLightType = ELumenLightType::Point;        break;
        case LightType_Spot:        LumenLightType = ELumenLightType::Spot;            break;
        case LightType_Rect:        LumenLightType = ELumenLightType::Rect;            break;
        }
        check(LumenLightType != ELumenLightType::MAX);
    }

    // 설정(Set)그림자(Shadow)정보 .
    FVisibleLightInfo& VisibleLightInfo = VisibleLightInfos[LightSceneInfo->Id];
    FLumenShadowSetup ShadowSetup = GetShadowForLumenDirectLighting(VisibleLightInfo);

    const bool bDynamicallyShadowed = ShadowSetup.DenseShadowMap != nullptr;

    FDistanceFieldObjectBufferParameters ObjectBufferParameters = DistanceField::SetupObjectBufferParameters(Scene->DistanceFieldSceneData);

    FLightTileIntersectionParameters LightTileIntersectionParameters;
    FDistanceFieldCulledObjectBufferParameters CulledObjectBufferParameters;
    FMatrix WorldToMeshSDFShadowValue = FMatrix::Identity;

    const bool bLumenUseHardwareRayTracedShadow = Lumen::UseHardwareRayTracedShadows(View) && bShadowed;
    const bool bTraceMeshSDFs = bShadowed 
        && LumenLightType == ELumenLightType::Directional 
        && DoesPlatformSupportDistanceFieldShadowing(View.GetShaderPlatform())
        && GLumenDirectLightingOffscreenShadowingTraceMeshSDFs != 0
        && Lumen::UseMeshSDFTracing()
        && ObjectBufferParameters.NumSceneObjects > 0;

    // 처리(Process)그림자(Shadow)ID.
    int32 VirtualShadowMapId = -1;
    if (bDynamicallyShadowed
        && !bLumenUseHardwareRayTracedShadow
        && GLumenDirectLightingVirtualShadowMap != 0
        && VirtualShadowMapArray.IsAllocated())
    {
        if (LightType == LightType_Directional)
        {
            VirtualShadowMapId = VisibleLightInfo.VirtualShadowMapClipmaps[0]->GetVirtualShadowMap()->ID;
        }
        else if (ShadowSetup.VirtualShadowMap)
        {
            VirtualShadowMapId = ShadowSetup.VirtualShadowMap->VirtualShadowMaps[0]->ID;
        }
    }

    const bool bUseVirtualShadowMap = VirtualShadowMapId >= 0;
    if (!bUseVirtualShadowMap)
    {
        // Fallback to a complete shadow map
        ShadowSetup.VirtualShadowMap = nullptr;
        ShadowSetup.DenseShadowMap = GetShadowForInjectionIntoVolumetricFog(VisibleLightInfo);
    }

    if (bLumenUseHardwareRayTracedShadow)
    {
        RenderHardwareRayTracedShadowIntoLumenCards(
            GraphBuilder, Scene, View, LumenCardSceneUniformBuffer, OpacityAtlas, 
            LightSceneInfo, LightName, CardScatterContext, ScatterInstanceIndex,
            LumenDirectLightingHardwareRayTracingData, bDynamicallyShadowed, LumenLightType);
    }
    else if (bTraceMeshSDFs)
    {
        CullMeshSDFsForLightCards(GraphBuilder, Scene, View, LightSceneInfo, ObjectBufferParameters, WorldToMeshSDFShadowValue, CulledObjectBufferParameters, LightTileIntersectionParameters);
    }

    FLumenCardDirectLighting* PassParameters = GraphBuilder.AllocParameters<FLumenCardDirectLighting>();
    {
        PassParameters->RenderTargets[0] = FRenderTargetBinding(FinalLightingAtlas, ERenderTargetLoadAction::ELoad);
        PassParameters->VS.InfluenceSphere = FVector4(LightBounds.Center, LightBounds.W);
        PassParameters->VS.LumenCardScene = LumenCardSceneUniformBuffer;
        PassParameters->VS.CardScatterParameters = CardScatterContext.Parameters;
        PassParameters->VS.ScatterInstanceIndex = ScatterInstanceIndex;
        PassParameters->VS.CardUVSamplingOffset = FVector2D::ZeroVector;

        // 페치/가져오기(Fetch)그림자(Shadow)shader파라미터 .
        GetVolumeShadowingShaderParameters(
            GraphBuilder,
            View,
            LightSceneInfo,
            ShadowSetup.DenseShadowMap,
            0,
            bDynamicallyShadowed,
            PassParameters->PS.VolumeShadowingShaderParameters);

        // 라이트(Light)글로벌 버퍼 .
        FDeferredLightUniformStruct DeferredLightUniforms = GetDeferredLightParameters(View, *LightSceneInfo);

        if (LightSceneInfo->Proxy->IsInverseSquared())
        {
            DeferredLightUniforms.LightParameters.FalloffExponent = 0;
        }

        PassParameters->PS.View = View.ViewUniformBuffer;
        PassParameters->PS.LumenCardScene = LumenCardSceneUniformBuffer;
        PassParameters->PS.OpacityAtlas = OpacityAtlas;
        DeferredLightUniforms.LightParameters.Color *= LightSceneInfo->Proxy->GetIndirectLightingScale();
        PassParameters->PS.DeferredLightUniforms = CreateUniformBufferImmediate(DeferredLightUniforms, UniformBuffer_SingleDraw);
        PassParameters->PS.ForwardLightData = View.ForwardLightingResources->ForwardLightDataUniformBuffer;
        SetupLightFunctionParameters(LightSceneInfo, 1.0f, PassParameters->PS.LightFunctionParameters);

        PassParameters->PS.VirtualShadowMapId = VirtualShadowMapId;
        if (bUseVirtualShadowMap)
        {
            PassParameters->PS.VirtualShadowMapSamplingParameters = VirtualShadowMapArray.GetSamplingParameters(GraphBuilder);
        }
        
        PassParameters->PS.ObjectBufferParameters = ObjectBufferParameters;
        PassParameters->PS.CulledObjectBufferParameters = CulledObjectBufferParameters;
        PassParameters->PS.LightTileIntersectionParameters = LightTileIntersectionParameters;

        FDistanceFieldAtlasParameters DistanceFieldAtlasParameters = DistanceField::SetupAtlasParameters(Scene->DistanceFieldSceneData);

        // 거리
        PassParameters->PS.DistanceFieldAtlasParameters = DistanceFieldAtlasParameters;
        PassParameters->PS.WorldToShadow = WorldToMeshSDFShadowValue;
        extern float GTwoSidedMeshDistanceBias;
        PassParameters->PS.TwoSidedMeshDistanceBias = GTwoSidedMeshDistanceBias;

        PassParameters->PS.TanLightSourceAngle = FMath::Tan(LightSceneInfo->Proxy->GetLightSourceAngle());
        PassParameters->PS.MaxTraceDistance = GOffscreenShadowingMaxTraceDistance;
        PassParameters->PS.StepFactor = FMath::Clamp(GOffscreenShadowingTraceStepFactor, .1f, 10.0f);
        PassParameters->PS.SurfaceBias = FMath::Clamp(GShadowingSurfaceBias, .01f, 100.0f);
        PassParameters->PS.SlopeScaledSurfaceBias = FMath::Clamp(GShadowingSlopeScaledSurfaceBias, .01f, 100.0f);
        PassParameters->PS.SDFSurfaceBiasScale = FMath::Clamp(GOffscreenShadowingSDFSurfaceBiasScale, .01f, 100.0f);
        PassParameters->PS.VirtualShadowMapSurfaceBias = FMath::Clamp(GLumenDirectLightingVirtualShadowMapBias, .01f, 100.0f);
        PassParameters->PS.ForceOffscreenShadowing = GLumenDirectLightingForceOffscreenShadowing;

        if (bLumenUseHardwareRayTracedShadow)
        {
            PassParameters->PS.ShadowMaskAtlas = LumenDirectLightingHardwareRayTracingData.ShadowMaskAtlas;
        }

        // IES
        {
            FTexture* IESTextureResource = LightSceneInfo->Proxy->GetIESTextureResource();

            if (View.Family->EngineShowFlags.TexturedLightProfiles && IESTextureResource)
            {
                PassParameters->PS.UseIESProfile = 1;
                PassParameters->PS.IESTexture = IESTextureResource->TextureRHI;
            }
            else
            {
                PassParameters->PS.UseIESProfile = 0;
                PassParameters->PS.IESTexture = GWhiteTexture->TextureRHI;
            }

            PassParameters->PS.IESTextureSampler = TStaticSamplerState<SF_Bilinear,AM_Clamp,AM_Clamp,AM_Clamp>::GetRHI();
        }
    }

    FRasterizeToCardsVS::FPermutationDomain VSPermutationVector;
    VSPermutationVector.Set< FRasterizeToCardsVS::FClampToInfluenceSphere >(LightType != LightType_Directional);
    auto VertexShader = View.ShaderMap->GetShader<FRasterizeToCardsVS>(VSPermutationVector);
    const FMaterialRenderProxy* LightFunctionMaterialProxy = LightSceneInfo->Proxy->GetLightFunctionMaterial();
    bool bUseLightFunction = true;

    if (!LightFunctionMaterialProxy
        || !LightFunctionMaterialProxy->GetIncompleteMaterialWithFallback(Scene->GetFeatureLevel()).IsLightFunction()
        || !EngineShowFlags.LightFunctions)
    {
        bUseLightFunction = false;
        LightFunctionMaterialProxy = UMaterial::GetDefaultMaterial(MD_LightFunction)->GetRenderProxy();
    }

    const bool bUseCloudTransmittance = SetupLightCloudTransmittanceParameters(Scene, View, GLumenDirectLightingCloudTransmittance != 0 ? LightSceneInfo : nullptr, PassParameters->PS.LightCloudTransmittanceParameters);

    // 설정(Set).
    FLumenCardDirectLightingPS::FPermutationDomain PermutationVector;
    PermutationVector.Set< FLumenCardDirectLightingPS::FLightType >(LumenLightType);
    PermutationVector.Set< FLumenCardDirectLightingPS::FDynamicallyShadowed >(bDynamicallyShadowed);
    PermutationVector.Set< FLumenCardDirectLightingPS::FShadowed >(bShadowed);
    PermutationVector.Set< FLumenCardDirectLightingPS::FTraceMeshSDFs >(bTraceMeshSDFs);
    PermutationVector.Set< FLumenCardDirectLightingPS::FVirtualShadowMap >(bUseVirtualShadowMap);
    PermutationVector.Set< FLumenCardDirectLightingPS::FLightFunction >(bUseLightFunction);
    PermutationVector.Set< FLumenCardDirectLightingPS::FRayTracingShadowPassCombine>(bLumenUseHardwareRayTracedShadow);
    PermutationVector.Set< FLumenCardDirectLightingPS::FCloudTransmittance >(bUseCloudTransmittance);
    
    PermutationVector = FLumenCardDirectLightingPS::RemapPermutation(PermutationVector);

    const FMaterial& Material = LightFunctionMaterialProxy->GetMaterialWithFallback(Scene->GetFeatureLevel(), LightFunctionMaterialProxy);
    const FMaterialShaderMap* MaterialShaderMap = Material.GetRenderingThreadShaderMap();
    auto PixelShader = MaterialShaderMap->GetShader<FLumenCardDirectLightingPS>(PermutationVector);

    ClearUnusedGraphResources(PixelShader, &PassParameters->PS);

    const uint32 CardIndirectArgOffset = CardScatterContext.GetIndirectArgOffset(ScatterInstanceIndex);

    // 라이팅 드로우/렌더(Draw)Pass.
    GraphBuilder.AddPass(
        RDG_EVENT_NAME("%s %s", *LightName, bDynamicallyShadowed ? TEXT("Shadowmap") : TEXT("")),
        PassParameters,
        ERDGPassFlags::Raster,
        [MaxAtlasSize = LumenSceneData.MaxAtlasSize, PassParameters, LightSceneInfo, VertexShader, PixelShader, GlobalShaderMap = View.ShaderMap, LightFunctionMaterialProxy, &Material, &View, CardIndirectArgOffset](FRHICommandListImmediate& RHICmdList)
        {
            DrawQuadsToAtlas(
                MaxAtlasSize,
                VertexShader,
                PixelShader,
                PassParameters,
                GlobalShaderMap,
                TStaticBlendState<CW_RGBA, BO_Add, BF_One, BF_One>::GetRHI(),
                RHICmdList,
                [LightFunctionMaterialProxy, &Material, &View](FRHICommandListImmediate& RHICmdList, TShaderRefBase<FLumenCardDirectLightingPS, FShaderMapPointerTable> Shader, FRHIPixelShader* ShaderRHI, const FLumenCardDirectLightingPS::FParameters& Parameters)
                {
                    Shader->SetParameters(RHICmdList, ShaderRHI, LightFunctionMaterialProxy, Material, View);
                },
                CardIndirectArgOffset);
        });
}

직사광선을 차단한 후의 과정은 다음과 같습니다.

조명 계산 과정에서 입력되는 텍스처 데이터는 광원 유형에 따라 다르지만 모든 광원 유형은 깊이, 노멀, 불투명도 및 기타 데이터를 입력합니다. 차이점은 로컬 광원(비평행광)이 거리 필드 관련 텍스처와 16x16x16 Perlin 노이즈 3D 텍스처를 입력하는 반면, 평행광은 128x128x128 3D 재질 VolumeTexture를 입력한다는 것입니다(아래 그림은 4배 확대 후 슬라이스 0의 효과입니다).

조명 계산 후 출력은 다음과 같습니다.

직접 조명 계산에 사용되는 PS는 다음과 같습니다.

hlsl
// Engine\Shaders\Private\Lumen\LumenSceneDirectLighting.usf

void LumenCardDirectLightingPS(
    FCardVSToPS CardInterpolants,
    out float4 OutColor : SV_Target0)
{
    float Opacity = Texture2DSampleLevel(OpacityAtlas, GlobalBilinearClampedSampler, CardInterpolants.AtlasCoord, 0).x;
    float3 Irradiance = 0;

    if (Opacity > 0)
    {
        // 라이트(Light)데이터 .
        FDeferredLightData LightData;
        {
            LightData.Position = DeferredLightUniforms.Position;
            LightData.InvRadius = DeferredLightUniforms.InvRadius;
            LightData.Color = DeferredLightUniforms.Color;
            LightData.FalloffExponent = DeferredLightUniforms.FalloffExponent;
            LightData.Direction = DeferredLightUniforms.Direction;  
            LightData.Tangent = DeferredLightUniforms.Tangent;
            LightData.SpotAngles = DeferredLightUniforms.SpotAngles;
            LightData.SourceRadius = DeferredLightUniforms.SourceRadius;
            LightData.SourceLength = DeferredLightUniforms.SourceLength;
            LightData.SoftSourceRadius = DeferredLightUniforms.SoftSourceRadius;
            LightData.SpecularScale = DeferredLightUniforms.SpecularScale;
            LightData.ContactShadowLength = abs(DeferredLightUniforms.ContactShadowLength);
            LightData.ContactShadowLengthInWS = DeferredLightUniforms.ContactShadowLength < 0.0f;
            LightData.DistanceFadeMAD = DeferredLightUniforms.DistanceFadeMAD;
            LightData.ShadowMapChannelMask = DeferredLightUniforms.ShadowMapChannelMask;
            LightData.ShadowedBits = DeferredLightUniforms.ShadowedBits;
            LightData.RectLightBarnCosAngle = DeferredLightUniforms.RectLightBarnCosAngle;
            LightData.RectLightBarnLength = DeferredLightUniforms.RectLightBarnLength;

            LightData.bInverseSquared = LightData.FalloffExponent == 0.0f;
            LightData.bRadialLight = LIGHT_TYPE != LIGHT_TYPE_DIRECTIONAL;
            LightData.bSpotLight = LIGHT_TYPE == LIGHT_TYPE_SPOT;
            LightData.bRectLight = LIGHT_TYPE == LIGHT_TYPE_RECT;
        }

        // 페치/가져오기(Fetch)Lumen데이터 .
        FLumenCardData LumenCardData = GetLumenCardData(CardInterpolants.CardId);

        float Depth = 1.0f - Texture2DSampleLevel(LumenCardScene.DepthAtlas, GlobalBilinearClampedSampler, CardInterpolants.AtlasCoord, 0).x;

        // 계산/산출(Calculate)위치 .
        float3 LocalPosition;
        LocalPosition.xy = (CardInterpolants.AtlasCoord - LumenCardData.LocalPositionToAtlasUVBias) / LumenCardData.LocalPositionToAtlasUVScale;
        LocalPosition.z = -LumenCardData.LocalExtent.z + Depth * 2 * LumenCardData.LocalExtent.z;

        float3 WorldPosition = mul(LumenCardData.WorldToLocalRotation, LocalPosition) + LumenCardData.Origin;

        float3 LightColor = DeferredLightUniforms.Color;
        float3 L = LightData.Direction;
        float3 ToLight = L;
    
        // 계산/산출(Calculate)라이트(Light)감쇠(Falloff/Attenuation).
#if LIGHT_TYPE == LIGHT_TYPE_DIRECTIONAL
        float CombinedAttenuation = 1;
#else
        float LightMask = 1;
        if (LightData.bRadialLight)
        {
            LightMask = GetLocalLightAttenuation(WorldPosition, LightData, ToLight, L);
        }

        float Attenuation;

        if (LightData.bRectLight)
        {
            FRect Rect = GetRect(ToLight, LightData);
            FRectTexture RectTexture = InitRectTexture(DeferredLightUniforms.SourceTexture);
            Attenuation = IntegrateLight(Rect, RectTexture);
        }
        else
        {
            FCapsuleLight Capsule = GetCapsule(ToLight, LightData);
            Capsule.DistBiasSqr = 0;
            Attenuation = IntegrateLight(Capsule, LightData.bInverseSquared);
        }

        float CombinedAttenuation = Attenuation * LightMask;
#endif

        if (CombinedAttenuation > 0)
        {
            float3 WorldNormal = Texture2DSampleLevel(LumenCardScene.NormalAtlas, GlobalBilinearClampedSampler, CardInterpolants.AtlasCoord, 0).xyz * 2 - 1;

            // 라이트(Light)의 테이블 계산/산출(Calculate)라이트(Light).
            if (dot(WorldNormal, L) > 0)
            {
                float ShadowFactor = 1.0f;

                #if SHADOWED_LIGHT  // 그림자(Shadow)
                {
                    // 그림자(Shadow)
                    #if HARDWARE_RAYTRACING_SHADOW_PASS_COMBINE
                    {
                        float2 AtlasTextureSize = LumenCardScene.AtlasSize;
                        uint2 Pos2D = CardInterpolants.AtlasCoord * AtlasTextureSize.xy - float2(0.5, 0.5) / AtlasTextureSize.xy;
                        ShadowFactor = ShadowMaskAtlas.Load(uint3(Pos2D, 0));
                    }
                    #else // 그림자(Shadow)
                    {
                        bool bShadowFactorComplete = false;
                        bool bVSMValid = false;

                        // 을(를) 활용하여 그림자(Shadow)
                        #if VIRTUAL_SHADOW_MAP
                        {
                            // Bias only ray start to maximize chances of hitting an allocated page
                            FVirtualShadowMapSampleResult VirtualShadowMapSample = SampleVirtualShadowMap(VirtualShadowMapId, WorldPosition, VirtualShadowMapSurfaceBias, WorldNormal);

                            bVSMValid = VirtualShadowMapSample.bValid;
                            bShadowFactorComplete = VirtualShadowMapSample.bValid && VirtualShadowMapSample.bOccluded;
                            ShadowFactor = VirtualShadowMapSample.ShadowFactor;
                        }
                        #endif

                        // 계산/산출(Calculate)그림자(Shadow)ShadowFactor.
                        if (!bShadowFactorComplete)
                        {
                            float3 WorldPositionForShadowing = GetWorldPositionForShadowing(WorldPosition, L, WorldNormal, 1.0f);

                            #if LIGHT_TYPE == LIGHT_TYPE_DIRECTIONAL
                            {
                                #if DYNAMICALLY_SHADOWED
                                    float SceneDepth = dot(WorldPositionForShadowing - View.WorldCameraOrigin, View.ViewForward);

                                    bool bShadowingFromValidUVArea = false;
                                    float NewShadowFactor = ComputeDirectionalLightDynamicShadowing(WorldPositionForShadowing, SceneDepth, bShadowingFromValidUVArea);

                                    float4 PostProjectionPosition = mul(float4(WorldPosition, 1.0), View.WorldToClip);
                                    // CSM's are culled so only query points inside the view are valid
                                    float2 ValidTexelSize = float2(length(ddx(WorldPosition)), length(ddy(WorldPosition))) * 2;
                                    if (bShadowingFromValidUVArea && all(PostProjectionPosition.xy - ValidTexelSize < PostProjectionPosition.w&& PostProjectionPosition.xy + ValidTexelSize > -PostProjectionPosition.w))
                                    { 
                                        ShadowFactor *= NewShadowFactor;
                                        bShadowFactorComplete = VIRTUAL_SHADOW_MAP ? bVSMValid : true;
                                    }
                                #endif
                            }
                            #else
                            {
                                bool bShadowingFromValidUVArea = false;
                                float NewShadowFactor = ComputeVolumeShadowing(WorldPositionForShadowing, LightData.bRadialLight && !LightData.bSpotLight, LightData.bSpotLight, bShadowingFromValidUVArea);

                                if (bShadowingFromValidUVArea) 
                                {
                                    ShadowFactor *= NewShadowFactor;
                                    bShadowFactorComplete = VIRTUAL_SHADOW_MAP ? bVSMValid : true;
                                }
                            }
                            #endif
                        }

                        // 처리(Process)그림자(Shadow).
                        bool bOffscreenShadowing = !bShadowFactorComplete;
                        if (ForceOffscreenShadowing != 0)
                        {
                            ShadowFactor = 1.0;
                            bOffscreenShadowing = true;
                        }

                        if (bOffscreenShadowing)
                        {
                            ShadowFactor *= TraceOffscreenShadows(WorldPosition, L, ToLight, WorldNormal);
                        }
                    }
                    #endif // End hardware/software shadow selection        
                }
                #endif // End ShadowLight

                // 라이팅
                #if LIGHT_FUNCTION
                    ShadowFactor *= GetLightFunction(WorldPosition);
                #endif

                // 투과
                #if USE_CLOUD_TRANSMITTANCE
                {
                    float OutOpticalDepth = 0.0f;
                    ShadowFactor *= lerp(1.0f, GetCloudVolumetricShadow(WorldPosition, CloudShadowmapWorldToLightClipMatrix, CloudShadowmapFarDepthKm, CloudShadowmapTexture, CloudShadowmapSampler, OutOpticalDepth), CloudShadowmapStrength);
                }
                #endif

                // IES
                if (UseIESProfile > 0)
                {
                    ShadowFactor *= ComputeLightProfileMultiplier(WorldPosition, DeferredLightUniforms.Position, -DeferredLightUniforms.Direction, DeferredLightUniforms.Tangent);
                }

                //
                float NoL = saturate(dot(WorldNormal, L));
                Irradiance = LightColor * (CombinedAttenuation * NoL * ShadowFactor);
                //Irradiance = bShadowFactorValid ? float3(0, 1, 0) : float3(0.2f, 0.0f, 0.0f);
            }
        }
    }
        
    OutColor = float4(Irradiance, 0);
}

6.5.6.6 PrefilterLumenSceneLighting

이 프로세스는 6.5.6.1 복셀 원뿔 추적에 언급된 형상 사전 필터링과 유사합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenScenePrefilter.cpp

void FDeferredShadingSceneRenderer::PrefilterLumenSceneLighting(
    FRDGBuilder& GraphBuilder,
    const FViewInfo& View,
    FLumenCardTracingInputs& TracingInputs,
    FGlobalShaderMap* GlobalShaderMap,
    const FLumenCardScatterContext& VisibleCardScatterContext)
{
    LLM_SCOPE_BYTAG(Lumen);
    RDG_EVENT_SCOPE(GraphBuilder, "Prefilter");

    FLumenSceneData& LumenSceneData = *Scene->LumenSceneData;

    // 에 따라 해상도 계산/산출(Calculate)Mip의 개수 .
    const int32 NumMips = FMath::CeilLogTwo(FMath::Max(LumenSceneData.MaxAtlasSize.X, LumenSceneData.MaxAtlasSize.Y)) + 1;
    {
        FIntPoint SrcSize = LumenSceneData.MaxAtlasSize;
        FIntPoint DestSize = SrcSize / 2;

        // 루프 Mip개수 -1회 (0단계 는 텍스처(Texture)), 회 개 MIP.
        for (int32 MipIndex = 1; MipIndex < NumMips; MipIndex++)
        {
            SrcSize.X = FMath::Max(SrcSize.X, 1);
            SrcSize.Y = FMath::Max(SrcSize.Y, 1);
            DestSize.X = FMath::Max(DestSize.X, 1);
            DestSize.Y = FMath::Max(DestSize.Y, 1);

            FLumenCardPrefilterLighting* PassParameters = GraphBuilder.AllocParameters<FLumenCardPrefilterLighting>();
            
            // 설정(Set)렌더링(Render), 3개 : 라이팅 , , .
            PassParameters->RenderTargets[0] = FRenderTargetBinding(TracingInputs.FinalLightingAtlas, ERenderTargetLoadAction::ENoAction, MipIndex);
            bool bUseIrradianceAtlas = Lumen::UseIrradianceAtlas(View);
            bool bUseIndirectIrradianceAtlas = Lumen::UseIndirectIrradianceAtlas(View);
            if (bUseIrradianceAtlas)
            {
                PassParameters->RenderTargets[1] = FRenderTargetBinding(TracingInputs.IrradianceAtlas, ERenderTargetLoadAction::ENoAction, MipIndex);
                if (bUseIndirectIrradianceAtlas)
                {
                    PassParameters->RenderTargets[2] = FRenderTargetBinding(TracingInputs.IndirectIrradianceAtlas, ERenderTargetLoadAction::ENoAction, MipIndex);
                }
            }
            else if (bUseIndirectIrradianceAtlas)
            {
                PassParameters->RenderTargets[1] = FRenderTargetBinding(TracingInputs.IndirectIrradianceAtlas, ERenderTargetLoadAction::ENoAction, MipIndex);
            }
            PassParameters->VS.LumenCardScene = TracingInputs.LumenCardSceneUniformBuffer;
            PassParameters->VS.CardScatterParameters = VisibleCardScatterContext.Parameters;
            PassParameters->VS.ScatterInstanceIndex = 0;
            PassParameters->VS.CardUVSamplingOffset = FVector2D::ZeroVector;
            PassParameters->PS.View = View.ViewUniformBuffer;
            PassParameters->PS.LumenCardScene = TracingInputs.LumenCardSceneUniformBuffer;
            PassParameters->PS.ParentFinalLightingAtlas = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(TracingInputs.FinalLightingAtlas, MipIndex - 1));
            // ※ 주의: 생성(Create)SRV을(를) 활용하여 의 는 CreateForMipLevel.
            if (bUseIrradianceAtlas)
            {
                PassParameters->PS.ParentIrradianceAtlas = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(TracingInputs.IrradianceAtlas, MipIndex - 1));
            }
            if (bUseIndirectIrradianceAtlas)
            {
                PassParameters->PS.ParentIndirectIrradianceAtlas = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(TracingInputs.IndirectIrradianceAtlas, MipIndex - 1));
            }
            PassParameters->PS.InvSize = FVector2D(1.0f / SrcSize.X, 1.0f / SrcSize.Y);

            FScene* LocalScene = Scene;

            // 추가(Add)필터링 Pass.
            GraphBuilder.AddPass(
                RDG_EVENT_NAME("PrefilterMip"),
                PassParameters,
                ERDGPassFlags::Raster,
                [LocalScene, PassParameters, DestSize, GlobalShaderMap, bUseIrradianceAtlas, bUseIndirectIrradianceAtlas](FRHICommandListImmediate& RHICmdList)
            {
                FLumenCardPrefilterLightingPS::FPermutationDomain PermutationVector;
                PermutationVector.Set<FLumenCardPrefilterLightingPS::FUseIrradianceAtlas>(bUseIrradianceAtlas != 0);
                PermutationVector.Set<FLumenCardPrefilterLightingPS::FUseIndirectIrradianceAtlas>(bUseIndirectIrradianceAtlas != 0);
                auto PixelShader = GlobalShaderMap->GetShader< FLumenCardPrefilterLightingPS >(PermutationVector);
                DrawQuadsToAtlas(DestSize, PixelShader, PassParameters, GlobalShaderMap, TStaticBlendState<>::GetRHI(), RHICmdList);
            });

            SrcSize /= 2;
            DestSize /= 2;
        }
    }
}

사용된 셰이더는 다음과 같습니다.

hlsl
// Engine\Shaders\Private\Lumen\LumenSceneLighting.usf

Texture2D ParentFinalLightingAtlas;
Texture2D ParentIrradianceAtlas;
Texture2D ParentIndirectIrradianceAtlas;

void LumenCardPrefilterLightingPS(
    FCardVSToPS CardInterpolants,
    out float4 OutLighting : SV_Target0,
    out float4 OutColor1 : SV_Target1,
    out float4 OutColor2 : SV_Target2)
{
    // 을(를) 활용하여 바이리니어(Bilinear)필터링 MIP레벨 단계 의 컬러 , 6.5.6.1을(를) 활용하여 가우시안(Gaussian)가중치(Weight).
    OutLighting = Texture2DSampleLevel(ParentFinalLightingAtlas, GlobalBilinearClampedSampler, CardInterpolants.AtlasCoord, 0);
#if USE_IRRADIANCE_ATLAS
    OutColor1 = Texture2DSampleLevel(ParentIrradianceAtlas, GlobalBilinearClampedSampler, CardInterpolants.AtlasCoord, 0);
    #if USE_INDIRECTIRRADIANCE_ATLAS
        OutColor2 = Texture2DSampleLevel(ParentIndirectIrradianceAtlas, GlobalBilinearClampedSampler, CardInterpolants.AtlasCoord, 0);
    #endif
#elif USE_INDIRECTIRRADIANCE_ATLAS
    OutColor1 = Texture2DSampleLevel(ParentIndirectIrradianceAtlas, GlobalBilinearClampedSampler, CardInterpolants.AtlasCoord, 0);
#endif
}

프레임 캡처에서 볼 수 있듯이 텍스처의 MIP 수준은 PrefilterMip의 패스 수와 동일합니다.

6.5.6.7 ComputeLumenSceneVoxelLighting

ComputeLumenSceneVoxelLighting의 주요 기능은 루멘 장면의 복셀 조명을 계산하는 것입니다. 코드는 다음과 같습니다:

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenVoxelLighting.cpp

void FDeferredShadingSceneRenderer::ComputeLumenSceneVoxelLighting(
    FRDGBuilder& GraphBuilder,
    FLumenCardTracingInputs& TracingInputs,
    FGlobalShaderMap* GlobalShaderMap)
{
    LLM_SCOPE_BYTAG(Lumen);

    const FViewInfo& View = Views[0];

    const int32 ClampedNumClipmapLevels = GetNumLumenVoxelClipmaps();
    const FIntVector ClipmapResolution = GetClipmapResolution();
    bool bForceFullUpdate = GLumenSceneVoxelLightingForceFullUpdate != 0;

    // 처리(Process)라이팅 3D텍스처(Texture).
    FRDGTextureRef VoxelLighting = TracingInputs.VoxelLighting;
    {
        FRDGTextureDesc LightingDesc(FRDGTextureDesc::Create3D(
            FIntVector(
                ClipmapResolution.X,
                ClipmapResolution.Y * ClampedNumClipmapLevels,
                ClipmapResolution.Z * GNumVoxelDirections),
            PF_FloatRGBA,
            FClearValueBinding::Black,
            TexCreate_ShaderResource | TexCreate_UAV | TexCreate_3DTiling));

        if (!VoxelLighting || VoxelLighting->Desc != LightingDesc)
        {
            bForceFullUpdate = true;
            VoxelLighting = GraphBuilder.CreateTexture(LightingDesc, TEXT("Lumen.VoxelLighting"));
        }
    }

    // 처리(Process)텍스처(Texture).
    FRDGTextureRef VoxelVisBuffer = View.ViewState->Lumen.VoxelVisBuffer ? GraphBuilder.RegisterExternalTexture(View.ViewState->Lumen.VoxelVisBuffer) : nullptr;
    {
        FRDGTextureDesc VoxelVisBufferDesc(FRDGTextureDesc::Create3D(
            FIntVector(
                ClipmapResolution.X,
                ClipmapResolution.Y * ClampedNumClipmapLevels,
                ClipmapResolution.Z * GNumVoxelDirections),
            PF_R32_UINT,
            FClearValueBinding::Black,
            TexCreate_ShaderResource | TexCreate_UAV | TexCreate_3DTiling));

        if (!VoxelVisBuffer
            || VoxelVisBuffer->Desc.Extent != VoxelVisBufferDesc.Extent
            || VoxelVisBuffer->Desc.Depth != VoxelVisBufferDesc.Depth)
        {
            bForceFullUpdate = true;
            VoxelVisBuffer = GraphBuilder.CreateTexture(VoxelVisBufferDesc, TEXT("Lumen.VoxelVisBuffer"));

            uint32 VisBufferClearValue[4] = { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF };
            AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(VoxelVisBuffer), VisBufferClearValue);
        }
    }

    // 버퍼 데이터 유효(Valid),만약 생성(Create).
    if (View.ViewState->Lumen.VoxelVisBufferCachedScene != Scene)
    {
        bForceFullUpdate = true;
        View.ViewState->Lumen.VoxelVisBufferCachedScene = Scene;
    }

    // 처리(Process)갱신(Update)의 Clipmap.
    TArray<int32, SceneRenderingAllocator> ClipmapsToUpdate;
    ClipmapsToUpdate.Empty(ClampedNumClipmapLevels);

    for (int32 ClipmapIndex = 0; ClipmapIndex < ClampedNumClipmapLevels; ClipmapIndex++)
    {
        if (bForceFullUpdate || ShouldUpdateVoxelClipmap(ClipmapIndex, ClampedNumClipmapLevels, View.ViewState->GetFrameIndex()))
        {
            ClipmapsToUpdate.Add(ClipmapIndex);
        }
    }

    ensureMsgf(bForceFullUpdate || ClipmapsToUpdate.Num() <= 1, TEXT("Tweak ShouldUpdateVoxelClipmap for better clipmap update distribution"));

    FString ClipmapsToUpdateString;

    for (int32 ToUpdateIndex = 0; ToUpdateIndex < ClipmapsToUpdate.Num(); ++ToUpdateIndex)
    {
        ClipmapsToUpdateString += FString::FromInt(ClipmapsToUpdate[ToUpdateIndex]);
        if (ToUpdateIndex + 1 < ClipmapsToUpdate.Num())
        {
            ClipmapsToUpdateString += TEXT(",");
        }
    }

    RDG_EVENT_SCOPE(GraphBuilder, "VoxelizeCards Clipmaps=[%s]", *ClipmapsToUpdateString);

    // 갱신(Update)버퍼 .
    if (ClipmapsToUpdate.Num() > 0)
    {
        TracingInputs.VoxelLighting = VoxelLighting;
        TracingInputs.VoxelGridResolution = GetClipmapResolution();
        TracingInputs.NumClipmapLevels = ClampedNumClipmapLevels;

        // 갱신(Update)버퍼
        UpdateVoxelVisBuffer(GraphBuilder, Scene, View, TracingInputs, VoxelVisBuffer, ClipmapsToUpdate, bForceFullUpdate);
        // 버퍼
        VoxelizeVisBuffer(View, Scene, TracingInputs, VoxelLighting, VoxelVisBuffer, ClipmapsToUpdate, GraphBuilder);

        ConvertToExternalTexture(GraphBuilder, VoxelLighting, View.ViewState->Lumen.VoxelLighting);
        View.ViewState->Lumen.VoxelGridResolution = TracingInputs.VoxelGridResolution;
        View.ViewState->Lumen.NumClipmapLevels = TracingInputs.NumClipmapLevels;
    }

    ConvertToExternalTexture(GraphBuilder, VoxelVisBuffer, View.ViewState->Lumen.VoxelVisBuffer);
}

위의 작업에는 가시성 캐시 업데이트 및 복셀화된 작업이 포함됩니다. 특정 코드는 더 이상 분석되지 않지만 프레임 캡처 프로세스는 다음과 같습니다.

UpdateVoxelVisBuffer 프로세스의 마지막 단계에서 VoxelTraceCS의 입력은 거리 필드 블록 3D 텍스처이고 출력은 VoxelVisBuffer의 3D 텍스처입니다.

VoxelizeVoxelVisBuffer 프로세스의 마지막 단계에서 VisBufferShading의 입력에는 SceneFinalLighting, SceneOpacity, SceneDepth, 거리 필드 블록 3D 텍스처 및 VoxelVisBuffer가 포함되며 출력은 VoxelLighting3D 텍스처입니다. 이 단계 이후에는 Lumen 장면의 조명 정보가 복셀화된 3D 텍스처에 저장되었습니다.

6.5.7 루멘 간접 조명

6.5.7.1 RenderDiffuseIndirectAndAmbientOcclusion

이 단계에서는 이전 루멘 계산에서 생성된 정보를 사용하여 전역 조명 효과를 시뮬레이션하기 위한 최종 간접 조명을 계산합니다. 프로세스는 다음과 같습니다.

SSGI 노이즈 감소, 스크린 공간 프로브 수집, 반사 및 간접광 조합과 같은 여러 단계가 있음을 알 수 있습니다. 해당 소스 코드 RenderDiffuseIndirectAndAmbientOcclusion은 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\IndirectLightRendering.cpp

oid FDeferredShadingSceneRenderer::RenderDiffuseIndirectAndAmbientOcclusion(
    FRDGBuilder& GraphBuilder,
    FSceneTextures& SceneTextures,
    FRDGTextureRef LightingChannelsTexture,
    bool bIsVisualizePass)
{
    using namespace HybridIndirectLighting;

    if (ViewFamily.EngineShowFlags.VisualizeLumenIndirectDiffuse != bIsVisualizePass)
    {
        return;
    }

    RDG_EVENT_SCOPE(GraphBuilder, "DiffuseIndirectAndAO");

    FSceneTextureParameters SceneTextureParameters = GetSceneTextureParameters(GraphBuilder, SceneTextures.UniformBuffer);
    FRDGTextureRef SceneColorTexture = SceneTextures.Color.Target;

    const FRDGSystemTextures& SystemTextures = FRDGSystemTextures::Get(GraphBuilder);

    // 개 view계산/산출(Calculate)회 .
    for (FViewInfo& View : Views)
    {
        RDG_GPU_MASK_SCOPE(GraphBuilder, View.GPUMask);

        const FPerViewPipelineState& ViewPipelineState = GetViewPipelineState(View);

        int32 DenoiseMode = CVarDiffuseIndirectDenoiser.GetValueOnRenderThread();

        // 설정(Set)을(를) 활용하여 의 디퓨즈(Diffuse)파라미터 .
        FCommonParameters CommonDiffuseParameters;
        SetupCommonDiffuseIndirectParameters(GraphBuilder, SceneTextureParameters, View, /* out */ CommonDiffuseParameters);

        // 로 디노이징(Denoising)갱신(Update)의 레이 트레이싱(Ray Tracing)구성(Configuration).
        IScreenSpaceDenoiser::FAmbientOcclusionRayTracingConfig RayTracingConfig;
        {
            RayTracingConfig.RayCountPerPixel = CommonDiffuseParameters.RayCountPerPixel;
            RayTracingConfig.ResolutionFraction = 1.0f / float(CommonDiffuseParameters.DownscaleFactor);
        }

        // 이전 프레임(Previous Frame)씬 컬러(SceneColor)
        ScreenSpaceRayTracing::FPrevSceneColorMip PrevSceneColorMip;
        if ((ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen || ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::SSGI) && View.PrevViewInfo.ScreenSpaceRayTracingInput.IsValid())
        {
            PrevSceneColorMip = ScreenSpaceRayTracing::ReducePrevSceneColorMip(GraphBuilder, SceneTextureParameters, View);
        }

        // 디노이징(Denoising)입력출력파라미터
        FSSDSignalTextures DenoiserOutputs;
        IScreenSpaceDenoiser::FDiffuseIndirectInputs DenoiserInputs;
        IScreenSpaceDenoiser::FDiffuseIndirectHarmonic DenoiserSphericalHarmonicInputs;
        FLumenReflectionCompositeParameters LumenReflectionCompositeParameters;
        bool bLumenUseDenoiserComposite = ViewPipelineState.bUseLumenProbeHierarchy;

        // 에 따라 의 메서드 디노이징(Denoising)입력 또는 출력구조체 .
        
        // Lumen레벨 회 구조체
        if (ViewPipelineState.bUseLumenProbeHierarchy)
        {
            check(ViewPipelineState.DiffuseIndirectDenoiser == IScreenSpaceDenoiser::EMode::Disabled);
            DenoiserOutputs = RenderLumenProbeHierarchy(
                GraphBuilder,
                SceneTextures,
                CommonDiffuseParameters, PrevSceneColorMip,
                View, &View.PrevViewInfo);
        }
        // 스크린 스페이스 글로벌 일루미네이션(SSGI)
        else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::SSGI)
        {
            RDG_EVENT_SCOPE(GraphBuilder, "SSGI %dx%d", CommonDiffuseParameters.TracingViewportSize.X, CommonDiffuseParameters.TracingViewportSize.Y);
            DenoiserInputs = ScreenSpaceRayTracing::CastStandaloneDiffuseIndirectRays(
                GraphBuilder, CommonDiffuseParameters, PrevSceneColorMip, View);
        }
        // 레이 트레이싱(Ray Tracing)글로벌 라이팅
        else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::RTGI)
        {
            // TODO: Refactor under the HybridIndirectLighting standard API.
            // TODO: hybrid SSGI / RTGI
            RenderRayTracingGlobalIllumination(GraphBuilder, SceneTextureParameters, View, /* out */ &RayTracingConfig, /* out */ &DenoiserInputs);
        }
        // Lumen글로벌 라이팅
        else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen)
        {
            check(ViewPipelineState.DiffuseIndirectDenoiser == IScreenSpaceDenoiser::EMode::Disabled);

            FLumenMeshSDFGridParameters MeshSDFGridParameters;

            DenoiserOutputs = RenderLumenScreenProbeGather(
                GraphBuilder, 
                SceneTextures,
                PrevSceneColorMip, 
                LightingChannelsTexture,
                View,
                &View.PrevViewInfo,
                bLumenUseDenoiserComposite,
                MeshSDFGridParameters);

            if (ViewPipelineState.ReflectionsMethod == EReflectionsMethod::Lumen)
            {
                DenoiserOutputs.Textures[2] = RenderLumenReflections(
                    GraphBuilder,
                    View,
                    SceneTextures, 
                    MeshSDFGridParameters,
                    LumenReflectionCompositeParameters);
            }

            if (!DenoiserOutputs.Textures[2])
            {
                DenoiserOutputs.Textures[2] = DenoiserOutputs.Textures[1];
            }
        }

        FRDGTextureRef AmbientOcclusionMask = DenoiserInputs.AmbientOcclusionMask;

        // 처리(Process)디노이징(Denoising).
        if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen)
        {
            // 에 의해 Lumen글로벌 출력의 디노이징(Denoising), 따라서 .
        }
        else if (ViewPipelineState.DiffuseIndirectDenoiser == IScreenSpaceDenoiser::EMode::Disabled)
        {
            DenoiserOutputs.Textures[0] = DenoiserInputs.Color;
            DenoiserOutputs.Textures[1] = SystemTextures.White;
        }
        else
        {
            const IScreenSpaceDenoiser* DefaultDenoiser = IScreenSpaceDenoiser::GetDefaultDenoiser();
            const IScreenSpaceDenoiser* DenoiserToUse = 
                ViewPipelineState.DiffuseIndirectDenoiser == IScreenSpaceDenoiser::EMode::DefaultDenoiser
                ? DefaultDenoiser : GScreenSpaceDenoiser;

            RDG_EVENT_SCOPE(GraphBuilder, "%s%s(DiffuseIndirect) %dx%d",
                DenoiserToUse != DefaultDenoiser ? TEXT("ThirdParty ") : TEXT(""),
                DenoiserToUse->GetDebugName(),
                View.ViewRect.Width(), View.ViewRect.Height());

            if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::RTGI)
            {
                // RTGI디노이징(Denoising).
                DenoiserOutputs = DenoiserToUse->DenoiseDiffuseIndirect(
                    GraphBuilder,
                    View,
                    &View.PrevViewInfo,
                    SceneTextureParameters,
                    DenoiserInputs,
                    RayTracingConfig);

                AmbientOcclusionMask = DenoiserOutputs.Textures[1];
            }
            else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::SSGI)
            {
                // SSGI의 결과 디노이징(Denoising).
                DenoiserOutputs = DenoiserToUse->DenoiseScreenSpaceDiffuseIndirect(
                    GraphBuilder,
                    View,
                    &View.PrevViewInfo,
                    SceneTextureParameters,
                    DenoiserInputs,
                    RayTracingConfig);

                AmbientOcclusionMask = DenoiserOutputs.Textures[1];
            }
        }

        // 렌더링(Render)AO
        bool bWritableAmbientOcclusionMask = true;
        if (ViewPipelineState.AmbientOcclusionMethod == EAmbientOcclusionMethod::Disabled)
        {
            ensure(!HasBeenProduced(SceneTextures.ScreenSpaceAO));
            AmbientOcclusionMask = nullptr;
            bWritableAmbientOcclusionMask = false;
        }
        else if (ViewPipelineState.AmbientOcclusionMethod == EAmbientOcclusionMethod::RTAO)
        {
            RenderRayTracingAmbientOcclusion(
                GraphBuilder,
                View,
                SceneTextureParameters,
                &AmbientOcclusionMask);
        }
        else if (ViewPipelineState.AmbientOcclusionMethod == EAmbientOcclusionMethod::SSGI)
        {
            check(AmbientOcclusionMask);
        }
        else if (ViewPipelineState.AmbientOcclusionMethod == EAmbientOcclusionMethod::SSAO)
        {
            // Fetch result of SSAO that was done earlier.
            if (HasBeenProduced(SceneTextures.ScreenSpaceAO))
            {
                AmbientOcclusionMask = SceneTextures.ScreenSpaceAO;
            }
            else
            {
                AmbientOcclusionMask = GetScreenSpaceAOFallback(SystemTextures);
                bWritableAmbientOcclusionMask = false;
            }
        }
        else
        {
            unimplemented();
            bWritableAmbientOcclusionMask = false;
        }

        // Extract the dynamic AO for application of AO beyond RenderDiffuseIndirectAndAmbientOcclusion()
        if (AmbientOcclusionMask && ViewPipelineState.AmbientOcclusionMethod != EAmbientOcclusionMethod::SSAO)
        {
            ensureMsgf(Views.Num() == 1, TEXT("Need to add support for one AO texture per view in FSceneTextures"));
            SceneTextures.ScreenSpaceAO = AmbientOcclusionMask;
        }

        if (HairStrands::HasViewHairStrandsData(View) && (ViewPipelineState.AmbientOcclusionMethod == EAmbientOcclusionMethod::SSGI || ViewPipelineState.AmbientOcclusionMethod == EAmbientOcclusionMethod::SSAO) && bWritableAmbientOcclusionMask)
        {
            RenderHairStrandsAmbientOcclusion(
                GraphBuilder,
                View,
                AmbientOcclusionMask);
        }

        // 적용 디퓨즈(Diffuse) 및 AO까지 씬 컬러(SceneColor).
        if ((DenoiserOutputs.Textures[0] || AmbientOcclusionMask) && (!bIsVisualizePass || ViewPipelineState.DiffuseIndirectDenoiser != IScreenSpaceDenoiser::EMode::Disabled || ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen)
            && !IsMetalPlatform(ShaderPlatform))
        {
            // 을(를) 활용하여 의 PS는 FDiffuseIndirectCompositePS
            FDiffuseIndirectCompositePS::FParameters* PassParameters = GraphBuilder.AllocParameters<FDiffuseIndirectCompositePS::FParameters>();
            
            PassParameters->AmbientOcclusionStaticFraction = FMath::Clamp(View.FinalPostProcessSettings.AmbientOcclusionStaticFraction, 0.0f, 1.0f);

            PassParameters->ApplyAOToDynamicDiffuseIndirect = 0.0f;

            if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen)
            {
                PassParameters->ApplyAOToDynamicDiffuseIndirect = 1.0f;
            }

            const FIntPoint BufferExtent = SceneTextureParameters.SceneDepthTexture->Desc.Extent;

            {
                // Placeholder texture for textures pulled in from SSDCommon.ush
                FRDGTextureDesc Desc = FRDGTextureDesc::Create2D(
                    FIntPoint(1),
                    PF_R32_UINT,
                    FClearValueBinding::Black,
                    TexCreate_ShaderResource);
                FRDGTextureRef CompressedMetadataPlaceholder = GraphBuilder.CreateTexture(Desc, TEXT("CompressedMetadataPlaceholder"));

                PassParameters->CompressedMetadata[0] = CompressedMetadataPlaceholder;
                PassParameters->CompressedMetadata[1] = CompressedMetadataPlaceholder;
            }

            PassParameters->BufferUVToOutputPixelPosition = BufferExtent;
            PassParameters->EyeAdaptation = GetEyeAdaptationTexture(GraphBuilder, View);
            PassParameters->LumenReflectionCompositeParameters = LumenReflectionCompositeParameters;

            PassParameters->bVisualizeDiffuseIndirect = bIsVisualizePass;

            PassParameters->DiffuseIndirect = DenoiserOutputs;
            PassParameters->DiffuseIndirectSampler = TStaticSamplerState<SF_Point>::GetRHI();

            PassParameters->PreIntegratedGF = GSystemTextures.PreintegratedGF->GetRenderTargetItem().ShaderResourceTexture;
            PassParameters->PreIntegratedGFSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();

            PassParameters->AmbientOcclusionTexture = AmbientOcclusionMask;
            PassParameters->AmbientOcclusionSampler = TStaticSamplerState<SF_Point>::GetRHI();
            
            if (!PassParameters->AmbientOcclusionTexture || bIsVisualizePass)
            {
                PassParameters->AmbientOcclusionTexture = SystemTextures.White;
            }

            // 설정(Set)디노이징(Denoising)의 을(를) 활용하여 shader파라미터 .
            Denoiser::SetupCommonShaderParameters(
                View, SceneTextureParameters,
                View.ViewRect,
                1.0f / CommonDiffuseParameters.DownscaleFactor,
                /* out */ &PassParameters->DenoiserCommonParameters);
            PassParameters->SceneTextures = SceneTextureParameters;
            PassParameters->ViewUniformBuffer = View.ViewUniformBuffer;

            PassParameters->RenderTargets[0] = FRenderTargetBinding(
                SceneColorTexture, ERenderTargetLoadAction::ELoad);

            {
                FRDGTextureDesc Desc = FRDGTextureDesc::Create2D(
                    SceneColorTexture->Desc.Extent,
                    PF_FloatRGBA,
                    FClearValueBinding::None,
                    TexCreate_ShaderResource | TexCreate_UAV);

                PassParameters->PassDebugOutput = GraphBuilder.CreateUAV(
                    GraphBuilder.CreateTexture(Desc, TEXT("DebugDiffuseIndirectComposite")));
            }

            const TCHAR* DiffuseIndirectSampling = TEXT("Disabled");
            FDiffuseIndirectCompositePS::FPermutationDomain PermutationVector;
            bool bUpscale = false;

            if (DenoiserOutputs.Textures[0])
            {
                if (bLumenUseDenoiserComposite)
                {
                    PermutationVector.Set<FDiffuseIndirectCompositePS::FApplyDiffuseIndirectDim>(2);
                    DiffuseIndirectSampling = TEXT("ProbeHierarchy");
                }
                else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::RTGI)
                {
                    PermutationVector.Set<FDiffuseIndirectCompositePS::FApplyDiffuseIndirectDim>(3);
                    DiffuseIndirectSampling = TEXT("RTGI");
                }
                else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen)
                {
                    PermutationVector.Set<FDiffuseIndirectCompositePS::FApplyDiffuseIndirectDim>(4);
                    DiffuseIndirectSampling = TEXT("ScreenProbeGather");
                }
                else
                {
                    PermutationVector.Set<FDiffuseIndirectCompositePS::FApplyDiffuseIndirectDim>(1);
                    DiffuseIndirectSampling = TEXT("SSGI");
                    bUpscale = DenoiserOutputs.Textures[0]->Desc.Extent != SceneColorTexture->Desc.Extent;
                }

                PermutationVector.Set<FDiffuseIndirectCompositePS::FUpscaleDiffuseIndirectDim>(bUpscale);
            }

            TShaderMapRef<FDiffuseIndirectCompositePS> PixelShader(View.ShaderMap, PermutationVector);
            // 및 최적화 을(를) 활용하여 의 shader리소스 바인딩(Bind).
            ClearUnusedGraphResources(PixelShader, PassParameters);

            FRHIBlendState* BlendState = TStaticBlendState<CW_RGBA, BO_Add, BF_One, BF_Source1Color, BO_Add, BF_One, BF_Source1Alpha>::GetRHI();

            if (bIsVisualizePass)
            {
                BlendState = TStaticBlendState<>::GetRHI();
            }

            // Pass.
            FPixelShaderUtils::AddFullscreenPass(
                GraphBuilder,
                View.ShaderMap,
                RDG_EVENT_NAME(
                    "DiffuseIndirectComposite(DiffuseIndirect=%s%s%s%s) %dx%d",
                    DiffuseIndirectSampling,
                    PermutationVector.Get<FDiffuseIndirectCompositePS::FUpscaleDiffuseIndirectDim>() ? TEXT(" UpscaleDiffuseIndirect") : TEXT(""),
                    AmbientOcclusionMask ? TEXT(" ApplyAOToSceneColor") : TEXT(""),
                    PassParameters->ApplyAOToDynamicDiffuseIndirect > 0.0f ? TEXT(" ApplyAOToDynamicDiffuseIndirect") : TEXT(""),
                    View.ViewRect.Width(), View.ViewRect.Height()),
                PixelShader,
                PassParameters,
                View.ViewRect,
                BlendState);
        } // if (DenoiserOutputs.Color || bApplySSAO)

        // 적용 cubemap.
        if (IsAmbientCubemapPassRequired(View) && !bIsVisualizePass && !ViewPipelineState.bUseLumenProbeHierarchy)
        {
            FAmbientCubemapCompositePS::FParameters* PassParameters = GraphBuilder.AllocParameters<FAmbientCubemapCompositePS::FParameters>();
            
            PassParameters->PreIntegratedGF = GSystemTextures.PreintegratedGF->GetRenderTargetItem().ShaderResourceTexture;
            PassParameters->PreIntegratedGFSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
            
            PassParameters->AmbientOcclusionTexture = AmbientOcclusionMask;
            PassParameters->AmbientOcclusionSampler = TStaticSamplerState<SF_Point>::GetRHI();
            
            if (!PassParameters->AmbientOcclusionTexture)
            {
                PassParameters->AmbientOcclusionTexture = SystemTextures.White;
            }

            PassParameters->SceneTextures = SceneTextureParameters;
            PassParameters->ViewUniformBuffer = View.ViewUniformBuffer;

            PassParameters->RenderTargets[0] = FRenderTargetBinding(
                SceneColorTexture, ERenderTargetLoadAction::ELoad);
        
            TShaderMapRef<FAmbientCubemapCompositePS> PixelShader(View.ShaderMap);
            GraphBuilder.AddPass(
                RDG_EVENT_NAME("AmbientCubemapComposite %dx%d", View.ViewRect.Width(), View.ViewRect.Height()),
                PassParameters,
                ERDGPassFlags::Raster,
                [PassParameters, &View, PixelShader](FRHICommandList& RHICmdList)
            {
                TShaderMapRef<FPostProcessVS> VertexShader(View.ShaderMap);
                
                RHICmdList.SetViewport(View.ViewRect.Min.X, View.ViewRect.Min.Y, 0.0f, View.ViewRect.Max.X, View.ViewRect.Max.Y, 0.0);

                FGraphicsPipelineStateInitializer GraphicsPSOInit;
                RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);

                // set the state
                GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGB, BO_Add, BF_One, BF_One, BO_Add, BF_One, BF_One>::GetRHI();
                GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
                GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();

                GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
                GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
                GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
                GraphicsPSOInit.PrimitiveType = PT_TriangleList;

                SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);

                uint32 Count = View.FinalPostProcessSettings.ContributingCubemaps.Num();
                for (const FFinalPostProcessSettings::FCubemapEntry& CubemapEntry : View.FinalPostProcessSettings.ContributingCubemaps)
                {
                    FAmbientCubemapCompositePS::FParameters ShaderParameters = *PassParameters;
                    SetupAmbientCubemapParameters(CubemapEntry, &ShaderParameters.AmbientCubemap);
                    SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), ShaderParameters);
                    
                    DrawPostProcessPass(
                        RHICmdList,
                        0, 0,
                        View.ViewRect.Width(), View.ViewRect.Height(),
                        View.ViewRect.Min.X, View.ViewRect.Min.Y,
                        View.ViewRect.Width(), View.ViewRect.Height(),
                        View.ViewRect.Size(),
                        GetSceneTextureExtent(),
                        VertexShader,
                        View.StereoPass, 
                        false, // TODO.
                        EDRF_UseTriangleOptimization);
                }
            });
        } // if (IsAmbientCubemapPassRequired(View))
    } // for (FViewInfo& View : Views)
}

6.5.7.2 RenderLumenScreenProbeGather

RenderLumenScreenProbeGather의 기능은 Lumen 화면 공간의 프로브 컬렉션을 렌더링하는 것입니다. 해당 코드는 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenScreenProbeGather.cpp

FSSDSignalTextures FDeferredShadingSceneRenderer::RenderLumenScreenProbeGather(
    FRDGBuilder& GraphBuilder,
    const FSceneTextures& SceneTextures,
    const ScreenSpaceRayTracing::FPrevSceneColorMip& PrevSceneColorMip,
    FRDGTextureRef LightingChannelsTexture,
    const FViewInfo& View,
    FPreviousViewInfo* PreviousViewInfos,
    bool& bLumenUseDenoiserComposite,
    FLumenMeshSDFGridParameters& MeshSDFGridParameters)
{
    LLM_SCOPE_BYTAG(Lumen);

    // 렌더링(Render)Lumen.
    if (GLumenIrradianceFieldGather != 0)
    {
        bLumenUseDenoiserComposite = false;
        return RenderLumenIrradianceFieldGather(GraphBuilder, SceneTextures, View);
    }

    RDG_EVENT_SCOPE(GraphBuilder, "LumenScreenProbeGather");
    RDG_GPU_STAT_SCOPE(GraphBuilder, LumenScreenProbeGather);

    check(ShouldRenderLumenDiffuseGI(Scene, View, true));
    const FRDGSystemTextures& SystemTextures = FRDGSystemTextures::Get(GraphBuilder);

    if (!LightingChannelsTexture)
    {
        LightingChannelsTexture = SystemTextures.Black;
    }

    // 만약 활성화(Enable)LumenScreenProbeGather, 이면 디노이징(Denoising)입력.
    if (!GLumenScreenProbeGather)
    {
        FSSDSignalTextures ScreenSpaceDenoiserInputs;
        ScreenSpaceDenoiserInputs.Textures[0] = SystemTextures.Black;
        FRDGTextureDesc RoughSpecularIndirectDesc = FRDGTextureDesc::Create2D(SceneTextures.Config.Extent, PF_FloatRGB, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV);
        ScreenSpaceDenoiserInputs.Textures[1] = GraphBuilder.CreateTexture(RoughSpecularIndirectDesc, TEXT("Lumen.ScreenProbeGather.RoughSpecularIndirect"));
        AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenSpaceDenoiserInputs.Textures[1])), FLinearColor::Black);
        bLumenUseDenoiserComposite = false;
        return ScreenSpaceDenoiserInputs;
    }

    // 로부터 버퍼 을(를) 활용하여 텍스처(Texture).
    const FSceneTextureParameters SceneTextureParameters = GetSceneTextureParameters(GraphBuilder, SceneTextures.UniformBuffer);

    // 설정(Set)스크린 스페이스의 파라미터 .
    FScreenProbeParameters ScreenProbeParameters;
    ScreenProbeParameters.ScreenProbeTracingOctahedronResolution = LumenScreenProbeGather::GetTracingOctahedronResolution(View);
    ensureMsgf(ScreenProbeParameters.ScreenProbeTracingOctahedronResolution < (1 << 6) - 1, TEXT("Tracing resolution %u was larger than supported by PackRayInfo()"), ScreenProbeParameters.ScreenProbeTracingOctahedronResolution);
    ScreenProbeParameters.ScreenProbeGatherOctahedronResolution = LumenScreenProbeGather::GetGatherOctahedronResolution(ScreenProbeParameters.ScreenProbeTracingOctahedronResolution);
    ScreenProbeParameters.ScreenProbeGatherOctahedronResolutionWithBorder = ScreenProbeParameters.ScreenProbeGatherOctahedronResolution + 2 * (1 << (GLumenScreenProbeGatherNumMips - 1));
    ScreenProbeParameters.ScreenProbeDownsampleFactor = LumenScreenProbeGather::GetScreenDownsampleFactor(View);

    ScreenProbeParameters.ScreenProbeViewSize = FIntPoint::DivideAndRoundUp(View.ViewRect.Size(), (int32)ScreenProbeParameters.ScreenProbeDownsampleFactor);
    ScreenProbeParameters.ScreenProbeAtlasViewSize = ScreenProbeParameters.ScreenProbeViewSize;
    ScreenProbeParameters.ScreenProbeAtlasViewSize.Y += FMath::TruncToInt(ScreenProbeParameters.ScreenProbeViewSize.Y * GLumenScreenProbeGatherAdaptiveProbeAllocationFraction);

    ScreenProbeParameters.ScreenProbeAtlasBufferSize = FIntPoint::DivideAndRoundUp(SceneTextures.Config.Extent, (int32)ScreenProbeParameters.ScreenProbeDownsampleFactor);
    ScreenProbeParameters.ScreenProbeAtlasBufferSize.Y += FMath::TruncToInt(ScreenProbeParameters.ScreenProbeAtlasBufferSize.Y * GLumenScreenProbeGatherAdaptiveProbeAllocationFraction);

    ScreenProbeParameters.ScreenProbeGatherMaxMip = GLumenScreenProbeGatherNumMips - 1;
    ScreenProbeParameters.RelativeSpeedDifferenceToConsiderLightingMoving = GLumenScreenProbeRelativeSpeedDifferenceToConsiderLightingMoving;
    ScreenProbeParameters.ScreenTraceNoFallbackThicknessScale = Lumen::UseHardwareRayTracedScreenProbeGather() ? 1.0f : GLumenScreenProbeScreenTracesThicknessScaleWhenNoFallback;
    ScreenProbeParameters.NumUniformScreenProbes = ScreenProbeParameters.ScreenProbeViewSize.X * ScreenProbeParameters.ScreenProbeViewSize.Y;
    ScreenProbeParameters.MaxNumAdaptiveProbes = FMath::TruncToInt(ScreenProbeParameters.NumUniformScreenProbes * GLumenScreenProbeGatherAdaptiveProbeAllocationFraction);
    extern int32 GLumenScreenProbeGatherVisualizeTraces;
    ScreenProbeParameters.FixedJitterIndex = GLumenScreenProbeGatherVisualizeTraces == 0 ? GLumenScreenProbeFixedJitterIndex : 6;

    FRDGTextureDesc DownsampledDepthDesc(FRDGTextureDesc::Create2D(ScreenProbeParameters.ScreenProbeAtlasBufferSize, PF_R32_UINT, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV));
    ScreenProbeParameters.ScreenProbeSceneDepth = GraphBuilder.CreateTexture(DownsampledDepthDesc, TEXT("Lumen.ScreenProbeGather.ScreenProbeSceneDepth"));

    FRDGTextureDesc DownsampledSpeedDesc(FRDGTextureDesc::Create2D(ScreenProbeParameters.ScreenProbeAtlasBufferSize, PF_R16F, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV));
    ScreenProbeParameters.ScreenProbeWorldSpeed = GraphBuilder.CreateTexture(DownsampledSpeedDesc, TEXT("Lumen.ScreenProbeGather.ScreenProbeWorldSpeed"));

    FBlueNoise BlueNoise;
    InitializeBlueNoise(BlueNoise);
    ScreenProbeParameters.BlueNoise = CreateUniformBufferImmediate(BlueNoise, EUniformBufferUsage::UniformBuffer_SingleDraw);

    ScreenProbeParameters.OctahedralSolidAngleParameters.OctahedralSolidAngleTextureResolutionSq = GLumenOctahedralSolidAngleTextureSize * GLumenOctahedralSolidAngleTextureSize;
    ScreenProbeParameters.OctahedralSolidAngleParameters.OctahedralSolidAngleTexture = InitializeOctahedralSolidAngleTexture(GraphBuilder, View.ShaderMap, GLumenOctahedralSolidAngleTextureSize, View.ViewState->Lumen.ScreenProbeGatherState.OctahedralSolidAngleTextureRT);

    // 다운샘플링(Downsampling)뎁스 .
    {
        FScreenProbeDownsampleDepthUniformCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FScreenProbeDownsampleDepthUniformCS::FParameters>();
        PassParameters->RWScreenProbeSceneDepth = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenProbeSceneDepth));
        PassParameters->RWScreenProbeWorldSpeed = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenProbeWorldSpeed));
        PassParameters->View = View.ViewUniformBuffer;
        PassParameters->SceneTexturesStruct = SceneTextures.UniformBuffer;
        PassParameters->SceneTextures = SceneTextureParameters;
        PassParameters->ScreenProbeParameters = ScreenProbeParameters;

        auto ComputeShader = View.ShaderMap->GetShader<FScreenProbeDownsampleDepthUniformCS>(0);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("UniformPlacement DownsampleFactor=%u", ScreenProbeParameters.ScreenProbeDownsampleFactor),
            ComputeShader,
            PassParameters,
            FComputeShaderUtils::GetGroupCount(ScreenProbeParameters.ScreenProbeViewSize, FScreenProbeDownsampleDepthUniformCS::GetGroupSize()));
    }

    FRDGBufferRef NumAdaptiveScreenProbes = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(sizeof(uint32), 1), TEXT("Lumen.ScreenProbeGather.NumAdaptiveScreenProbes"));
    FRDGBufferRef AdaptiveScreenProbeData = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(sizeof(uint32), FMath::Max<uint32>(ScreenProbeParameters.MaxNumAdaptiveProbes, 1)), TEXT("Lumen.ScreenProbeGather.daptiveScreenProbeData"));

    ScreenProbeParameters.NumAdaptiveScreenProbes = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(NumAdaptiveScreenProbes, PF_R32_UINT));
    ScreenProbeParameters.AdaptiveScreenProbeData = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(AdaptiveScreenProbeData, PF_R32_UINT));

    const FIntPoint ScreenProbeViewportBufferSize = FIntPoint::DivideAndRoundUp(SceneTextures.Config.Extent, (int32)ScreenProbeParameters.ScreenProbeDownsampleFactor);
    FRDGTextureDesc ScreenTileAdaptiveProbeHeaderDesc(FRDGTextureDesc::Create2D(ScreenProbeViewportBufferSize, PF_R32_UINT, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV));
    FIntPoint ScreenTileAdaptiveProbeIndicesBufferSize = FIntPoint(ScreenProbeViewportBufferSize.X * ScreenProbeParameters.ScreenProbeDownsampleFactor, ScreenProbeViewportBufferSize.Y * ScreenProbeParameters.ScreenProbeDownsampleFactor);
    FRDGTextureDesc ScreenTileAdaptiveProbeIndicesDesc(FRDGTextureDesc::Create2D(ScreenTileAdaptiveProbeIndicesBufferSize, PF_R16_UINT, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV));
    ScreenProbeParameters.ScreenTileAdaptiveProbeHeader = GraphBuilder.CreateTexture(ScreenTileAdaptiveProbeHeaderDesc, TEXT("Lumen.ScreenProbeGather.ScreenTileAdaptiveProbeHeader"));
    ScreenProbeParameters.ScreenTileAdaptiveProbeIndices = GraphBuilder.CreateTexture(ScreenTileAdaptiveProbeIndicesDesc, TEXT("Lumen.ScreenProbeGather.ScreenTileAdaptiveProbeIndices"));

    FComputeShaderUtils::ClearUAV(GraphBuilder, View.ShaderMap, GraphBuilder.CreateUAV(FRDGBufferUAVDesc(NumAdaptiveScreenProbes, PF_R32_UINT)), 0);
    uint32 ClearValues[4] = {0, 0, 0, 0};
    AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenTileAdaptiveProbeHeader)), ClearValues);

    const uint32 AdaptiveProbeMinDownsampleFactor = FMath::Clamp(GLumenScreenProbeGatherAdaptiveProbeMinDownsampleFactor, 1, 64);

    if (ScreenProbeParameters.MaxNumAdaptiveProbes > 0 && AdaptiveProbeMinDownsampleFactor < ScreenProbeParameters.ScreenProbeDownsampleFactor)
    { 
        // 위치 .
        uint32 PlacementDownsampleFactor = ScreenProbeParameters.ScreenProbeDownsampleFactor;
        do
        {
            PlacementDownsampleFactor /= 2;
            FScreenProbeAdaptivePlacementCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FScreenProbeAdaptivePlacementCS::FParameters>();
            PassParameters->RWScreenProbeSceneDepth = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenProbeSceneDepth));
            PassParameters->RWScreenProbeWorldSpeed = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenProbeWorldSpeed));
            PassParameters->RWNumAdaptiveScreenProbes = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(NumAdaptiveScreenProbes, PF_R32_UINT));
            PassParameters->RWAdaptiveScreenProbeData = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(AdaptiveScreenProbeData, PF_R32_UINT));
            PassParameters->RWScreenTileAdaptiveProbeHeader = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenTileAdaptiveProbeHeader));
            PassParameters->RWScreenTileAdaptiveProbeIndices = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenTileAdaptiveProbeIndices));
            PassParameters->View = View.ViewUniformBuffer;
            PassParameters->SceneTexturesStruct = SceneTextures.UniformBuffer;
            PassParameters->SceneTextures = SceneTextureParameters;
            PassParameters->ScreenProbeParameters = ScreenProbeParameters;
            PassParameters->PlacementDownsampleFactor = PlacementDownsampleFactor;

            auto ComputeShader = View.ShaderMap->GetShader<FScreenProbeAdaptivePlacementCS>(0);

            FComputeShaderUtils::AddPass(
                GraphBuilder,
                RDG_EVENT_NAME("AdaptivePlacement DownsampleFactor=%u", PlacementDownsampleFactor),
                ComputeShader,
                PassParameters,
                FComputeShaderUtils::GetGroupCount(FIntPoint::DivideAndRoundDown(View.ViewRect.Size(), (int32)PlacementDownsampleFactor), FScreenProbeAdaptivePlacementCS::GetGroupSize()));
        }
        while (PlacementDownsampleFactor > AdaptiveProbeMinDownsampleFactor);
    }
    else
    {
        FComputeShaderUtils::ClearUAV(GraphBuilder, View.ShaderMap, GraphBuilder.CreateUAV(FRDGBufferUAVDesc(AdaptiveScreenProbeData, PF_R32_UINT)), 0);
        AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.ScreenTileAdaptiveProbeIndices)), ClearValues);
    }

    FRDGBufferRef ScreenProbeIndirectArgs = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateIndirectDesc<FRHIDispatchIndirectParameters>((uint32)EScreenProbeIndirectArgs::Max), TEXT("Lumen.ScreenProbeGather.ScreenProbeIndirectArgs"));

    // 설정(Set)의 파라미터 .
    {
        FSetupAdaptiveProbeIndirectArgsCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FSetupAdaptiveProbeIndirectArgsCS::FParameters>();
        PassParameters->RWScreenProbeIndirectArgs = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(ScreenProbeIndirectArgs, PF_R32_UINT));
        PassParameters->ScreenProbeParameters = ScreenProbeParameters;

        auto ComputeShader = View.ShaderMap->GetShader<FSetupAdaptiveProbeIndirectArgsCS>(0);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("SetupAdaptiveProbeIndirectArgs"),
            ComputeShader,
            PassParameters,
            FIntVector(1, 1, 1));
    }

    ScreenProbeParameters.ProbeIndirectArgs = ScreenProbeIndirectArgs;

    FLumenCardTracingInputs TracingInputs(GraphBuilder, Scene, View);

    FRDGTextureRef BRDFProbabilityDensityFunction = nullptr;
    FRDGBufferSRVRef BRDFProbabilityDensityFunctionSH = nullptr;
    GenerateBRDF_PDF(GraphBuilder, View, SceneTextures, BRDFProbabilityDensityFunction, BRDFProbabilityDensityFunctionSH, ScreenProbeParameters);

    const LumenRadianceCache::FRadianceCacheInputs RadianceCacheInputs = LumenScreenProbeGatherRadianceCache::SetupRadianceCacheInputs();
    LumenRadianceCache::FRadianceCacheInterpolationParameters RadianceCacheParameters;

    // 캐싱(Cache).
    if (LumenScreenProbeGather::UseRadianceCache(View))
    {
        FScreenGatherMarkUsedProbesData MarkUsedProbesData;
        MarkUsedProbesData.Parameters.View = View.ViewUniformBuffer;
        MarkUsedProbesData.Parameters.SceneTexturesStruct = SceneTextures.UniformBuffer;
        MarkUsedProbesData.Parameters.ScreenProbeParameters = ScreenProbeParameters;
        MarkUsedProbesData.Parameters.VisualizeLumenScene = View.Family->EngineShowFlags.VisualizeLumenScene != 0 ? 1 : 0;
        MarkUsedProbesData.Parameters.RadianceCacheParameters = RadianceCacheParameters;

        // 렌더링(Render)캐싱(Cache).
        RenderRadianceCache(
            GraphBuilder, 
            TracingInputs, 
            RadianceCacheInputs, 
            Scene,
            View, 
            &ScreenProbeParameters, 
            BRDFProbabilityDensityFunctionSH, 
            FMarkUsedRadianceCacheProbes::CreateStatic(&ScreenGatherMarkUsedProbes), 
            &MarkUsedProbesData, 
            View.ViewState->RadianceCacheState, 
            RadianceCacheParameters);
    }

    if (LumenScreenProbeGather::UseImportanceSampling(View))
    {
        // 샘플링광선(Ray).
        GenerateImportanceSamplingRays(
            GraphBuilder,
            View,
            SceneTextures,
            RadianceCacheParameters,
            BRDFProbabilityDensityFunction,
            BRDFProbabilityDensityFunctionSH,
            ScreenProbeParameters);
    }

    const FIntPoint ScreenProbeTraceBufferSize = ScreenProbeParameters.ScreenProbeAtlasBufferSize * ScreenProbeParameters.ScreenProbeTracingOctahedronResolution;
    FRDGTextureDesc TraceRadianceDesc(FRDGTextureDesc::Create2D(ScreenProbeTraceBufferSize, PF_FloatRGB, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV));
    ScreenProbeParameters.TraceRadiance = GraphBuilder.CreateTexture(TraceRadianceDesc, TEXT("Lumen.ScreenProbeGather.TraceRadiance"));
    ScreenProbeParameters.RWTraceRadiance = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.TraceRadiance));

    FRDGTextureDesc TraceHitDesc(FRDGTextureDesc::Create2D(ScreenProbeTraceBufferSize, PF_R32_UINT, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV));
    ScreenProbeParameters.TraceHit = GraphBuilder.CreateTexture(TraceHitDesc, TEXT("Lumen.ScreenProbeGather.TraceHit"));
    ScreenProbeParameters.RWTraceHit = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(ScreenProbeParameters.TraceHit));

    // 스크린 스페이스의 .
    TraceScreenProbes(
        GraphBuilder, 
        Scene,
        View, 
        GLumenGatherCvars.TraceMeshSDFs != 0 && Lumen::UseMeshSDFTracing(),
        SceneTextures.UniformBuffer,
        PrevSceneColorMip,
        LightingChannelsTexture,
        TracingInputs,
        RadianceCacheParameters,
        ScreenProbeParameters,
        MeshSDFGridParameters);
    
    FScreenProbeGatherParameters GatherParameters;
    // 필터링 스크린 스페이스.
    FilterScreenProbes(GraphBuilder, View, ScreenProbeParameters, GatherParameters);

    FScreenSpaceBentNormalParameters ScreenSpaceBentNormalParameters;
    ScreenSpaceBentNormalParameters.UseScreenBentNormal = 0;
    ScreenSpaceBentNormalParameters.ScreenBentNormal = SystemTextures.Black;
    ScreenSpaceBentNormalParameters.ScreenDiffuseLighting = SystemTextures.Black;

    // 계산/산출(Calculate)스크린 스페이스의 노멀(Normal).
    if (LumenScreenProbeGather::UseScreenSpaceBentNormal())
    {
        ScreenSpaceBentNormalParameters = ComputeScreenSpaceBentNormal(GraphBuilder, Scene, View, SceneTextures, LightingChannelsTexture, ScreenProbeParameters);
    }

    FRDGTextureDesc DiffuseIndirectDesc = FRDGTextureDesc::Create2D(SceneTextures.Config.Extent, PF_FloatRGBA, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV);
    FRDGTextureRef DiffuseIndirect = GraphBuilder.CreateTexture(DiffuseIndirectDesc, TEXT("Lumen.ScreenProbeGather.DiffuseIndirect"));

    FRDGTextureDesc RoughSpecularIndirectDesc = FRDGTextureDesc::Create2D(SceneTextures.Config.Extent, PF_FloatRGB, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV);
    FRDGTextureRef RoughSpecularIndirect = GraphBuilder.CreateTexture(RoughSpecularIndirectDesc, TEXT("Lumen.ScreenProbeGather.RoughSpecularIndirect"));

    {
        FScreenProbeIndirectCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FScreenProbeIndirectCS::FParameters>();
        PassParameters->RWDiffuseIndirect = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(DiffuseIndirect));
        PassParameters->RWRoughSpecularIndirect = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(RoughSpecularIndirect));
        PassParameters->GatherParameters = GatherParameters;
        PassParameters->ScreenProbeParameters = ScreenProbeParameters;
        PassParameters->View = View.ViewUniformBuffer;
        PassParameters->SceneTexturesStruct = SceneTextures.UniformBuffer;
        PassParameters->FullResolutionJitterWidth = GLumenScreenProbeFullResolutionJitterWidth;
        extern float GLumenReflectionMaxRoughnessToTrace;
        extern float GLumenReflectionRoughnessFadeLength;
        PassParameters->MaxRoughnessToTrace = GLumenReflectionMaxRoughnessToTrace;
        PassParameters->RoughnessFadeLength = GLumenReflectionRoughnessFadeLength;
        PassParameters->ScreenSpaceBentNormalParameters = ScreenSpaceBentNormalParameters;

        FScreenProbeIndirectCS::FPermutationDomain PermutationVector;
        PermutationVector.Set< FScreenProbeIndirectCS::FDiffuseIntegralMethod >(LumenScreenProbeGather::GetDiffuseIntegralMethod());
        auto ComputeShader = View.ShaderMap->GetShader<FScreenProbeIndirectCS>(PermutationVector);

        // 계산/산출(Calculate)스크린 스페이스의 .
        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("ComputeIndirect %ux%u", View.ViewRect.Width(), View.ViewRect.Height()),
            ComputeShader,
            PassParameters,
            FComputeShaderUtils::GetGroupCount(View.ViewRect.Size(), FScreenProbeIndirectCS::GetGroupSize()));
    }

    FSSDSignalTextures DenoiserOutputs;
    DenoiserOutputs.Textures[0] = DiffuseIndirect;
    DenoiserOutputs.Textures[1] = RoughSpecularIndirect;
    bLumenUseDenoiserComposite = false;

    // 스크린 스페이스의 필터링 .
    if (GLumenScreenProbeTemporalFilter)
    {
        if (GLumenScreenProbeUseHistoryNeighborhoodClamp)
        {
            FRDGTextureRef CompressedDepthTexture;
            FRDGTextureRef CompressedShadingModelTexture;
            {
                FRDGTextureDesc Desc = FRDGTextureDesc::Create2D(
                    SceneTextures.Depth.Resolve->Desc.Extent,
                    PF_R16F,
                    FClearValueBinding::None,                    
                    /* InTargetableFlags = */ TexCreate_ShaderResource | TexCreate_UAV);

                CompressedDepthTexture = GraphBuilder.CreateTexture(Desc, TEXT("Lumen.ScreenProbeGather.CompressedDepth"));

                Desc.Format = PF_R8_UINT;
                CompressedShadingModelTexture = GraphBuilder.CreateTexture(Desc, TEXT("Lumen.ScreenProbeGather.CompressedShadingModelID"));
            }

            {
                FGenerateCompressedGBuffer::FParameters* PassParameters = GraphBuilder.AllocParameters<FGenerateCompressedGBuffer::FParameters>();
                PassParameters->RWCompressedDepthBufferOutput = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(CompressedDepthTexture));
                PassParameters->RWCompressedShadingModelOutput = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(CompressedShadingModelTexture));
                PassParameters->View = View.ViewUniformBuffer;
                PassParameters->SceneTextures = SceneTextureParameters;

                auto ComputeShader = View.ShaderMap->GetShader<FGenerateCompressedGBuffer>(0);

                FComputeShaderUtils::AddPass(
                    GraphBuilder,
                    RDG_EVENT_NAME("GenerateCompressedGBuffer"),
                    ComputeShader,
                    PassParameters,
                    FComputeShaderUtils::GetGroupCount(View.ViewRect.Size(), FGenerateCompressedGBuffer::GetGroupSize()));
            }

            FSSDSignalTextures ScreenSpaceDenoiserInputs;
            ScreenSpaceDenoiserInputs.Textures[0] = DiffuseIndirect;
            ScreenSpaceDenoiserInputs.Textures[1] = RoughSpecularIndirect;

            DenoiserOutputs = IScreenSpaceDenoiser::DenoiseIndirectProbeHierarchy(
                GraphBuilder,
                View, 
                PreviousViewInfos,
                SceneTextureParameters,
                ScreenSpaceDenoiserInputs,
                CompressedDepthTexture,
                CompressedShadingModelTexture);

            bLumenUseDenoiserComposite = true;
        }
        else
        {
            UpdateHistoryScreenProbeGather(
                GraphBuilder,
                View,
                SceneTextures,
                DiffuseIndirect,
                RoughSpecularIndirect);

            DenoiserOutputs.Textures[0] = DiffuseIndirect;
            DenoiserOutputs.Textures[1] = RoughSpecularIndirect;
        }
    }

    return DenoiserOutputs;
}

소스 코드와 RenderDoc 프레임 캡처 데이터를 결합하면 화면 공간의 프로브 수집 단계가 매우 복잡하다는 것을 알 수 있습니다. 기존 프로세스의 주요 단계는 전역 및 적응형 위치 조정, BRDF 계산, 발광 캐시 렌더링, 조명 PDF 계산, 샘플링 광선 생성, 화면 공간에서 프로브 추적, 추적 결과 압축, 복셀 복셀 추적, 추적 결과 결합, 스트립으로 수집된 발광 필터링, 환경 노멀 처리, 간접광 계산 및 기록 데이터 업데이트입니다.

위의 단계에는 너무 많은 단계가 포함되므로 프레임 캡처 데이터를 기반으로 분석을 위한 몇 가지 중요한 단계만 선택할 수 있습니다.

  • 래디언스캐시

RadianceCache 역시 일련의 매우 복잡한 프로세스입니다. 청소, 마킹, 업데이트, 프로브 할당, 그리기 매개변수 설정, 프로브 추적, 프로브 휘도 필터링 등의 단계를 연속적으로 거쳤습니다.

RadianceCache에서 가장 중요한 것은 화면 공간을 추적하는 프로브입니다. 입력 데이터에는 전역 거리 필드, VoxelLighting 및 기타 텍스처가 포함됩니다.

출력은 4096x4096 방사성 프로브 아틀라스 및 깊이입니다.

TraceFromProbes의 프로브 아틀라스 출력(부분 확대).

사용된 Compute Shader 코드는 다음과 같습니다.

cpp
// Engine\Shaders\Private\Lumen\LumenRadianceCache.usf

groupshared float3 SharedTraceRadiance[THREADGROUP_SIZE][THREADGROUP_SIZE];
groupshared float SharedTraceHitDistance[THREADGROUP_SIZE][THREADGROUP_SIZE];

[numthreads(THREADGROUP_SIZE, THREADGROUP_SIZE, 1)]
void TraceFromProbesCS(
    uint3 GroupId : SV_GroupID,
    uint2 GroupThreadId : SV_GroupThreadID)
{
    uint TraceTileIndex = GroupId.y * TRACE_TILE_GROUP_STRIDE + GroupId.x;

    if (TraceTileIndex < ProbeTraceTileAllocator[0])
    {
        uint2 TraceTileCoord;
        uint TraceTileLevel;
        uint ProbeTraceIndex;
        // 페치/가져오기(Fetch)의 정보
        UnpackTraceTileInfo(ProbeTraceTileData[TraceTileIndex], TraceTileCoord, TraceTileLevel, ProbeTraceIndex);

        uint TraceResolution = (RadianceProbeResolution / 2) << TraceTileLevel;
        // 좌표
        uint2 ProbeTexelCoord = TraceTileCoord * THREADGROUP_SIZE + GroupThreadId.xy;

        float3 ProbeWorldCenter;
        uint ClipmapIndex;
        uint ProbeIndex;
        // 페치/가져오기(Fetch)의 데이터 .
        GetProbeTraceData(ProbeTraceIndex, ProbeWorldCenter, ClipmapIndex, ProbeIndex);

        if (all(ProbeTexelCoord < TraceResolution))
        {
            float2 ProbeTexelCenter = float2(0.5, 0.5);
            float2 ProbeUV = (ProbeTexelCoord + ProbeTexelCenter) / float(TraceResolution);
            float3 WorldConeDirection = OctahedralMapToDirection(ProbeUV);

            float FinalMinTraceDistance = max(MinTraceDistance, GetRadianceProbeTMin(ClipmapIndex));
            float FinalMaxTraceDistance = MaxTraceDistance;
            float EffectiveStepFactor = StepFactor;

            // 의 분포 ,는 의 .
            float ConeHalfAngle = acosFast(1.0f - 1.0f / (float)(TraceResolution * TraceResolution));

            // 설정(Set)입력데이터 .
            FConeTraceInput TraceInput;
            TraceInput.Setup(
                ProbeWorldCenter, WorldConeDirection,
                ConeHalfAngle, MinSampleRadius,
                FinalMinTraceDistance, FinalMaxTraceDistance,
                EffectiveStepFactor);
            TraceInput.VoxelStepFactor = VoxelStepFactor;

            bool bContinueCardTracing = false;

            TraceInput.VoxelTraceStartDistance = CalculateVoxelTraceStartDistance(FinalMinTraceDistance, FinalMaxTraceDistance, MaxMeshSDFTraceDistance, bContinueCardTracing);

            // 로 실행(Execute).
            FConeTraceResult TraceResult = TraceForProbeTexel(TraceInput);

            // 저장(Store)의 라이팅 결과 .
            SharedTraceRadiance[GroupThreadId.y][GroupThreadId.x] = TraceResult.Lighting;

            // 저장(Store)의 뎁스 .
            #if RADIANCE_CACHE_STORE_DEPTHS
                SharedTraceHitDistance[GroupThreadId.y][GroupThreadId.x] = TraceResult.OpaqueHitDistance;
            #endif
        }

        GroupMemoryBarrierWithGroupSync();

        uint2 ProbeAtlasBaseCoord = RadianceProbeResolution * uint2(ProbeIndex % ProbeAtlasResolutionInProbes.x, ProbeIndex / ProbeAtlasResolutionInProbes.x);

        // 저장(Store)라이팅 결과 및 교차점(Intersection)의 거리.
        if (TraceResolution < RadianceProbeResolution)
        {
            uint UpsampleFactor = RadianceProbeResolution / TraceResolution;
            ProbeAtlasBaseCoord += (THREADGROUP_SIZE * TraceTileCoord + GroupThreadId.xy) * UpsampleFactor;

            float3 Lighting = SharedTraceRadiance[GroupThreadId.y][GroupThreadId.x];

            for (uint Y = 0; Y < UpsampleFactor; Y++)
            {
                for (uint X = 0; X < UpsampleFactor; X++)
                {
                    RWRadianceProbeAtlasTexture[ProbeAtlasBaseCoord + uint2(X, Y)] = Lighting;
                }
            }

            #if RADIANCE_CACHE_STORE_DEPTHS
                float HitDistance = min(SharedTraceHitDistance[GroupThreadId.y][GroupThreadId.x], MaxHalfFloat);

                for (uint Y = 0; Y < UpsampleFactor; Y++)
                {
                    for (uint X = 0; X < UpsampleFactor; X++)
                    {
                        RWDepthProbeAtlasTexture[ProbeAtlasBaseCoord + uint2(X, Y)] = HitDistance;
                    }
                }
            #endif
        }
        else
        {
            uint DownsampleFactor = TraceResolution / RadianceProbeResolution;
            uint WriteTileSize = THREADGROUP_SIZE / DownsampleFactor;

            if (all(GroupThreadId.xy < WriteTileSize))
            {
                float3 Lighting = 0;

                for (uint Y = 0; Y < DownsampleFactor; Y++)
                {
                    for (uint X = 0; X < DownsampleFactor; X++)
                    {
                        Lighting += SharedTraceRadiance[GroupThreadId.y * DownsampleFactor + Y][GroupThreadId.x * DownsampleFactor + X];
                    }
                }

                ProbeAtlasBaseCoord += WriteTileSize * TraceTileCoord + GroupThreadId.xy;
                RWRadianceProbeAtlasTexture[ProbeAtlasBaseCoord] = Lighting / (float)(DownsampleFactor * DownsampleFactor);

                #if RADIANCE_CACHE_STORE_DEPTHS
                    float HitDistance = MaxHalfFloat;

                    for (uint Y = 0; Y < DownsampleFactor; Y++)
                    {
                        for (uint X = 0; X < DownsampleFactor; X++)
                        {
                            HitDistance = min(HitDistance, SharedTraceHitDistance[GroupThreadId.y * DownsampleFactor + Y][GroupThreadId.x * DownsampleFactor + X]);
                        }
                    }

                    RWDepthProbeAtlasTexture[ProbeAtlasBaseCoord] = HitDistance;
                #endif
            }
        }
    }
}

이제 TraceForProbeTexel 분석 프로브 텍셀의 추적 스택을 입력합니다.

hlsl
FConeTraceResult TraceForProbeTexel(FConeTraceInput TraceInput)
{
    // 결과 구조체 .
    FConeTraceResult TraceResult;
    TraceResult = (FConeTraceResult)0;
    TraceResult.Lighting = 0.0;
    TraceResult.Transparency = 1.0;
    TraceResult.OpaqueHitDistance = TraceInput.MaxTraceDistance;

    // Lumen의 , .
    ConeTraceLumenSceneVoxels(TraceInput, TraceResult);

    // 거리의 .
#if TRACE_DISTANT_SCENE
    if (TraceResult.Transparency > .01f)
    {
        FConeTraceResult DistantTraceResult;
        // Lumen, .
        ConeTraceLumenDistantScene(TraceInput, DistantTraceResult);
        TraceResult.Lighting += DistantTraceResult.Lighting * TraceResult.Transparency;
        TraceResult.Transparency *= DistantTraceResult.Transparency;
    }
#endif

    // 스카이라이트(Skylight)처리(Process).
#if ENABLE_DYNAMIC_SKY_LIGHT
    if (ReflectionStruct.SkyLightParameters.y > 0)
    {
        float SkyAverageBrightness = 1.0f;
        float Roughness = TanConeAngleToRoughness(tan(TraceInput.ConeAngle));

        TraceResult.Lighting = TraceResult.Lighting + GetSkyLightReflection(TraceInput.ConeDirection, Roughness, SkyAverageBrightness) * TraceResult.Transparency;
    }
#endif

    return TraceResult;
}

// Lumen의
void ConeTraceLumenSceneVoxels(
    FConeTraceInput TraceInput,
    inout FConeTraceResult OutResult)
{
#if SCENE_TRACE_VOXELS
    if (TraceInput.VoxelTraceStartDistance < TraceInput.MaxTraceDistance)
    {
        FConeTraceInput VoxelTraceInput = TraceInput;
        VoxelTraceInput.MinTraceDistance = TraceInput.VoxelTraceStartDistance;
        FConeTraceResult VoxelTraceResult;
        // , .
        ConeTraceVoxels(VoxelTraceInput, VoxelTraceResult);

        // 적용 반투명(Translucent).
        #if !VISIBILITY_ONLY_TRACE
            OutResult.Lighting += VoxelTraceResult.Lighting * OutResult.Transparency;
        #endif
        OutResult.Transparency *= VoxelTraceResult.Transparency;
        OutResult.NumSteps += VoxelTraceResult.NumSteps;
        OutResult.OpaqueHitDistance = min(OutResult.OpaqueHitDistance, VoxelTraceResult.OpaqueHitDistance);
    }
#endif
}

// Lumen.
void ConeTraceLumenDistantScene(
    FConeTraceInput TraceInput,
    inout FConeTraceResult OutResult)
{
    float3 debug = 0;
    TraceInput.MaxTraceDistance = LumenCardScene.DistantSceneMaxTraceDistance;
    TraceInput.bBlackOutSteepIntersections = true;

    FCardTraceBlendState CardTraceBlendState;
    CardTraceBlendState.Initialize(TraceInput.MaxTraceDistance);

    if (LumenCardScene.NumDistantCards > 0)
    {
        // 로부터 클리핑/클램핑(Clipping)페치/가져오기(Fetch)거리.
        if (NumClipmapLevels > 0)
        {
            float3 VoxelLightingCenter = ClipmapWorldCenter[NumClipmapLevels - 1].xyz;
            float3 VoxelLightingExtent = ClipmapWorldSamplingExtent[NumClipmapLevels - 1].xyz;

            float3 RayEnd = TraceInput.ConeOrigin + TraceInput.ConeDirection * TraceInput.MaxTraceDistance;
            float2 IntersectionTimes = LineBoxIntersect(TraceInput.ConeOrigin, RayEnd, VoxelLightingCenter - VoxelLightingExtent, VoxelLightingCenter + VoxelLightingExtent);

            // If we are starting inside the voxel clipmaps, move the start of the trace past the voxel clipmaps
            if (IntersectionTimes.x < IntersectionTimes.y && IntersectionTimes.x < .001f)
            {
                TraceInput.MinTraceDistance = IntersectionTimes.y * TraceInput.MaxTraceDistance;
            }
        }

        float TraceEndDistance = TraceInput.MinTraceDistance;

        {
            uint ListIndex = 0;
            uint CardIndex = LumenCardScene.DistantCardIndices[ListIndex];

            // 개 Lumen, .
            ConeTraceSingleLumenCard(
                TraceInput,
                CardIndex,
                debug,
                TraceEndDistance,
                CardTraceBlendState);
        }
    }

    OutResult = (FConeTraceResult)0;

    // 저장(Store)결과 .
    #if !VISIBILITY_ONLY_TRACE
        OutResult.Lighting = CardTraceBlendState.GetFinalLighting();
    #endif
    OutResult.Transparency = CardTraceBlendState.GetTransparency();
    OutResult.NumSteps = CardTraceBlendState.NumSteps;
    OutResult.NumOverlaps = CardTraceBlendState.NumOverlaps;
    OutResult.OpaqueHitDistance = CardTraceBlendState.OpaqueHitDistance;
    OutResult.Debug = debug;
}

// 개 Lumen
void ConeTraceSingleLumenCard(
    FConeTraceInput TraceInput,
    uint CardIndex,
    inout float3 Debug,
    inout float OutTraceEndDistance,
    inout FCardTraceBlendState CardTraceBlendState)
{
    // 페치/가져오기(Fetch)데이터 .
    FLumenCardData LumenCardData = GetLumenCardData(CardIndex);

    // 계산/산출(Calculate)로컬 의 데이터 .
    float3 LocalConeOrigin = mul(TraceInput.ConeOrigin - LumenCardData.Origin, LumenCardData.WorldToLocalRotation);
    float3 LocalConeDirection = mul(TraceInput.ConeDirection, LumenCardData.WorldToLocalRotation);
    float3 LocalTraceEnd = LocalConeOrigin + LocalConeDirection * TraceInput.MaxTraceDistance;

    // 교차(Intersection)범위 .
    float2 IntersectionRange = LineBoxIntersect(LocalConeOrigin, LocalTraceEnd, -LumenCardData.LocalExtent, LumenCardData.LocalExtent);
    IntersectionRange.x = max(IntersectionRange.x, TraceInput.MinTraceDistance / TraceInput.MaxTraceDistance);
    OutTraceEndDistance = IntersectionRange.y * TraceInput.MaxTraceDistance;

    if (IntersectionRange.y > IntersectionRange.x
        && LumenCardData.bVisible)
    {
        {
            // 블렌딩(Blending)상태 .
            FCardTraceBlendState ConeStepBlendState;
            ConeStepBlendState.Initialize(TraceInput.MaxTraceDistance);

            float StepTime = IntersectionRange.x * TraceInput.MaxTraceDistance;
            float3 SamplePosition = LocalConeOrigin + StepTime * LocalConeDirection;
            float TraceEndDistance = IntersectionRange.y * TraceInput.MaxTraceDistance;

            float IntersectionLength = (IntersectionRange.y - IntersectionRange.x) * TraceInput.MaxTraceDistance;
            float MinStepSize = IntersectionLength / (float)LumenCardScene.MaxConeSteps;

            float PreviousStepTime = StepTime;
            float3 PreviousSamplePosition = SamplePosition;
            // Magic value to prevent linear intersection approximation on first step
            float PreviousHeightfieldZ = -2;

            bool bClampedToEnd = false;
            bool bFoundSurface = false;
            bool bRayAboveSurface = false;
            float IntersectionStepTime = 0;
            float2 IntersectionSamplePositionXY = SamplePosition.xy;
            float IntersectionSlope = 0;

            uint NumStepsPerLoop = 4; // 회 루프 샘플링4회 .
            for (uint StepIndex = 0; StepIndex < LumenCardScene.MaxConeSteps && StepTime < TraceEndDistance; StepIndex += NumStepsPerLoop)
            {
                float SampleRadius = max(TraceInput.ConeStartRadius + TraceInput.TanConeAngle * StepTime, TraceInput.MinSampleRadius);
                float StepSize = max(SampleRadius * TraceInput.StepFactor, MinStepSize);
                float TraceClampDistance = TraceEndDistance - StepSize * .0001f;

                float DepthMip;
                float2 DepthValidRegionScale;
                CalculateMip(SampleRadius, LumenCardData, LumenCardData.LocalExtent, LumenCardData.MaxMip, DepthMip, DepthValidRegionScale);

                // 4개 샘플링위치 .
                float3 SamplePosition1 = LocalConeOrigin + min(StepTime + 0 * StepSize, TraceClampDistance) * LocalConeDirection;
                float3 SamplePosition2 = LocalConeOrigin + min(StepTime + 1 * StepSize, TraceClampDistance) * LocalConeDirection;
                float3 SamplePosition3 = LocalConeOrigin + min(StepTime + 2 * StepSize, TraceClampDistance) * LocalConeDirection;
                float3 SamplePosition4 = LocalConeOrigin + min(StepTime + 3 * StepSize, TraceClampDistance) * LocalConeDirection;

                // 4개 뎁스 UV.
                float2 DepthAtlasUV1 = CalculateAtlasUV(SamplePosition1.xy, DepthValidRegionScale, LumenCardData);
                float2 DepthAtlasUV2 = CalculateAtlasUV(SamplePosition2.xy, DepthValidRegionScale, LumenCardData);
                float2 DepthAtlasUV3 = CalculateAtlasUV(SamplePosition3.xy, DepthValidRegionScale, LumenCardData);
                float2 DepthAtlasUV4 = CalculateAtlasUV(SamplePosition4.xy, DepthValidRegionScale, LumenCardData);

                // 4개 뎁스 .
                float Depth1 = Texture2DSampleLevel(DepthAtlas, TRACING_ATLAS_SAMPLER, DepthAtlasUV1, DepthMip).x;
                float Depth2 = Texture2DSampleLevel(DepthAtlas, TRACING_ATLAS_SAMPLER, DepthAtlasUV2, DepthMip).x;
                float Depth3 = Texture2DSampleLevel(DepthAtlas, TRACING_ATLAS_SAMPLER, DepthAtlasUV3, DepthMip).x;
                float Depth4 = Texture2DSampleLevel(DepthAtlas, TRACING_ATLAS_SAMPLER, DepthAtlasUV4, DepthMip).x;

                // 4개 Z.
                float HeightfieldZ1 = LumenCardData.LocalExtent.z - Depth1 * 2 * LumenCardData.LocalExtent.z;
                float HeightfieldZ2 = LumenCardData.LocalExtent.z - Depth2 * 2 * LumenCardData.LocalExtent.z;
                float HeightfieldZ3 = LumenCardData.LocalExtent.z - Depth3 * 2 * LumenCardData.LocalExtent.z;
                float HeightfieldZ4 = LumenCardData.LocalExtent.z - Depth4 * 2 * LumenCardData.LocalExtent.z;

                ConeStepBlendState.RegisterStep(NumStepsPerLoop);

                // 는 교차(Intersection).
                bool4 HeightfieldHit = bool4(
                    SamplePosition1.z < HeightfieldZ1,
                    SamplePosition2.z < HeightfieldZ2,
                    SamplePosition3.z < HeightfieldZ3,
                    SamplePosition4.z < HeightfieldZ4);

                bool bRayBelowHeightfield = any(HeightfieldHit);
                bool bRayWasAboveSurface = bRayAboveSurface;

                if (!bRayBelowHeightfield)
                {
                    bRayAboveSurface = true;
                }

                // 로부터 로써 의 필수: 까지 로써 히트(Hit)
                if (bRayBelowHeightfield && bRayWasAboveSurface)
                {
                    float HeightfieldZ;
                    if (HeightfieldHit.x)
                    {
                        SamplePosition = SamplePosition1;
                        HeightfieldZ = HeightfieldZ1;
                        StepTime = StepTime + 0 * StepSize;
                    }
                    else if (HeightfieldHit.y)
                    {
                        PreviousSamplePosition = SamplePosition1;
                        PreviousHeightfieldZ = HeightfieldZ1;
                        PreviousStepTime = StepTime + 0 * StepSize;

                        SamplePosition = SamplePosition2;
                        HeightfieldZ = HeightfieldZ2;
                        StepTime = StepTime + 1 * StepSize;
                    }
                    else if (HeightfieldHit.z)
                    {
                        PreviousSamplePosition = SamplePosition2;
                        PreviousHeightfieldZ = HeightfieldZ2;
                        PreviousStepTime = StepTime + 1 * StepSize;

                        SamplePosition = SamplePosition3;
                        HeightfieldZ = HeightfieldZ3;
                        StepTime = StepTime + 2 * StepSize;
                    }
                    else
                    {
                        PreviousSamplePosition = SamplePosition3;
                        PreviousHeightfieldZ = HeightfieldZ3;
                        PreviousStepTime = StepTime + 2 * StepSize;

                        SamplePosition = SamplePosition4;
                        HeightfieldZ = HeightfieldZ4;
                        StepTime = StepTime + 3 * StepSize;
                    }

                    StepTime = min(StepTime, TraceClampDistance);

                    if (PreviousHeightfieldZ != -2)
                    {
                        // x의 교차점(Intersection).
                        IntersectionStepTime = PreviousStepTime + ((PreviousSamplePosition.z - PreviousHeightfieldZ) * (StepTime - PreviousStepTime)) / (HeightfieldZ - PreviousHeightfieldZ + PreviousSamplePosition.z - SamplePosition.z);

                        float2 LocalPositionSlopeXY = (SamplePosition.xy - PreviousSamplePosition.xy) / (StepTime - PreviousStepTime);
                        IntersectionSamplePositionXY = LocalPositionSlopeXY * (IntersectionStepTime - PreviousStepTime) + PreviousSamplePosition.xy;

                        IntersectionSlope = abs(PreviousHeightfieldZ - HeightfieldZ) / max(length(PreviousSamplePosition.xy - SamplePosition.xy), .0001f);

                        PreviousHeightfieldZ = -2;
                        // 까지 테이블 .
                        bFoundSurface = true;
                    }
                    break;
                }

                PreviousStepTime = StepTime + 3 * StepSize;
                PreviousSamplePosition = SamplePosition4;
                PreviousHeightfieldZ = HeightfieldZ4;
                StepTime += 4 * StepSize;

                if (StepTime >= TraceEndDistance && !bClampedToEnd)
                {
                    bClampedToEnd = true;
                    // Stop the last step just before the intersection end, since the linear approximation needs to step past the surface to detect a hit, without terminating the loop
                    StepTime = TraceClampDistance;
                }
            }

            // 만약 까지 테이블 포인트 .
            if (bFoundSurface)
            {
                float IntersectionSampleRadius = TraceInput.ConeStartRadius + TraceInput.TanConeAngle * IntersectionStepTime;

                float MaxMip;
                float2 ValidRegionScale;
                CalculateMip(IntersectionSampleRadius, LumenCardData, LumenCardData.LocalExtent, LumenCardData.MaxMip, MaxMip, ValidRegionScale);

                float2 IntersectionAtlasUV = CalculateAtlasUV(IntersectionSamplePositionXY, ValidRegionScale, LumenCardData);

                float DistanceToSurface = 0;
                float ConeIntersectSurface = saturate(DistanceToSurface / IntersectionSampleRadius);
                float ConeVisibility = ConeIntersectSurface;

                float MaxDistanceFade = 1;

                ConeStepBlendState.RegisterOpaqueHit(IntersectionStepTime);
                OutTraceEndDistance = IntersectionStepTime;

                float Opacity = Texture2DSampleLevel(OpacityAtlas, TRACING_ATLAS_SAMPLER, IntersectionAtlasUV, MaxMip).x;
                float ConeOcclusion = (1.0f - ConeVisibility) * Opacity * MaxDistanceFade;

                #if VISIBILITY_ONLY_TRACE
                    float3 StepLighting = 0;
                #else
                    float3 StepLighting = Texture2DSampleLevel(FinalLightingAtlas, TRACING_ATLAS_SAMPLER, IntersectionAtlasUV, MaxMip).rgb;
                #endif
            
                if (TraceInput.bBlackOutSteepIntersections)
                {
                    // 의 ,。
                    float SlopeFade = 1 - saturate((IntersectionSlope - 5) / 1.0f);
                    StepLighting = lerp(0, StepLighting, SlopeFade);
                    ConeOcclusion = lerp(0, ConeOcclusion, SlopeFade);
                }

                ConeStepBlendState.AddLighting(StepLighting, ConeOcclusion, IntersectionStepTime);
            }

            CardTraceBlendState.AddCardTrace(ConeStepBlendState);
        }
    }
}

위에서 볼 수 있듯이 RadianceCache 단계는 복잡한 렌더링 과정을 거친다. TraceFromProbes만으로는 원뿔 추적 Voxel 조명 필드와 장면에서 멀리 떨어진 카드를 연속적으로 고려합니다. 마지막으로, 스카이라이트의 영향도 고려해야 합니다.

  • TraceScreenProbes

TraceScreenProbes에는 추적 화면 프로브, 그리드 거리 필드, 복셀 조명 등이 포함됩니다. 구체적인 코드는 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenScreenProbeTracing.cpp

void TraceScreenProbes(
    FRDGBuilder& GraphBuilder, 
    const FScene* Scene,
    const FViewInfo& View, 
    bool bTraceMeshSDFs,
    TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTexturesUniformBuffer,
    const ScreenSpaceRayTracing::FPrevSceneColorMip& PrevSceneColor,
    FRDGTextureRef LightingChannelsTexture,
    const FLumenCardTracingInputs& TracingInputs,
    const LumenRadianceCache::FRadianceCacheInterpolationParameters& RadianceCacheParameters,
    FScreenProbeParameters& ScreenProbeParameters,
    FLumenMeshSDFGridParameters& MeshSDFGridParameters)
{
    const FSceneTextureParameters SceneTextures = GetSceneTextureParameters(GraphBuilder, SceneTexturesUniformBuffer);

    // .
    {
        FClearTracesCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FClearTracesCS::FParameters>();
        PassParameters->ScreenProbeParameters = ScreenProbeParameters;

        auto ComputeShader = View.ShaderMap->GetShader<FClearTracesCS>(0);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("ClearTraces %ux%u", ScreenProbeParameters.ScreenProbeTracingOctahedronResolution, ScreenProbeParameters.ScreenProbeTracingOctahedronResolution),
            ComputeShader,
            PassParameters,
            ScreenProbeParameters.ProbeIndirectArgs,
            (uint32)EScreenProbeIndirectArgs::ThreadPerTrace * sizeof(FRHIDispatchIndirectParameters));
    }

    FLumenIndirectTracingParameters IndirectTracingParameters;
    SetupLumenDiffuseTracingParameters(IndirectTracingParameters);

    const bool bTraceScreen = View.PrevViewInfo.ScreenSpaceRayTracingInput.IsValid() 
        && GLumenScreenProbeGatherScreenTraces != 0
        && !View.Family->EngineShowFlags.VisualizeLumenIndirectDiffuse;

    // 스크린 스페이스의 .
    if (bTraceScreen)
    {
        FScreenProbeTraceScreenTexturesCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FScreenProbeTraceScreenTexturesCS::FParameters>();

        ScreenSpaceRayTracing::SetupCommonScreenSpaceRayParameters(GraphBuilder, SceneTextures, PrevSceneColor, View, /* out */ &PassParameters->ScreenSpaceRayParameters);

        PassParameters->ScreenSpaceRayParameters.CommonDiffuseParameters.SceneTextures = SceneTextures;

        {
            const FVector2D HZBUvFactor(
                float(View.ViewRect.Width()) / float(2 * View.HZBMipmap0Size.X),
                float(View.ViewRect.Height()) / float(2 * View.HZBMipmap0Size.Y));

            const FVector4 ScreenPositionScaleBias = View.GetScreenPositionScaleBias(SceneTextures.SceneDepthTexture->Desc.Extent, View.ViewRect);
            const FVector2D HZBUVToScreenUVScale = FVector2D(1.0f / HZBUvFactor.X, 1.0f / HZBUvFactor.Y) * FVector2D(2.0f, -2.0f) * FVector2D(ScreenPositionScaleBias.X, ScreenPositionScaleBias.Y);
            const FVector2D HZBUVToScreenUVBias = FVector2D(-1.0f, 1.0f) * FVector2D(ScreenPositionScaleBias.X, ScreenPositionScaleBias.Y) + FVector2D(ScreenPositionScaleBias.W, ScreenPositionScaleBias.Z);
            PassParameters->HZBUVToScreenUVScaleBias = FVector4(HZBUVToScreenUVScale, HZBUVToScreenUVBias);
        }

        checkf(View.ClosestHZB, TEXT("Lumen screen tracing: ClosestHZB was not setup, should have been setup by FDeferredShadingSceneRenderer::RenderHzb"));
        PassParameters->ClosestHZBTexture = View.ClosestHZB;
        PassParameters->SceneDepthTexture = SceneTextures.SceneDepthTexture;
        PassParameters->LightingChannelsTexture = LightingChannelsTexture;
        PassParameters->HZBBaseTexelSize = FVector2D(1.0f / View.ClosestHZB->Desc.Extent.X, 1.0f / View.ClosestHZB->Desc.Extent.Y);
        PassParameters->MaxHierarchicalScreenTraceIterations = GLumenScreenProbeGatherHierarchicalScreenTracesMaxIterations;
        PassParameters->UncertainTraceRelativeDepthThreshold = GLumenScreenProbeGatherUncertainTraceRelativeDepthThreshold;
        PassParameters->NumThicknessStepsToDetermineCertainty = GLumenScreenProbeGatherNumThicknessStepsToDetermineCertainty;

        PassParameters->ScreenProbeParameters = ScreenProbeParameters;
        PassParameters->IndirectTracingParameters = IndirectTracingParameters;
        PassParameters->RadianceCacheParameters = RadianceCacheParameters;

        FScreenProbeTraceScreenTexturesCS::FPermutationDomain PermutationVector;
        PermutationVector.Set< FScreenProbeTraceScreenTexturesCS::FRadianceCache >(LumenScreenProbeGather::UseRadianceCache(View));
        PermutationVector.Set< FScreenProbeTraceScreenTexturesCS::FHierarchicalScreenTracing >(GLumenScreenProbeGatherHierarchicalScreenTraces != 0);
        PermutationVector.Set< FScreenProbeTraceScreenTexturesCS::FStructuredImportanceSampling >(LumenScreenProbeGather::UseImportanceSampling(View));
        auto ComputeShader = View.ShaderMap->GetShader<FScreenProbeTraceScreenTexturesCS>(PermutationVector);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("TraceScreen"),
            ComputeShader,
            PassParameters,
            ScreenProbeParameters.ProbeIndirectArgs,
            (uint32)EScreenProbeIndirectArgs::ThreadPerTrace * sizeof(FRHIDispatchIndirectParameters));
    }

    // 거리.
    if (bTraceMeshSDFs)
    {
        //
        if (Lumen::UseHardwareRayTracedScreenProbeGather())
        {
            FCompactedTraceParameters CompactedTraceParameters = CompactTraces(
                GraphBuilder,
                View,
                ScreenProbeParameters,
                WORLD_MAX,
                IndirectTracingParameters.MaxTraceDistance);

            RenderHardwareRayTracingScreenProbe(GraphBuilder,
                Scene,
                SceneTextures,
                ScreenProbeParameters,
                View,
                TracingInputs,
                IndirectTracingParameters,
                RadianceCacheParameters,
                CompactedTraceParameters);
        }
        //
        else
        {
            CullForCardTracing(
                GraphBuilder,
                Scene, View,
                TracingInputs,
                IndirectTracingParameters,
                /* out */ MeshSDFGridParameters);

            if (MeshSDFGridParameters.TracingParameters.DistanceFieldObjectBuffers.NumSceneObjects > 0)
            {
                FCompactedTraceParameters CompactedTraceParameters = CompactTraces(
                    GraphBuilder,
                    View,
                    ScreenProbeParameters,
                    IndirectTracingParameters.CardTraceEndDistanceFromCamera,
                    IndirectTracingParameters.MaxMeshSDFTraceDistance);

                {
                    FScreenProbeTraceMeshSDFsCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FScreenProbeTraceMeshSDFsCS::FParameters>();
                    GetLumenCardTracingParameters(View, TracingInputs, PassParameters->TracingParameters);
                    PassParameters->MeshSDFGridParameters = MeshSDFGridParameters;
                    PassParameters->ScreenProbeParameters = ScreenProbeParameters;
                    PassParameters->IndirectTracingParameters = IndirectTracingParameters;
                    PassParameters->SceneTexturesStruct = SceneTexturesUniformBuffer;
                    PassParameters->CompactedTraceParameters = CompactedTraceParameters;

                    FScreenProbeTraceMeshSDFsCS::FPermutationDomain PermutationVector;
                    PermutationVector.Set< FScreenProbeTraceMeshSDFsCS::FStructuredImportanceSampling >(LumenScreenProbeGather::UseImportanceSampling(View));
                    auto ComputeShader = View.ShaderMap->GetShader<FScreenProbeTraceMeshSDFsCS>(PermutationVector);

                    FComputeShaderUtils::AddPass(
                        GraphBuilder,
                        RDG_EVENT_NAME("TraceMeshSDFs"),
                        ComputeShader,
                        PassParameters,
                        CompactedTraceParameters.IndirectArgs,
                        0);
                }
            }
        }
    }

    // 압축/패킹(Packing)파라미터 .
    FCompactedTraceParameters CompactedTraceParameters = CompactTraces(
        GraphBuilder,
        View,
        ScreenProbeParameters,
        WORLD_MAX,
        // Make sure the shader runs on all misses to apply radiance cache + skylight
        IndirectTracingParameters.MaxTraceDistance + 1);

    // Voxel라이팅 .
    {
        FScreenProbeTraceVoxelsCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FScreenProbeTraceVoxelsCS::FParameters>();
        PassParameters->RadianceCacheParameters = RadianceCacheParameters;
        GetLumenCardTracingParameters(View, TracingInputs, PassParameters->TracingParameters);
        PassParameters->ScreenProbeParameters = ScreenProbeParameters;
        PassParameters->IndirectTracingParameters = IndirectTracingParameters;
        PassParameters->SceneTexturesStruct = SceneTexturesUniformBuffer;
        PassParameters->CompactedTraceParameters = CompactedTraceParameters;

        const bool bRadianceCache = LumenScreenProbeGather::UseRadianceCache(View);

        FScreenProbeTraceVoxelsCS::FPermutationDomain PermutationVector;
        PermutationVector.Set< FScreenProbeTraceVoxelsCS::FDynamicSkyLight >(Lumen::ShouldHandleSkyLight(Scene, *View.Family));
        PermutationVector.Set< FScreenProbeTraceVoxelsCS::FTraceDistantScene >(Scene->LumenSceneData->DistantCardIndices.Num() > 0);
        PermutationVector.Set< FScreenProbeTraceVoxelsCS::FRadianceCache >(bRadianceCache);
        PermutationVector.Set< FScreenProbeTraceVoxelsCS::FStructuredImportanceSampling >(LumenScreenProbeGather::UseImportanceSampling(View));
        auto ComputeShader = View.ShaderMap->GetShader<FScreenProbeTraceVoxelsCS>(PermutationVector);

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("TraceVoxels"),
            ComputeShader,
            PassParameters,
            CompactedTraceParameters.IndirectArgs,
            0);
    }

    if (GLumenScreenProbeGatherVisualizeTraces)
    {
        SetupVisualizeTraces(GraphBuilder, Scene, View, ScreenProbeParameters);
    }
}

먼저 프레임 캡처 데이터를 기반으로 TraceScreen을 분석합니다. 입력은 BlueNoise, Velocity, 깊이, 프로브 속도, 광선 정보, HZB, SSRReducedSceneColor 및 기타 텍스처입니다. 출력은 R11G11B10의 픽셀 형식을 사용하는 TraceRadiance 및 R32 TraceHit 텍스처입니다.

왼쪽: TraceRadiance, 오른쪽: TraceHit.

사용되는 Compute Shader는 다음과 같습니다.

hlsl
// Engine\Shaders\Private\Lumen\LumenScreenProbeTracing.usf

[numthreads(PROBE_THREADGROUP_SIZE_2D, PROBE_THREADGROUP_SIZE_2D, 1)]
void ScreenProbeTraceScreenTexturesCS(
    uint3 GroupId : SV_GroupID,
    uint3 DispatchThreadId : SV_DispatchThreadID,
    uint3 GroupThreadId : SV_GroupThreadID)
{
#define DEINTERLEAVED_SCREEN_TRACING 1
    // 계산/산출(Calculate)텍스처(Texture)좌표
#if DEINTERLEAVED_SCREEN_TRACING
    uint2 AtlasSizeInProbes = uint2(ScreenProbeAtlasViewSize.x, (GetNumScreenProbes() + ScreenProbeAtlasViewSize.x - 1) / ScreenProbeAtlasViewSize.x);
    uint2 ScreenProbeAtlasCoord = DispatchThreadId.xy % AtlasSizeInProbes;
    uint2 TraceTexelCoord = DispatchThreadId.xy / AtlasSizeInProbes;
#else
    uint2 ScreenProbeAtlasCoord = DispatchThreadId.xy / ScreenProbeTracingOctahedronResolution;
    uint2 TraceTexelCoord = DispatchThreadId.xy - ScreenProbeAtlasCoord * ScreenProbeTracingOctahedronResolution;
#endif

    uint ScreenProbeIndex = ScreenProbeAtlasCoord.y * ScreenProbeAtlasViewSize.x + ScreenProbeAtlasCoord.x;

    uint2 ScreenProbeScreenPosition = GetScreenProbeScreenPosition(ScreenProbeIndex);
    uint2 ScreenTileCoord = GetScreenTileCoord(ScreenProbeScreenPosition);

    if (ScreenProbeIndex < GetNumScreenProbes() && all(TraceTexelCoord < ScreenProbeTracingOctahedronResolution))
    {
        float2 ScreenUV = GetScreenUVFromScreenProbePosition(ScreenProbeScreenPosition);
        float SceneDepth = GetScreenProbeDepth(ScreenProbeAtlasCoord);

        if (SceneDepth > 0.0f)
        {
            float3 WorldPosition = GetWorldPositionFromScreenUV(ScreenUV, SceneDepth);

            float2 ProbeUV;
            float ConeHalfAngle;
            // 페치/가져오기(Fetch)의 UV.
            GetProbeTracingUV(ScreenProbeAtlasCoord, TraceTexelCoord, GetProbeTexelCenter(ScreenTileCoord), 1, ProbeUV, ConeHalfAngle);

            float3 WorldConeDirection = OctahedralMapToDirection(ProbeUV);

            float DepthThresholdScale = HasDistanceFieldRepresentation(ScreenUV) ? 1.0f : ScreenTraceNoFallbackThicknessScale;

            {
                float TraceDistance = MaxTraceDistance;
                bool bCoveredByRadianceCache = false;
                #if RADIANCE_CACHE
                    float ProbeOcclusionDistance = GetRadianceProbeOcclusionDistanceWithInterpolation(WorldPosition, WorldConeDirection, bCoveredByRadianceCache);
                    TraceDistance = min(TraceDistance, ProbeOcclusionDistance);
                #endif

#if HIERARCHICAL_SCREEN_TRACING // 레벨 단계

                bool bHit;
                bool bUncertain;
                float3 HitUVz;

                //
                TraceScreen(
                    WorldPosition + View.PreViewTranslation,
                    WorldConeDirection,
                    TraceDistance,
                    HZBUvFactorAndInvFactor,
                    MaxHierarchicalScreenTraceIterations, 
                    UncertainTraceRelativeDepthThreshold * DepthThresholdScale,
                    NumThicknessStepsToDetermineCertainty,
                    bHit,
                    bUncertain,
                    HitUVz);
                
                float Level = 1;
                bool bWriteDepthOnMiss = true;
#else // 레벨 단계
    
                uint NumSteps = 16;
                float StartMipLevel = 1.0f;
                float MaxScreenTraceFraction = .2f;

                // 거리,오직 의 내에서 의 품질 .
                float MaxWorldTraceDistance = SceneDepth * MaxScreenTraceFraction * 2.0 * GetTanHalfFieldOfView().x;
                TraceDistance = min(TraceDistance, MaxWorldTraceDistance);

                uint2 NoiseCoord = ScreenProbeAtlasCoord * ScreenProbeTracingOctahedronResolution + TraceTexelCoord;
                float StepOffset = InterleavedGradientNoise(NoiseCoord + 0.5f, 0);

                float RayRoughness = .2f;
                StepOffset = StepOffset - .9f;

                FSSRTCastingSettings CastSettings = CreateDefaultCastSettings();
                CastSettings.bStopWhenUncertain = true;

                bool bHit = false;
                float Level;
                float3 HitUVz;
                bool bRayWasClipped;

                // 초기화(Initialize)스크린 스페이스의 의 .
                FSSRTRay Ray = InitScreenSpaceRayFromWorldSpace(
                    WorldPosition + View.PreViewTranslation, WorldConeDirection,
                    /* WorldTMax = */ TraceDistance,
                    /* SceneDepth = */ SceneDepth,
                    /* SlopeCompareToleranceScale */ 2.0f * DepthThresholdScale,
                    /* bExtendRayToScreenBorder = */ false,
                    /* out */ bRayWasClipped);

                bool bUncertain;
                float3 DebugOutput;

                // 스크린 스페이스의 광선(Ray).
                CastScreenSpaceRay(
                    FurthestHZBTexture, FurthestHZBTextureSampler,
                    StartMipLevel,
                    CastSettings,
                    Ray, RayRoughness, NumSteps, StepOffset,
                    HZBUvFactorAndInvFactor, false,
                    /* out */ DebugOutput,
                    /* out */ HitUVz,
                    /* out */ Level,
                    /* out */ bHit,
                    /* out */ bUncertain);

                // CastScreenSpaceRay skips Mesh SDF tracing in a lot of places where it shouldn't, in particular missing thin occluders due to low NumSteps.  
                bool bWriteDepthOnMiss = !bUncertain;

#endif
                bHit = bHit && !bUncertain;

                uint2 TraceCoord = GetTraceBufferCoord(ScreenProbeAtlasCoord, TraceTexelCoord);
                bool bFastMoving = false;

                // 처리(Process)교차(Intersection)의 로직 .
                if (bHit)
                {
                    float2 ReducedColorUV = HitUVz.xy * ColorBufferScaleBias.xy + ColorBufferScaleBias.zw;
                    ReducedColorUV = min(ReducedColorUV, ReducedColorUVMax);

                    float3 Lighting = ColorTexture.SampleLevel(ColorTextureSampler, ReducedColorUV, Level).rgb;
                    
                    #if DEBUG_VISUALIZE_TRACE_TYPES
                        RWTraceRadiance[TraceCoord] = float3(.5f, 0, 0) * View.PreExposure;
                    #else
                        RWTraceRadiance[TraceCoord] = Lighting;
                    #endif

                    float3 HitWorldVelocity;
                    {
                        float2 HitScreenUV = HitUVz.xy;
                        float2 HitScreenPosition = (HitScreenUV.xy - View.ScreenPositionScaleBias.wz) / View.ScreenPositionScaleBias.xy;

                        float HitDeviceZ = HitUVz.z;
                        float HitSceneDepth = ConvertFromDeviceZ(HitUVz.z);
                        float3 HitHistoryScreenPosition = GetHistoryScreenPosition(HitScreenPosition, HitScreenUV, HitDeviceZ);

                        float3 HitTranslatedWorldPosition = mul(float4(HitScreenPosition * HitSceneDepth, HitSceneDepth, 1), View.ScreenToTranslatedWorld).xyz;
                        HitWorldVelocity = HitTranslatedWorldPosition - GetPrevTranslatedWorldPosition(HitHistoryScreenPosition);
                    }

                    float ProbeWorldSpeed = ScreenProbeWorldSpeed.Load(int3(ScreenProbeAtlasCoord, 0)).x;
                    float HitWorldSpeed = length(HitWorldVelocity);

                    bFastMoving = abs(ProbeWorldSpeed - HitWorldSpeed) / max(SceneDepth, 100.0f) > RelativeSpeedDifferenceToConsiderLightingMoving;
                }

                // 교차(Intersection) 또는 뎁스 이면 저장(Save)뎁스 .
                if (bHit || bWriteDepthOnMiss)
                {
                    float HitDistance = min(sqrt(ComputeRayHitSqrDistance(WorldPosition + View.PreViewTranslation, HitUVz)), MaxTraceDistance);
                    RWTraceHit[TraceCoord] = EncodeProbeRayDistance(HitDistance, bHit, bFastMoving);
                }
            }
        }
    }
}

위의 내용은 HIERARCHICAL_SCREEN_TRACING 여부에 따라 두 가지 다른 화면 추적 방법을 입력합니다. 프레임 캡처 데이터는 HIERARCHICAL_SCREEN_TRACING이 1임을 보여줍니다. 이는 TraceScreen에 들어가지만 CastScreenSpaceRay에는 들어가지 않는다는 의미입니다. TraceScreen을 분석해 보겠습니다.

hlsl
// Engine\Shaders\Private\Lumen\LumenScreenTracing.ush

// 순회(Traverse/Iterate)HZB스크린 스페이스, 그러나 비교 。
void TraceScreen(
    float3 RayTranslatedWorldOrigin, 
    float3 RayWorldDirection,
    float MaxWorldTraceDistance,
    float4 HZBUvFactorAndInvFactor,
    float MaxIterations,
    float UncertainTraceRelativeDepthThreshold,
    float NumThicknessStepsToDetermineCertainty,
    inout bool bHit,
    inout bool bUncertain,
    inout float3 OutScreenUV)
{
    // 계산/산출(Calculate)광선(Ray)포인트 의 UV.
    float3 RayStartScreenUV;
    {
        float4 RayStartClip = mul(float4(RayTranslatedWorldOrigin, 1.0f), View.TranslatedWorldToClip);
        float3 RayStartScreenPosition = RayStartClip.xyz / max(RayStartClip.w, 1.0f);
        RayStartScreenUV = float3((RayStartScreenPosition.xy * float2(0.5f, -0.5f) + 0.5f) * HZBUvFactorAndInvFactor.xy, RayStartScreenPosition.z);
    }
    
    // 계산/산출(Calculate)광선(Ray)포인트 의 UV.
    float3 RayEndScreenUV;
    {
        float3 ViewRayDirection = mul(float4(RayWorldDirection, 0.0), View.TranslatedWorldToView).xyz;
        float SceneDepth = mul(float4(RayTranslatedWorldOrigin, 1.0f), View.TranslatedWorldToView).z;
        // 광선(Ray)Z==0의 ,포인트 NDC 내에서 유효(Valid).
        float RayEndWorldDistance = ViewRayDirection.z < 0.0 ? min(-0.99f * SceneDepth / ViewRayDirection.z, MaxWorldTraceDistance) : MaxWorldTraceDistance;

        float3 RayWorldEnd = RayTranslatedWorldOrigin + RayWorldDirection * RayEndWorldDistance;
        float4 RayEndClip = mul(float4(RayWorldEnd, 1.0f), View.TranslatedWorldToClip);
        float3 RayEndScreenPosition = RayEndClip.xyz / RayEndClip.w;
        RayEndScreenUV = float3((RayEndScreenPosition.xy * float2(0.5f, -0.5f) + 0.5f) * HZBUvFactorAndInvFactor.xy, RayEndScreenPosition.z);

        float2 ScreenEdgeIntersections = LineBoxIntersect(RayStartScreenUV, RayEndScreenUV, float3(0, 0, 0), float3(HZBUvFactorAndInvFactor.xy, 1));

        // 계산/산출(Calculate)의 포인트 .
        RayEndScreenUV = RayStartScreenUV + (RayEndScreenUV - RayStartScreenUV) * ScreenEdgeIntersections.y;
    }

    float BaseMipLevel = HZB_TRACE_INCLUDE_FULL_RES_DEPTH ? -1 : 0;
    float MipLevel = BaseMipLevel;

    // 현재(Current)타일드(Tiled)히트(Hit),로써 차폐/오클루전 . 는 의 ,이므로 HZB mip 0는 2x2뎁스 의 ,HZB저장(Store)16포인트 내에서
    bool bStepOutOfCurrentTile = true;
    if (bStepOutOfCurrentTile)
    {
        float2 HZBTileSize = exp2(MipLevel) * HZBBaseTexelSize;
        float2 BiasedUV = RayStartScreenUV.xy;
        float3 HZBTileMin = float3(floor(BiasedUV.xy / HZBTileSize) * HZBTileSize, 0.0f);
        float3 HZBTileMax = float3(HZBTileMin.xy + HZBTileSize, 1);
        float2 TileIntersections = LineBoxIntersect(RayStartScreenUV, RayEndScreenUV, HZBTileMin, HZBTileMax);

        {
            float3 RayTileHit = RayStartScreenUV + (RayEndScreenUV - RayStartScreenUV) * TileIntersections.y;
            RayStartScreenUV = RayTileHit;
        }
    }

    bHit = false;
    bUncertain = false;

    float RayLength2D = length(RayEndScreenUV.xy - RayStartScreenUV.xy);
    float2 RayDirectionScreenUV = (RayEndScreenUV.xy - RayStartScreenUV.xy) / max(RayLength2D, .0001f);
    float3 RayScreenUV = RayStartScreenUV;
    float NumIterations = 0;
    
    // 순회(Traverse/Iterate)HZB.
    while (MipLevel >= BaseMipLevel && NumIterations < MaxIterations)
    {
        float2 HZBTileSize = exp2(MipLevel) * HZBBaseTexelSize;
        // RayScreenUV is on a tile boundary due to bStepOutOfCurrentTile
        // Offset the UV along the ray direction so it always quantizes to the next tile
        float2 BiasedUV = RayScreenUV.xy + .01f * RayDirectionScreenUV.xy * HZBTileSize;
        float3 HZBTileMin = float3(floor(BiasedUV / HZBTileSize) * HZBTileSize, 0.0f);
        float3 HZBTileMax = float3(HZBTileMin.xy + HZBTileSize, 1);
        float2 TileIntersections = LineBoxIntersect(RayStartScreenUV, RayEndScreenUV, HZBTileMin, HZBTileMax);
        float3 RayTileHit = RayStartScreenUV + (RayEndScreenUV - RayStartScreenUV) * TileIntersections.y;

        float TileZ;
        float AvoidSelfIntersectionZScale = 1.0f;

#if HZB_TRACE_INCLUDE_FULL_RES_DEPTH
        if (MipLevel < 0)
        {
            TileZ = SceneDepthTexture.SampleLevel(GlobalPointClampedSampler, BiasedUV * HZBUVToScreenUVScaleBias.xy + HZBUVToScreenUVScaleBias.zw, 0).x;
        }
        else
#endif
        {
            TileZ = ClosestHZBTexture.SampleLevel(GlobalPointClampedSampler, BiasedUV, MipLevel).x;
            // 의 차폐/오클루전 , 이므로 HZB mip 0는 2x2뎁스 의 ,HZB저장(Store)16포인트 내에서
            AvoidSelfIntersectionZScale = lerp(.99f, 1.0f, saturate(TileIntersections.y * 10.0f));
        }

        if (RayTileHit.z > TileZ * AvoidSelfIntersectionZScale)
        {
            RayScreenUV = RayTileHit;
            MipLevel++;

            if (TileIntersections.y == 1.0f)
            {
                // 광선(Ray) 및 HZB교차(Intersection).
                MipLevel = BaseMipLevel - 1;
            }
        }
        else
        {
            if (abs(MipLevel - BaseMipLevel) < .1f)
            {
                // 교차점(Intersection)의 UV정렬 까지 의 내에서 ,SceneColor탐색/조회(Find/Lookup).
                RayScreenUV = float3(.5f * (HZBTileMin.xy + HZBTileMax.xy), RayTileHit.z);
                bHit = true;
                float IntersectionDepth = ConvertFromDeviceZ(TileZ);
                float RayTileEnterZ = RayStartScreenUV.z + (RayEndScreenUV.z - RayStartScreenUV.z) * TileIntersections.x;
                bUncertain = (ConvertFromDeviceZ(RayTileEnterZ) - IntersectionDepth) / max(IntersectionDepth, .00001f) > UncertainTraceRelativeDepthThreshold;
            }

            MipLevel--;
        }

        NumIterations++;
    }

    // 광선(Ray)의 선형(Linear)단계/스텝 ,로써 기각(Rejection)의 테이블 (, , )의 교차(Intersection).
    if (bHit && !bUncertain && NumThicknessStepsToDetermineCertainty > 0)
    {
        float ThicknessSearchMipLevel = 0.0f;
        float MipNumTexels = exp2(ThicknessSearchMipLevel);
        float2 HZBTileSize = MipNumTexels * HZBBaseTexelSize;
        float NumSteps = NumThicknessStepsToDetermineCertainty / MipNumTexels;
        float ThicknessSearchEndTime = min(length(RayDirectionScreenUV * HZBTileSize * NumSteps) / length(RayEndScreenUV.xy - RayScreenUV.xy), 1.0f);

        for (float I = 0; I < NumSteps; I++)
        {
            float3 SampleUV = RayScreenUV + (I / NumSteps) * ThicknessSearchEndTime * (RayEndScreenUV - RayScreenUV);

            if (all(SampleUV.xy > 0 && SampleUV.xy < HZBUvFactorAndInvFactor.xy))
            {
                float SampleTileZ = ClosestHZBTexture.SampleLevel(GlobalPointClampedSampler, SampleUV.xy, ThicknessSearchMipLevel).x;

                if (SampleUV.z > SampleTileZ)
                {
                    bUncertain = true;
                }
            }
        }
    }

    OutScreenUV.xy = RayScreenUV.xy * HZBUVToScreenUVScaleBias.xy + HZBUVToScreenUVScaleBias.zw;
    OutScreenUV.z = RayScreenUV.z;
}

HZB 화면 공간의 레이 트레이싱에 대해서는 Master Yan Lingqi의 Lecture9 실시간 전역 조명(화면 공간)의 그래픽 강좌 "GAMES202-고품질 실시간 렌더링"을 참조하는 것이 좋습니다. 영상은 HZB의 순회 및 추적 과정을 자세하고 동적으로 설명합니다. 아래 그림은 비디오 스크린샷의 예입니다.

  • TraceVoxels

복셀 추적을 위한 입력에는 전역 거리 필드, 노멀, 깊이, 하늘빛, 블루 노이즈, VoxelLighting, RadianceProbeIndirectTexture, FinalRadianceAtlas, 광선 정보 등이 포함됩니다. 출력에는 R32의 TraceHit 및 R11G11B10의 TraceRandiance가 포함됩니다.

TraceVoxels의 출력 텍스처 TraceHit은 교차점의 깊이를 저장합니다. 오른쪽 상단 모서리 범위가 조정되었습니다.

TraceVoxels의 출력 텍스처 TraceRadiance는 교차점의 광도를 저장합니다.

사용되는 컴퓨팅 셰이더를 분석해 보겠습니다.

cpp
// Engine\Shaders\Private\Lumen\LumenScreenProbeTracing.usf

[numthreads(PROBE_THREADGROUP_SIZE_1D, 1, 1)]
void ScreenProbeTraceVoxelsCS(
    uint3 GroupId : SV_GroupID,
    uint3 DispatchThreadId : SV_DispatchThreadID,
    uint3 GroupThreadId : SV_GroupThreadID)
{
    if (DispatchThreadId.x < CompactedTraceTexelAllocator[0])
    {
        uint ScreenProbeIndex;
        uint2 TraceTexelCoord;
        float TraceHitDistance;
        // 의 정보 .
        DecodeTraceTexel(CompactedTraceTexelData[DispatchThreadId.x], ScreenProbeIndex, TraceTexelCoord, TraceHitDistance);

        // 계산/산출(Calculate)의 UV.
        uint2 ScreenProbeAtlasCoord = uint2(ScreenProbeIndex % ScreenProbeAtlasViewSize.x, ScreenProbeIndex / ScreenProbeAtlasViewSize.x);
        // 의 라이팅 .
        TraceVoxels(ScreenProbeAtlasCoord, TraceTexelCoord, ScreenProbeIndex, TraceHitDistance);
    }
}

void TraceVoxels(
    uint2 ScreenProbeAtlasCoord,
    uint2 TraceTexelCoord,
    uint ScreenProbeIndex,
    float TraceHitDistance)
{
    // 계산/산출(Calculate)의 UV.
    uint2 ScreenProbeScreenPosition = GetScreenProbeScreenPosition(ScreenProbeIndex);
    uint2 ScreenTileCoord = GetScreenTileCoord(ScreenProbeScreenPosition);

    uint2 TraceCoord = GetTraceBufferCoord(ScreenProbeAtlasCoord, TraceTexelCoord);
    
    {
        // 페치/가져오기(Fetch)스크린 스페이스의 데이터 .
        float2 ScreenUV = GetScreenUVFromScreenProbePosition(ScreenProbeScreenPosition);
        float SceneDepth = GetScreenProbeDepth(ScreenProbeAtlasCoord);
        float3 SceneNormal = DecodeNormal(SceneTexturesStruct.GBufferATexture.Load(int3(ScreenUV * View.BufferSizeAndInvSize.xy, 0)).xyz);

        bool bHit = false;

        {
            // 계산/산출(Calculate)월드 좌표(World Position).
            float3 WorldPosition = GetWorldPositionFromScreenUV(ScreenUV, SceneDepth);

            float2 ProbeUV;
            float ConeHalfAngle;
            // 페치/가져오기(Fetch)UV.
            GetProbeTracingUV(ScreenProbeAtlasCoord, TraceTexelCoord, GetProbeTexelCenter(ScreenTileCoord), 1, ProbeUV, ConeHalfAngle);

            // 로부터 .
            float3 WorldConeDirection = OctahedralMapToDirection(ProbeUV);

            // 샘플링위치 .
            float3 SamplePosition = WorldPosition + SurfaceBias * WorldConeDirection;
            SamplePosition += SurfaceBias * SceneNormal;

            float TraceDistance = MaxTraceDistance;
            bool bCoveredByRadianceCache = false;
#if RADIANCE_CACHE
            float ProbeOcclusionDistance = GetRadianceProbeOcclusionDistanceWithInterpolation(WorldPosition, WorldConeDirection, bCoveredByRadianceCache);
            TraceDistance = min(TraceDistance, ProbeOcclusionDistance);
#endif

            // 입력데이터 .
            FConeTraceInput TraceInput;
            TraceInput.Setup(SamplePosition, WorldConeDirection, ConeHalfAngle, MinSampleRadius, MinTraceDistance, TraceDistance, StepFactor);
            TraceInput.VoxelStepFactor = VoxelStepFactor;
            TraceInput.VoxelTraceStartDistance = max(MinTraceDistance, TraceHitDistance);

            // 출력데이터 .
            FConeTraceResult TraceResult = (FConeTraceResult)0;
            TraceResult.Lighting = 0;
            TraceResult.Transparency = 1;
            TraceResult.OpaqueHitDistance = TraceInput.MaxTraceDistance;

            // Lumen의 라이팅 .
            ConeTraceLumenSceneVoxels(TraceInput, TraceResult);

            if (TraceResult.Transparency <= .5f)
            {
                // 의 자가 교차(Self-intersection)의 노이즈 필터링 .
                #define USE_VOXEL_TRACE_HIT_DISTANCE 0
                #if USE_VOXEL_TRACE_HIT_DISTANCE
                    TraceHitDistance = TraceResult.OpaqueHitDistance;
                #else
                    TraceHitDistance = TraceDistance;
                #endif
                bHit = true;
            }

#if RADIANCE_CACHE
            if (bCoveredByRadianceCache)
            {
                if (TraceResult.Transparency > .5f)
                {
                    // 저장(Save)캐싱(Cache)교차점(Intersection)의 뎁스 .
                    TraceHitDistance = MaxTraceDistance;
                }

                SampleRadianceCacheAndApply(WorldPosition, WorldConeDirection, ConeHalfAngle, float3(0, 0, 0), TraceResult.Lighting, TraceResult.Transparency);
            }
            else
#endif
            {
#if TRACE_DISTANT_SCENE
                // .
                if (TraceResult.Transparency > .01f)
                {
                    FConeTraceResult DistantTraceResult;
                    ConeTraceLumenDistantScene(TraceInput, DistantTraceResult);
                    TraceResult.Lighting += DistantTraceResult.Lighting * TraceResult.Transparency;
                    TraceResult.Transparency *= DistantTraceResult.Transparency;
                }
#endif
                // 계산/산출(Calculate)스카이라이트(Skylight).
                EvaluateSkyRadianceForCone(WorldConeDirection, tan(ConeHalfAngle), TraceResult);

                if (TraceHitDistance >= GetProbeMaxHitDistance())
                {
                    TraceHitDistance = MaxTraceDistance;
                }
            }
            
            #if USE_PREEXPOSURE
                TraceResult.Lighting *= View.PreExposure;
            #endif

            #if DEBUG_VISUALIZE_TRACE_TYPES
                RWTraceRadiance[TraceCoord] = float3(0, 0, .5f) * View.PreExposure;
            #else
                RWTraceRadiance[TraceCoord] = TraceResult.Lighting;
            #endif
        }

        // 저장(Store)결과 , 교차점(Intersection)거리/는 교차(Intersection)/는 까지 32 내에서 .
        RWTraceHit[TraceCoord] = EncodeProbeRayDistance(TraceHitDistance, bHit, false);
    }
}
  • 복합 추적

CompositeTraces는 이전 단계에서 생성된 TraceHit, RayInfo 및 TraceRadianc를 기반으로 ScreenProbeRadiance, ScreenProbeHitDistance 및 ScreenProbeTraceMoving 텍스처를 생성합니다. 사용된 Compute Shader는 LumenScreenProbeFiltering.usf이고, 주요 입구는 ScreenProbeCompositeTracesWithScatterCS입니다. 이 문서에서는 특정 코드가 무시됩니다.

  • FilterRadiananceWithGather

그런 다음 CompositeTrace는 FilterRadianceWithGather를 여러 번 거쳐 프로브 방사율 필터링을 수행합니다.

왼쪽: 필터링 전의 ScreenProbeRadiance; 오른쪽: 여러 필터링 작업을 수행한 후의 ScreenProbeRadiance.

  • 간접 계산

이 단계에서는 이전에 생성된 다양한 화면 공간(깊이, 일반, 기본 색상, FilteredScreenProbeRadiance, BentNormal)의 프로브 데이터를 사용하여 최종 장면 간접광 색상을 계산합니다(아래 그림).

6.5.7.3 RenderLumenReflections

RenderLumenReflections는 루멘 장면에서 표면의 반사를 상대적으로 낮은 거칠기와 매끄러움으로 렌더링하는 것입니다. 해당 프로세스는 RenderLumenScreenProbeGather와 유사하지만 더 간단하고 단계가 더 적습니다.

관련된 C++ 렌더링 코드는 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\Lumen\LumenReflections.cpp

FRDGTextureRef FDeferredShadingSceneRenderer::RenderLumenReflections(
    FRDGBuilder& GraphBuilder, 
    const FViewInfo& View,
    const FSceneTextures& SceneTextures,
    const FLumenMeshSDFGridParameters& MeshSDFGridParameters,
    FLumenReflectionCompositeParameters& OutCompositeParameters)
{
    // 반사(Reflection)의 의 러프니스(Roughness), 의 테이블 무시/스킵(Ignore).
    OutCompositeParameters.MaxRoughnessToTrace = GLumenReflectionMaxRoughnessToTrace;
    OutCompositeParameters.InvRoughnessFadeLength = 1.0f / GLumenReflectionRoughnessFadeLength;

    (......)

    {
        (......)

        auto ComputeShader = View.ShaderMap->GetShader<FReflectionGenerateRaysCS>(0);

        // 광선(Ray)Pass.
        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("GenerateRaysCS"),
            ComputeShader,
            PassParameters,
            ReflectionTileParameters.TracingIndirectArgs,
            0);
    }

    FLumenCardTracingInputs TracingInputs(GraphBuilder, Scene, View);

    (......)

    // 반사(Reflection).
    TraceReflections(
        GraphBuilder, 
        Scene,
        View, 
        GLumenReflectionTraceMeshSDFs != 0 && Lumen::UseMeshSDFTracing(),
        SceneTextures,
        TracingInputs,
        ReflectionTracingParameters,
        ReflectionTileParameters,
        MeshSDFGridParameters);
    
    (......)

    {
        FReflectionResolveCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FReflectionResolveCS::FParameters>();
        
        (......)
        
        auto ComputeShader = View.ShaderMap->GetShader<FReflectionResolveCS>(PermutationVector);

        // 반사(Reflection).
        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("ReflectionResolve"),
            ComputeShader,
            PassParameters,
            ReflectionTileParameters.ResolveIndirectArgs,
            0);
    }

    (......)

    // 갱신(Update)히스토리 데이터(History Data).
    UpdateHistoryReflections(
        GraphBuilder,
        View,
        SceneTextures,
        ReflectionTileParameters,
        ResolvedSpecularIndirect,
        SpecularIndirect);

    return SpecularIndirect;
}

void TraceReflections(
    FRDGBuilder& GraphBuilder,
    const FScene* Scene,
    const FViewInfo& View,
    bool bTraceMeshSDFs,
    const FSceneTextures& SceneTextures,
    const FLumenCardTracingInputs& TracingInputs,
    const FLumenReflectionTracingParameters& ReflectionTracingParameters,
    const FLumenReflectionTileParameters& ReflectionTileParameters,
    const FLumenMeshSDFGridParameters& InMeshSDFGridParameters)
{
    {
        (......)

        auto ComputeShader = View.ShaderMap->GetShader<FReflectionClearTracesCS>(0);

        // 출력텍스처(Texture).
        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("ClearTraces"),
            ComputeShader,
            PassParameters,
            ReflectionTileParameters.TracingIndirectArgs,
            0);
    }

    FLumenIndirectTracingParameters IndirectTracingParameters;
    SetupIndirectTracingParametersForReflections(IndirectTracingParameters);

    const FSceneTextureParameters& SceneTextureParameters = GetSceneTextureParameters(GraphBuilder, SceneTextures);

    const bool bScreenTraces = GLumenReflectionScreenTraces != 0;

    if (bScreenTraces)
    {
        FReflectionTraceScreenTexturesCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FReflectionTraceScreenTexturesCS::FParameters>();

        (......)

        FReflectionTraceScreenTexturesCS::FPermutationDomain PermutationVector;
        auto ComputeShader = View.ShaderMap->GetShader<FReflectionTraceScreenTexturesCS>(PermutationVector);

        // .
        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("TraceScreen"),
            ComputeShader,
            PassParameters,
            ReflectionTileParameters.TracingIndirectArgs,
            0);
    }
    
    // 거리.
    if (bTraceMeshSDFs)
    {
        if (Lumen::UseHardwareRayTracedReflections()) // 반사(Reflection).
        {
            FCompactedReflectionTraceParameters CompactedTraceParameters = CompactTraces(
                GraphBuilder,
                View,
                ReflectionTracingParameters,
                ReflectionTileParameters,
                WORLD_MAX,
                IndirectTracingParameters.MaxTraceDistance);

            RenderLumenHardwareRayTracingReflections(
                GraphBuilder,
                SceneTextureParameters,
                View,
                ReflectionTracingParameters,
                ReflectionTileParameters,
                TracingInputs,
                CompactedTraceParameters,
                IndirectTracingParameters.MaxTraceDistance);
        }
        else
        {
            FLumenMeshSDFGridParameters MeshSDFGridParameters = InMeshSDFGridParameters;
            if (!MeshSDFGridParameters.NumGridCulledMeshSDFObjects)
            {
                CullForCardTracing(
                    GraphBuilder,
                    Scene, View,
                    TracingInputs,
                    IndirectTracingParameters,
                    /* out */ MeshSDFGridParameters);
            }

            if (MeshSDFGridParameters.TracingParameters.DistanceFieldObjectBuffers.NumSceneObjects > 0)
            {
                // 압축/패킹(Packing).
                FCompactedReflectionTraceParameters CompactedTraceParameters = CompactTraces(
                    GraphBuilder,
                    View,
                    ReflectionTracingParameters,
                    ReflectionTileParameters,
                    IndirectTracingParameters.CardTraceEndDistanceFromCamera,
                    IndirectTracingParameters.MaxMeshSDFTraceDistance);

                {
                    (......)
                    
                    auto ComputeShader = View.ShaderMap->GetShader<FReflectionTraceMeshSDFsCS>(PermutationVector);

                    // 거리.
                    FComputeShaderUtils::AddPass(
                        GraphBuilder,
                        RDG_EVENT_NAME("TraceMeshSDFs"),
                        ComputeShader,
                        PassParameters,
                        CompactedTraceParameters.IndirectArgs,
                        0);
                }
            }
        }
    }

    FCompactedReflectionTraceParameters CompactedTraceParameters = CompactTraces(...);

    {
        (......)
        
        auto ComputeShader = View.ShaderMap->GetShader<FReflectionTraceVoxelsCS>(PermutationVector);

        // Voxel라이팅 .
        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("TraceVoxels"),
            ComputeShader,
            PassParameters,
            CompactedTraceParameters.IndirectArgs,
            0);
    }
}

루멘 반사 간접광과 루멘 디퓨즈 간접광의 가장 중요한 차이점은 광선을 추적하는 수와 방법입니다. 루멘 반사는 GLumenReflectionMaxRoughnessToTrace 추적의 최대 거칠기를 지정해야 하며(기본값은 0.4이며 콘솔 명령 r.Lumen.Reflections.MaxRoughnessToTrace로 변경할 수 있음), 생성된 TraceHit 및 TraceRadiance 결과도 달라집니다.

반사와 디퓨즈에 관련된 기술은 매우 유사하므로 이 기사에서는 기술적인 세부 사항을 자세히 다루지 않습니다.

6.5.7.4 확산간접복합

이 단계에서는 이전 RenderLumenScreenProbeGather에서 생성된 프로브 정보(DiffuseIndirect, RoughSpecularIndirect)와 RenderLumenReflections에서 생성된 반사 정보(SpecularIndirect)를 장면의 GBuffer 및 관련 데이터와 결합하여 최종 장면 색상을 생성합니다.

GI의 디퓨즈와 스페큘러 반사를 결합한 후의 장면 색상입니다. (1.5배 확대, 색상범위 조정)

조합 프로세스에 대해서는 사용된 PS에서 답을 찾을 수 있습니다.

hlsl
// Engine\Shaders\Private\DiffuseIndirectComposite.usf

void MainPS(
    float4 SvPosition : SV_POSITION
    , out float4 OutAddColor : SV_Target0
    , out float4 OutMultiplyColor : SV_Target1
)
{
    float2 SceneBufferUV = SvPositionToBufferUV(SvPosition);
    float2 ScreenPosition = SvPositionToScreenPosition(SvPosition).xy;

    // 샘플링의 GBuffer.
    FGBufferData GBuffer = GetGBufferDataFromSceneTextures(SceneBufferUV);

    // 샘플링동적 의 AO.
    float DynamicAmbientOcclusion = AmbientOcclusionTexture.SampleLevel(AmbientOcclusionSampler, SceneBufferUV, 0).r;

    // 계산/산출(Calculate)적용 의 AO.  
    float AOMask = (GBuffer.ShadingModelID != SHADINGMODELID_UNLIT);
    float FinalAmbientOcclusion = lerp(1.0f, GBuffer.GBufferAO * DynamicAmbientOcclusion, AOMask * AmbientOcclusionStaticFraction);

    float3 TranslatedWorldPosition = mul(float4(ScreenPosition * GBuffer.Depth, GBuffer.Depth, 1), View.ScreenToTranslatedWorld).xyz;

    float3 N = GBuffer.WorldNormal;
    float3 V = normalize(View.TranslatedWorldCameraOrigin - TranslatedWorldPosition);
    float NoV = saturate(dot(N, V));

    // 적용 디퓨즈(Diffuse).
#if DIM_APPLY_DIFFUSE_INDIRECT
    {
        float3 DiffuseIndirectLighting = 0;
        float3 RoughSpecularIndirectLighting = 0;
        float3 SpecularIndirectLighting = 0;

        #if DIM_APPLY_DIFFUSE_INDIRECT == 4
            DiffuseIndirectLighting = DiffuseIndirect_Textures_0.SampleLevel(GlobalPointClampedSampler, SceneBufferUV, 0).rgb;
            RoughSpecularIndirectLighting = DiffuseIndirect_Textures_1.SampleLevel(GlobalPointClampedSampler, SceneBufferUV, 0).rgb;
            SpecularIndirectLighting = DiffuseIndirect_Textures_2.SampleLevel(GlobalPointClampedSampler, SceneBufferUV, 0).rgb;
        #else
        {
            // 샘플링디노이징(Denoising)의 출력.
            FSSDKernelConfig KernelConfig = CreateKernelConfig();
                
            #if DEBUG_OUTPUT
            {
                KernelConfig.DebugPixelPosition = uint2(SvPosition.xy);
                KernelConfig.DebugEventCounter = 0;
            }
            #endif

            // Compile time.
            KernelConfig.bSampleKernelCenter = true;
            KernelConfig.BufferLayout = CONFIG_SIGNAL_INPUT_LAYOUT;
            KernelConfig.bUnroll = true;

            #if DIM_UPSCALE_DIFFUSE_INDIRECT
            {
                KernelConfig.SampleSet = SAMPLE_SET_2X2_BILINEAR;
                KernelConfig.BilateralDistanceComputation = SIGNAL_WORLD_FREQUENCY_REF_METADATA_ONLY;
                KernelConfig.WorldBluringDistanceMultiplier = 16.0;
                
                KernelConfig.BilateralSettings[0] = BILATERAL_POSITION_BASED(3);
                
                // SGPRs(Scalar General Purpose Register, 스칼라 을(를) 활용하여 레지스터 )
                KernelConfig.BufferSizeAndInvSize = View.BufferSizeAndInvSize * float4(0.5, 0.5, 2.0, 2.0);
                KernelConfig.BufferBilinearUVMinMax = View.BufferBilinearUVMinMax;
            }
            #else
            {
                KernelConfig.SampleSet = SAMPLE_SET_1X1;
                KernelConfig.bNormalizeSample = true;
                
                // SGPRs
                KernelConfig.BufferSizeAndInvSize = View.BufferSizeAndInvSize;
                KernelConfig.BufferBilinearUVMinMax = View.BufferBilinearUVMinMax;
            }
            #endif

            // VGPRs(Vector General Purpose Register, 벡터 을(를) 활용하여 레지스터 )
            KernelConfig.BufferUV = SceneBufferUV; 
            {
                KernelConfig.CompressedRefSceneMetadata = GBufferDataToCompressedSceneMetadata(GBuffer);
                KernelConfig.RefBufferUV = SceneBufferUV;
                KernelConfig.RefSceneMetadataLayout = METADATA_BUFFER_LAYOUT_DISABLED;
            }
            KernelConfig.HammersleySeed = Rand3DPCG16(int3(SvPosition.xy, View.StateFrameIndexMod8)).xy;
                
            FSSDSignalAccumulatorArray UncompressedAccumulators = CreateSignalAccumulatorArray();
            FSSDCompressedSignalAccumulatorArray CompressedAccumulators = CompressAccumulatorArray(
                UncompressedAccumulators, CONFIG_ACCUMULATOR_VGPR_COMPRESSION);

            // 누적 합산(Accumulate)컨볼루션 커널(Convolution Kernel)
            AccumulateKernel(
                KernelConfig,
                DiffuseIndirect_Textures_0,
                DiffuseIndirect_Textures_1,
                DiffuseIndirect_Textures_2,
                DiffuseIndirect_Textures_3,
                /* inout */ UncompressedAccumulators,
                /* inout */ CompressedAccumulators);

            // 샘플링
            FSSDSignalSample Sample;
            #if DIM_UPSCALE_DIFFUSE_INDIRECT
                Sample = NormalizeToOneSample(UncompressedAccumulators.Array[0].Moment1);
            #else
                Sample = UncompressedAccumulators.Array[0].Moment1;
            #endif
            
            // DIM_APPLY_DIFFUSE_INDIRECT는 1 또는 3오직 디퓨즈(Diffuse).
            #if DIM_APPLY_DIFFUSE_INDIRECT == 1 || DIM_APPLY_DIFFUSE_INDIRECT == 3
            {
                DiffuseIndirectLighting = Sample.SceneColor.rgb;
            }
            // DIM_APPLY_DIFFUSE_INDIRECT는 2디퓨즈(Diffuse) 및 .
            #elif DIM_APPLY_DIFFUSE_INDIRECT == 2
            {
                DiffuseIndirectLighting = UncompressedAccumulators.Array[0].Moment1.ColorArray[0];
                SpecularIndirectLighting = UncompressedAccumulators.Array[0].Moment1.ColorArray[1];
            }
            #else
                #error Unimplemented
            #endif
        }
        #endif

        float3 DiffuseColor = bVisualizeDiffuseIndirect ? float3(.18f, .18f, .18f) : GBuffer.DiffuseColor;
        float3 SpecularColor = GBuffer.SpecularColor;

        #if DIM_APPLY_DIFFUSE_INDIRECT == 4
            RemapClearCoatDiffuseAndSpecularColor(GBuffer, NoV, DiffuseColor, SpecularColor);
        #endif

        #if DIM_APPLY_DIFFUSE_INDIRECT == 2 || DIM_APPLY_DIFFUSE_INDIRECT == 4
            float DiffuseIndirectAO = 1;
        #else
            float DiffuseIndirectAO = lerp(1, FinalAmbientOcclusion, ApplyAOToDynamicDiffuseIndirect);
        #endif

        FDirectLighting IndirectLighting;
        if (GBuffer.ShadingModelID == SHADINGMODELID_HAIR)
        {
            IndirectLighting.Diffuse = DiffuseIndirectLighting * GBuffer.BaseColor;
            IndirectLighting.Specular = 0;
        }
        else
        {
            IndirectLighting.Diffuse = DiffuseIndirectLighting * DiffuseColor * DiffuseIndirectAO;
            IndirectLighting.Transmission = 0;

            #if DIM_APPLY_DIFFUSE_INDIRECT == 4
                IndirectLighting.Specular = CombineRoughSpecular(GBuffer, NoV, SpecularIndirectLighting, RoughSpecularIndirectLighting, SpecularColor);
            #else
                IndirectLighting.Specular = SpecularIndirectLighting * EnvBRDF(SpecularColor, GBuffer.Roughness, NoV);
            #endif
        }

        const bool bNeedsSeparateSubsurfaceLightAccumulation = UseSubsurfaceProfile(GBuffer.ShadingModelID);

        if (bNeedsSeparateSubsurfaceLightAccumulation &&
            View.bSubsurfacePostprocessEnabled > 0 && View.bCheckerboardSubsurfaceProfileRendering > 0)
        {
            bool bChecker = CheckerFromSceneColorUV(SceneBufferUV);

            // Adjust for checkerboard. only apply non-diffuse lighting (including emissive) 
            // to the specular component, otherwise lighting is applied twice
            IndirectLighting.Specular *= !bChecker;
        }

        // 누적 합산(Accumulate)라이팅 결과 .
        FLightAccumulator LightAccumulator = (FLightAccumulator)0;
        LightAccumulator_Add(
            LightAccumulator,
            IndirectLighting.Diffuse + IndirectLighting.Specular,
            IndirectLighting.Diffuse,
            1.0f,
            bNeedsSeparateSubsurfaceLightAccumulation);
        // 페치/가져오기(Fetch)라이팅 결과 .
        OutAddColor = LightAccumulator_GetResult(LightAccumulator);
    }
    #else
    {
        OutAddColor = 0;
    }
    #endif

    OutMultiplyColor = FinalAmbientOcclusion;
}

6.5.8 루멘 요약

루멘의 단계는 많고 복잡하지만 몇 가지 단계로 요약할 수 있습니다.

  1. MeshCard와 LumenCard를 빌드하고 업데이트합니다.

  2. Lumen 장면의 카드 정보를 기반으로 해당 텍셀을 추적하고 업데이트합니다.

  3. 디퓨즈 및 스페큘러 반사 단계에서는 다양한 방법을 사용하여 화면 공간 표면의 조명을 추적하고 계산합니다.

  4. 이전 단계에서 얻은 간접광의 디퓨즈와 스페큘러 반사를 결합하여 간접광과 겹쳐진 최종 장면 색상을 얻습니다.

또한 추적 프로세스에는 여러 가지 방법이 있으며 가중치에 따라 전환됩니다(아래 그림).

하이브리드 추적 다이어그램. 빨간색은 화면 추적을 나타내고, 녹색은 그리드 거리 필드 추적을 나타내고, 파란색은 복셀 조명 추적을 나타냅니다. 색상 전환은 다양한 추적 유형 간의 전환을 나타냅니다.

DEBUG_VISUALIZE_TRACE_TYPES를 1로 수정하고 명령줄에서 ShowFlag.DirectLighting을 꺼 추적 가중치 시각화 모드를 활성화합니다.

cpp
// Engine\Shaders\Private\Lumen\LumenScreenProbeTracing.usf

#define DEBUG_VISUALIZE_TRACE_TYPES 1 // 활성화(Enable)가중치(Weight)시각화(Visualization)(기본값(Default)로 0)

전체적으로 Lumen은 SSGI, SDF(Mesh SDF 및 Global SDF), Lumen Card, Voxel Cone 등의 추적 기술을 통합하고 다양한 기술을 적용하여 다양한 유형의 데이터 정보(adaptive Screen Space Probe, Irradiance Probe, Surface Cache, Prefilter Radiance, Voxel Lighting, RSM, Virtual Texture, Clipmap)를 생성하고 간접광의 디퓨즈와 스페큘러 반사를 계산하고 최종적으로 가중치에 따라 장면 색상에 혼합합니다.

Lumen Diffuse GI는 소프트웨어와 하드웨어 방식을 모두 지원합니다. 기본 매개변수에서 소프트웨어 방법과 관련된 다양한 유형의 추적이 다음과 같이 설명됩니다.

추적 유형 번역된 이름 범위 설명

화면 추적 화면 추적 전체 장면 즉, SSGI는 교차점을 추적할 수 있는 한 리바운드 정보를 먼저 사용합니다.

복셀 조명 추적 복셀 빛 추적 카메라로부터 200m 이내 원뿔 기반 레이 트레이싱은 MIP를 샘플링하여 다양한 적중 거리에 대한 정보를 빠르게 얻습니다.

메시카드 추적 세부정보 세부 그리드 카드 추적 2~40미터 MeshCard 조명 정보를 샘플링할 때 VSM과 유사한 방식으로 폐색 확률 추정이 사용됩니다.

원거리 MeshCard 추적 장거리 그리드 카드 추적 200~1000미터 미리 생성된 전역 거리 필드가 추적되고 폐색 추정치는 더 이상 사용되지 않습니다.

루멘 스페큘러 반사 GI는 소프트웨어와 하드웨어 방식도 모두 지원합니다. 소프트웨어 방식은 SSR + SDF Tracing(Mesh SDF, Global SDF) 기술을 결합합니다.

6.6 기타 렌더링 기술

6.6.1 시간적 초해상도

**TSR(Temporal Super Resolution)**은 기존(UE4) TAA를 대체하는 데 사용되는 차세대 템포럴 안티앨리어싱(TAA) 알고리즘입니다. 그 특성은 저해상도 입력에서 고해상도 출력을 얻는 데 도움이 되며 품질은 기본 해상도보다 우수하고 고주파수에서 고스팅 및 깜박임이 적습니다. PS5 등의 플랫폼에 최적화되어 있지만 SM5.0 이상의 그래픽 플랫폼도 필요합니다.

TSR에서 사용하는 기술은 DLSS가 Tensor Core의 딥 러닝을 기반으로 가속화되는 반면 TSR은 Tensor Core에 의존할 필요가 없다는 점을 제외하면 NVIDIA의 DLSS 및 AMD의 FidelityFX Super Resolution(FSR)과 유사합니다. 즉, TSR은 RTX 그래픽 카드에 의존하지 않고 다른 그래픽 카드 제조업체의 장비에서 실행할 수 있습니다. TSR은 고해상도 텍스처를 출력하기 위해 저해상도를 사용할 수 있기 때문에 앤티앨리어싱 효과를 향상시킬 뿐만 아니라 렌더링 성능을 향상시키고 에너지 소비를 줄일 수 있습니다.

UE4와 다르게 구성이 TemporalAA를 명시적으로 비활성화하지 않는 한 UE5는 어떤 앤티앨리어싱이 선택되었는지에 관계없이 포스트 프로세스 단계에서 TSR 채널을 사용합니다. 호출 스택은 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessing.cpp

void AddPostProcessingPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View, ...)
{
    (......)
    
    // TAA안티앨리어싱(Anti-Aliasing).
    EMainTAAPassConfig TAAConfig = ITemporalUpscaler::GetMainTAAPassConfig(View);
    // TAA구성(Configuration)비활성화(Disable).
    if (TAAConfig != EMainTAAPassConfig::Disabled)
    {
        (......)
        
        // 호출(Call)FDefaultTemporalUpscaler::AddPasses, 의 .
        UpscalerToUse->AddPasses(
            GraphBuilder,
            View,
            UpscalerPassInputs,
            &SceneColor.Texture,
            &SecondaryViewRect,
            &DownsampledSceneColor.Texture,
            &DownsampledSceneColor.ViewRect);
    }
    
    (......)
}

// Engine\Source\Runtime\Renderer\Private\PostProcess\TemporalAA.cpp

void FDefaultTemporalUpscaler::AddPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View,...) const final
{
    // 만약 활성화(Enable)지원 TAA, 이면 TSR채널/패스(Pass).
    if (CVarTAAAlgorithm.GetValueOnRenderThread() && DoesPlatformSupportGen5TAA(View.GetShaderPlatform()))
    {
        *OutSceneColorHalfResTexture = nullptr;

        return AddTemporalSuperResolutionPasses(
            GraphBuilder,
            View,
            PassInputs,
            OutSceneColorTexture,
            OutSceneColorViewRect);
    }
    (......)
}

이로 인해 AddTemporalSuperResolutionPasses가 발생합니다. 다음은 RenderDoc이 가로채는 TSR 렌더링 프로세스입니다.

TSR에는 UE4의 TAA보다 더 많은 패스가 있음을 알 수 있습니다. 여기에는 주로 이전 프레임 텍스처 정리, 속도 버퍼 증폭, 유효하지 않은 속도 버퍼 삭제, 빈도 필터링, 기록 데이터 비교, 사후 필터링 재투영, 재투영 증폭, 기록 업데이트와 같은 여러 단계가 포함됩니다.

위 단계에서 가장 중요한 단계는 업데이트 내역 단계로, 입력 장면 색상, 깊이, 증폭 후 속도, 시차 계수, 과거 프레임 데이터(증폭 후 재투영, 재투영, 고주파수, 저주파, 메타데이터, 하위 픽셀 정보) 및 기타 데이터를 기반으로 최종 앤티앨리어싱된 장면 색상과 현재 과거 프레임 데이터를 생성합니다.

왼쪽: 장면 색상 입력; 오른쪽: TSR 이후 장면 색상 출력.

TSR에서 출력되는 과거 프레임 데이터: 저주파, 고주파, 메타데이터, 하위 픽셀 정보.

분석을 위해 업데이트 기록 단계에서 사용되는 Compute Shader로 직접 이동해 보겠습니다.

hlsl
// /Engine/Private/TemporalAA/TAAUpdateHistory.usf

[numthreads(TILE_SIZE, TILE_SIZE, 1)]
void MainCS(
    uint2 GroupId : SV_GroupID,
    uint GroupThreadIndex : SV_GroupIndex)
{
    uint GroupWaveIndex = GetGroupWaveIndex(GroupThreadIndex, /* GroupSize = */ TILE_SIZE * TILE_SIZE);

    float4 Debug = 0.0;

    // 픽셀(Pixel)위치 .
    taa_short2 HistoryPixelPos = (
        taa_short2(GroupId) * taa_short2(TILE_SIZE, TILE_SIZE) +
        Map8x8Tile2x2Lane(GroupThreadIndex));

    float2 ViewportUV = (float2(HistoryPixelPos) + 0.5f) * HistoryInfo_ViewportSizeInverse;
    float2 ScreenPos = ViewportUVToScreenPos(ViewportUV);
    
    // 입력뷰포트(Viewport) 내에서 출력픽셀(Pixel)O 내에서 의 픽셀(Pixel)좌표 .
    float2 PPCo = ViewportUV * InputInfo_ViewportSize + InputJitter;

    // 의 입력픽셀(Pixel)K의 내에서 픽셀(Pixel)좌표 。
    float2 PPCk = floor(PPCo) + 0.5;
    
    taa_short2 InputPixelPos = ClampPixelOffset(
        taa_short2(InputPixelPosMin) + taa_short2(PPCo),
        InputPixelPosMin, InputPixelPosMax);

    // 페치/가져오기(Fetch)리프로젝션(Reprojection)의 정보 .
    float2 PrevScreenPos = ScreenPos;
    taa_half ParallaxRejectionMask = taa_half(1.0);
    taa_half LowFrequencyRejection = taa_half(1.0);
    taa_half OutputPixelVelocity = taa_half(0.0);
    #if 1
    {
        float2 EncodedVelocity = DilatedVelocityTexture[InputPixelPos];
        ParallaxRejectionMask = ParallaxRejectionMaskTexture[InputPixelPos];

        float2 ScreenVelocity = DecodeVelocityFromTexture(float4(EncodedVelocity, 0.0, 0.0)).xy;

        PrevScreenPos = ScreenPos - ScreenVelocity;
        OutputPixelVelocity = taa_half(length(ScreenVelocity * HistoryInfo_ViewportSize));

        taa_ushort2 RejectionPixelPos = (taa_ushort2(InputPixelPos) - taa_short2(InputPixelPosMin)) / 2;
        LowFrequencyRejection = HistoryRejectionTexture[RejectionPixelPos];
        
        #if !CONFIG_CLAMP
        {
            ParallaxRejectionMask = taa_half(1.0);
            LowFrequencyRejection = taa_half(1.0);
        }
        #endif
    }
    #endif

    // 페치/가져오기(Fetch)픽셀(Pixel)는 AA.
    bool bIsResponsiveAAPixel = false;
    #if CONFIG_RESPONSIVE_STENCIL
    {
        const uint kResponsiveStencilMask = 1 << 3;
            
        uint SceneStencilRef = InputSceneStencilTexture.Load(int3(InputPixelPos, 0)) STENCIL_COMPONENT_SWIZZLE;

        bIsResponsiveAAPixel = (SceneStencilRef & kResponsiveStencilMask) != 0;
    }
    #endif
    
    // 검사/감지(Detect)HistoryBufferUV는 뷰포트(Viewport).
    bool bOffScreen = IsOffScreen(bCameraCut, PrevScreenPos, ParallaxRejectionMask);
    
    taa_half TotalRejection = bOffScreen ? 0.0 : saturate(LowFrequencyRejection * 4.0);

    // 로써 주파수(Frequency)필터링 입력씬 컬러(SceneColor).
    taa_half3 FilteredInputColor;
    taa_half3 InputMinColor;
    taa_half3 InputMaxColor;
    taa_half InputPixelAlignement;
    taa_half ClosestInputLuma4;
    
    ISOLATE
    {
        // 로부터 픽셀(Pixel)K까지 O의 벡터 .
        taa_half2 dKO = taa_half2(PPCo - PPCk);

        FilteredInputColor = taa_half(0.0);

        taa_half FilteredInputColorWeight = taa_half(0.0);
        
        #if 0 // shader compiler bug :'(
            taa_half InputToHistoryFactor = taa_half(HistoryInfo_ViewportSize.x * InputInfo_ViewportSizeInverse.x);
            taa_half FinalInputToHistoryFactor = bOffScreen ? taa_half(1.0) : InputToHistoryFactor;
        #else
            float InputToHistoryFactor = float(HistoryInfo_ViewportSize.x * InputInfo_ViewportSizeInverse.x);
            float FinalInputToHistoryFactor = lerp(1.0, InputToHistoryFactor, TotalRejection);
        #endif

        InputMinColor = taa_half(INFINITE_FLOAT);
        InputMaxColor = taa_half(-INFINITE_FLOAT);

        // 에 따라 CONFIG_SAMPLES을(를) 활용하여 샘플링좌표 샘플링입력의 씬 컬러(SceneColor).
        UNROLL_N(CONFIG_SAMPLES)
        for (uint SampleId = 0; SampleId < CONFIG_SAMPLES; SampleId++)
        {
            taa_short2 SampleInputPixelPos;
            taa_half2 PixelOffset;
            
            #if CONFIG_SAMPLES == 9
            {
                taa_short2 iPixelOffset = taa_short2(kOffsets3x3[kSquareIndexes3x3[SampleId]]);
                PixelOffset = taa_half2(iPixelOffset);
                
                SampleInputPixelPos = AddAndClampPixelOffset(
                    InputPixelPos,
                    iPixelOffset, iPixelOffset,
                    InputPixelPosMin, InputPixelPosMax);
            }
            #elif CONFIG_SAMPLES == 5 || CONFIG_SAMPLES == 6
            {
                if (SampleId == 5)
                {
                    taa_short2 iPixelOffset;
                    #if CONFIG_COMPILE_FP16
                        iPixelOffset = int16_t2(1, 1) - int16_t2((asuint16(dKO) & uint16_t(0x8000)) >> uint16_t(14));
                        PixelOffset = asfloat16(asuint16(half(1.0)).xx | (asuint16(dKO) & uint16_t(0x8000)));
                    #else
                        iPixelOffset = SignFastInt(dKO);
                        PixelOffset = asfloat(asuint(1.0).xx | (asuint(dKO) & uint(0x80000000)));
                    #endif
                        
                    SampleInputPixelPos = ClampPixelOffset(InputPixelPos, InputPixelPosMin, InputPixelPosMax);
                }
                else
                {
                    taa_short2 iPixelOffset = taa_short2(kOffsets3x3[kPlusIndexes3x3[SampleId]]);
                    PixelOffset = taa_half2(iPixelOffset);
                    
                    SampleInputPixelPos = AddAndClampPixelOffset(
                        InputPixelPos,
                        iPixelOffset, iPixelOffset,
                        InputPixelPosMin, InputPixelPosMax);
                }
            }
            #else
                #error Unknown sample count
            #endif

            taa_half3 InputColor = InputSceneColorTexture[SampleInputPixelPos];

            taa_half2 dPP = PixelOffset - dKO;
            taa_half SampleSpatialWeight = ComputeSampleWeigth(FinalInputToHistoryFactor, dPP, /* MinimalContribution = */ float(0.005));

            taa_half ToneWeight = HdrWeight4(InputColor);

            FilteredInputColor       += (SampleSpatialWeight * ToneWeight) * InputColor;
            FilteredInputColorWeight += (SampleSpatialWeight * ToneWeight);

            if (SampleId == 0)
            {
                ClosestInputLuma4 = Luma4(InputColor);
                InputMinColor = TransformColorForClampingBox(InputColor);
                InputMaxColor = TransformColorForClampingBox(InputColor);
            }
            else
            {
                InputMinColor = min(InputMinColor, TransformColorForClampingBox(InputColor));
                InputMaxColor = max(InputMaxColor, TransformColorForClampingBox(InputColor));
            }
        }
        
        FilteredInputColor *= rcp(FilteredInputColorWeight);

        InputPixelAlignement = ComputeSampleWeigth(InputToHistoryFactor, dKO, /* MinimalContribution = */ float(0.0));
    }
        
    // 저장(Save)까지 LDS 내에서 ,로 VGPR샘플링히스토리 데이터(History Data).
    #if CONFIG_MANUAL_LDS_SPILL
    ISOLATE
    {
        uint LocalGroupThreadIndex = GetGroupThreadIndex(GroupThreadIndex, GroupWaveIndex);

        SharedArray0[LocalGroupThreadIndex] = taa_half4(FilteredInputColor, LowFrequencyRejection);
        SharedArray1[LocalGroupThreadIndex] = taa_half4(InputMinColor, InputPixelAlignement);
        SharedArray2[LocalGroupThreadIndex] = taa_half4(InputMaxColor, OutputPixelVelocity);
    }
    #endif
    
    // 리프로젝션(Reprojection)히스토리 데이터(History Data).
    taa_half3 PrevHistoryMoment1;
    taa_half PrevHistoryValidity;
    
    taa_half3 PrevHistoryMommentMin;
    taa_half3 PrevHistoryMommentMax;

    taa_half3 PrevFallbackColor;
    taa_half PrevFallbackWeight;
    
    taa_subpixel_details PrevSubpixelDetails;

    ISOLATE
    {
        // 리프로젝션(Reprojection)히스토리 데이터(History Data).
        taa_half3 RawHistory0 = taa_half(0);
        taa_half3 RawHistory1 = taa_half(0);
        taa_half2 RawHistory2 = taa_half(0);

        taa_half3 RawHistory1Min = INFINITE_FLOAT;
        taa_half3 RawHistory1Max = -INFINITE_FLOAT;

        // 샘플링의 히스토리 데이터(History Data).
        {
            float2 PrevHistoryBufferUV = (PrevHistoryInfo_ScreenPosToViewportScale * PrevScreenPos + PrevHistoryInfo_ScreenPosToViewportBias) * PrevHistoryInfo_ExtentInverse;
            PrevHistoryBufferUV = clamp(PrevHistoryBufferUV, PrevHistoryInfo_UVViewportBilinearMin, PrevHistoryInfo_UVViewportBilinearMax);

            #if 1
            {
                FCatmullRomSamples Samples = GetBicubic2DCatmullRomSamples(PrevHistoryBufferUV, PrevHistoryInfo_Extent, PrevHistoryInfo_ExtentInverse);

                UNROLL
                for (uint i = 0; i < Samples.Count; i++)
                {
                    float2 SampleUV = clamp(Samples.UV[i], PrevHistoryInfo_UVViewportBilinearMin, PrevHistoryInfo_UVViewportBilinearMax);

                    taa_half3 Sample0 = PrevHistory_Textures_0.SampleLevel(GlobalBilinearClampedSampler, SampleUV, 0);
                    taa_half3 Sample1 = PrevHistory_Textures_1.SampleLevel(GlobalBilinearClampedSampler, SampleUV, 0);
                    taa_half2 Sample2 = PrevHistory_Textures_2.SampleLevel(GlobalBilinearClampedSampler, SampleUV, 0);

                    RawHistory1Min = min(RawHistory1Min, Sample1 * SafeRcp(Sample2.g));
                    RawHistory1Max = max(RawHistory1Max, Sample1 * SafeRcp(Sample2.g));

                    RawHistory0 += Sample0 * taa_half(Samples.Weight[i]);
                    RawHistory1 += Sample1 * taa_half(Samples.Weight[i]);
                    RawHistory2 += Sample2 * taa_half(Samples.Weight[i]);
                }
                RawHistory0 *= taa_half(Samples.FinalMultiplier);
                RawHistory1 *= taa_half(Samples.FinalMultiplier);
                RawHistory2 *= taa_half(Samples.FinalMultiplier);
            }
            #else
            {
                RawHistory0 = PrevHistory_Textures_0.SampleLevel(GlobalBilinearClampedSampler, PrevHistoryBufferUV, 0);
                RawHistory1 = PrevHistory_Textures_1.SampleLevel(GlobalBilinearClampedSampler, PrevHistoryBufferUV, 0);
                RawHistory2 = PrevHistory_Textures_2.SampleLevel(GlobalBilinearClampedSampler, PrevHistoryBufferUV, 0);
            }
            #endif
            
            FSubpixelNeighborhood SubpixelNeighborhood = GatherPrevSubpixelNeighborhood(PrevHistory_Textures_3, PrevHistoryBufferUV);
            {
                PrevSubpixelDetails = 0;
                UNROLL_N(SUB_PIXEL_COUNT)
                for (uint SubpixelId = 0; SubpixelId < SUB_PIXEL_COUNT; SubpixelId++)
                {
                    taa_subpixel_payload SubpixelPayload = GetSubpixelPayload(SubpixelNeighborhood, SubpixelId);
                    PrevSubpixelDetails |= SubpixelPayload << (SUB_PIXEL_BIT_COUNT * SubpixelId);
                }
            }

            RawHistory0 = -min(-RawHistory0, taa_half(0.0));
            RawHistory1 = -min(-RawHistory1, taa_half(0.0));
            RawHistory2 = -min(-RawHistory2, taa_half(0.0));
        }
        
        // 압축해제/언패킹(Unpacking)히스토리 데이터(History Data).
        {
            PrevFallbackColor = RawHistory0;
            PrevFallbackWeight = RawHistory2.r;
            
            PrevHistoryMommentMin = RawHistory1Min;
            PrevHistoryMommentMax = RawHistory1Max;

            PrevHistoryMoment1 = RawHistory1;
            PrevHistoryValidity = RawHistory2.g;
        }

        // 보정(Correct)히스토리 데이터(History Data).
        {
            PrevHistoryMommentMin *= taa_half(HistoryPreExposureCorrection);
            PrevHistoryMommentMax *= taa_half(HistoryPreExposureCorrection);
            PrevHistoryMoment1 *= taa_half(HistoryPreExposureCorrection);
            PrevFallbackColor *= taa_half(HistoryPreExposureCorrection);
        }
    }
    
    // 로부터 LDS로드/읽기(Read)데이터 .
    #if CONFIG_MANUAL_LDS_SPILL
    ISOLATE
    {
        uint LocalGroupThreadIndex = GetGroupThreadIndex(GroupThreadIndex, GroupWaveIndex);

        taa_half4 RawLDS0 = SharedArray0[LocalGroupThreadIndex];
        taa_half4 RawLDS1 = SharedArray1[LocalGroupThreadIndex];
        taa_half4 RawLDS2 = SharedArray2[LocalGroupThreadIndex];

        FilteredInputColor = RawLDS0.rgb;
        InputMinColor = RawLDS1.rgb;
        InputMaxColor = RawLDS2.rgb;
        
        LowFrequencyRejection = RawLDS0.a;
        InputPixelAlignement = RawLDS1.a;
        OutputPixelVelocity = RawLDS2.a;
    }
    #endif

    // 만약 현재(Current), 기각/디스카드(Rejection).
    #if CONFIG_LOW_FREQUENCY_DRIFT_REJECTION
    {
        taa_half3 PrevHighFrequencyYCoCg = TransformColorForClampingBox(PrevHistoryMoment1 * SafeRcp(PrevHistoryValidity));
        taa_half3 PrevYCoCg = TransformColorForClampingBox(PrevFallbackColor);
        taa_half3 ClampedPrevYCoCg = TransformColorForClampingBox(clamp(PrevFallbackColor, PrevHistoryMommentMin, PrevHistoryMommentMax));

        taa_half HighFrequencyRejection = MeasureRejectionFactor(
            PrevYCoCg, ClampedPrevYCoCg,
            PrevHighFrequencyYCoCg, InputMinColor, InputMaxColor);
        
        PrevHistoryMoment1 *= HighFrequencyRejection;
        PrevHistoryValidity *= HighFrequencyRejection;
    }
    #endif

    // 현재(Current)의 입력입력까지 다음 프레임(Next Frame)의 내에서 .
    const taa_half Histeresis = rcp(taa_half(MAX_SAMPLE_COUNT));
    const taa_half PredictionOnlyValidity = Histeresis * taa_half(2.0);
    
    // 데이터 .
    taa_half LumaMin;
    taa_half LumaMax;
    taa_half3 ClampedFallbackColor;
    taa_half FallbackRejection;
    {
        LumaMin = InputMinColor.x;
        LumaMax = InputMaxColor.x;

        taa_half3 PrevYCoCg = TransformColorForClampingBox(PrevFallbackColor);
        taa_half3 ClampedPrevYCoCg = clamp(PrevYCoCg, InputMinColor, InputMaxColor);
        taa_half3 InputCenterYCoCg = TransformColorForClampingBox(FilteredInputColor);

        ClampedFallbackColor = YCoCgToRGB(ClampedPrevYCoCg);
        
        FallbackRejection = MeasureRejectionFactor(
            PrevYCoCg, ClampedPrevYCoCg,
            InputCenterYCoCg, InputMinColor, InputMaxColor);

        #if !CONFIG_CLAMP
        {
            ClampedFallbackColor = PrevFallbackColor;
            FallbackRejection = taa_half(1.0);
        }
        #endif
    }

    taa_half3 FinalHistoryMoment1;
    taa_half FinalHistoryValidity;
    {
        // 에 따라 ,계산/산출(Calculate)기각/디스카드(Rejection)히스토리 버퍼.
        taa_half PrevHistoryRejectionWeight = LowFrequencyRejection;
            
        FLATTEN
        if (bOffScreen)
        {
            PrevHistoryRejectionWeight = taa_half(0.0);
        }

        taa_half DesiredCurrentContribution = max(Histeresis * InputPixelAlignement, taa_half(0.0));

        // 의 기각/디스카드(Rejection)는 .
        taa_half RejectionConfidentEnough = taa_half(1); // saturate(RejectionValidity * MAX_SAMPLE_COUNT - 3.0);

        // 계산/산출(Calculate)기각/디스카드(Rejection)의 유효성(Validity).
        taa_half RejectedValidity = (
            min(PrevHistoryValidity, PredictionOnlyValidity - DesiredCurrentContribution) +
            max(PrevHistoryValidity - PredictionOnlyValidity + DesiredCurrentContribution, taa_half(0.0)) * PrevHistoryRejectionWeight);

        RejectedValidity = PrevHistoryValidity * PrevHistoryRejectionWeight;

        // 계산/산출(Calculate)출력유효성(Validity).
        taa_half OutputValidity = (
            clamp(RejectedValidity + DesiredCurrentContribution, taa_half(0.0), PredictionOnlyValidity) +
            clamp(RejectedValidity + DesiredCurrentContribution * PrevHistoryRejectionWeight * RejectionConfidentEnough - PredictionOnlyValidity, 0.0, 1.0 - PredictionOnlyValidity));

        FLATTEN
        if (bIsResponsiveAAPixel)
        {
            OutputValidity = taa_half(0.0);
        }
        
        taa_half InvPrevHistoryValidity = SafeRcp(PrevHistoryValidity);

        taa_half PrevMomentWeight = max(OutputValidity - DesiredCurrentContribution, taa_half(0.0));
        taa_half CurrentMomentWeight = min(DesiredCurrentContribution, OutputValidity);
        
        {
            taa_half PrevHistoryToneWeight = HdrWeightY(Luma4(PrevHistoryMoment1) * InvPrevHistoryValidity);
            taa_half FilteredInputToneWeight = HdrWeight4(FilteredInputColor);
            
            taa_half BlendPrevHistory = PrevMomentWeight * PrevHistoryToneWeight;
            taa_half BlendFilteredInput = CurrentMomentWeight * FilteredInputToneWeight;

            taa_half CommonWeight = OutputValidity * SafeRcp(BlendPrevHistory + BlendFilteredInput);

            FinalHistoryMoment1 = (
                PrevHistoryMoment1 * (CommonWeight * BlendPrevHistory * InvPrevHistoryValidity) +
                FilteredInputColor * (CommonWeight * BlendFilteredInput));
        }

        // 양자화 유효성(Validity)의 8조정(Adjust),로써 .
        taa_half OutputInvValidity = SafeRcp(OutputValidity);
        FinalHistoryValidity = ceil(taa_half(255.0) * OutputValidity) * rcp(taa_half(255.0));
        FinalHistoryMoment1 *= FinalHistoryValidity * OutputInvValidity;
    }

    // 계산/산출(Calculate)을(를) 활용하여 의 히스토리 데이터(History Data).
    taa_half3 FinalFallbackColor;
    taa_half FinalFallbackWeight;
    {
        const taa_half TargetHesteresisCurrentFrameWeight = rcp(taa_half(MAX_FALLBACK_SAMPLE_COUNT));

        taa_half LumaHistory = Luma4(PrevFallbackColor);
        taa_half LumaFiltered = Luma4(FilteredInputColor);

        {
            taa_half OutputBlend = ComputeFallbackContribution(FinalHistoryValidity);
        }

        taa_half BlendFinal;
        #if 1
        {
            taa_half CurrentFrameSampleCount = max(InputPixelAlignement, taa_half(0.005));
            
            // 을(를) 활용하여 개 로써 복구 기각/디스카드(Rejection), 그러나 즉, ,로써 픽셀(Pixel)주파수(Frequency)로써 을(를) 활용하여 .
            taa_half PrevFallbackSampleCount;
            FLATTEN
            if (PrevFallbackWeight < taa_half(1.0))
            {
                PrevFallbackSampleCount = PrevFallbackWeight;
            }
            else
            {
                PrevFallbackSampleCount = taa_half(MAX_FALLBACK_SAMPLE_COUNT);
            }

            // 에 따라 기각/디스카드(Rejection)히스토리 데이터(History Data).
            #if 1
            {
                taa_half PrevFallbackRejectionFactor = saturate(LowFrequencyRejection * (CurrentFrameSampleCount + PrevFallbackSampleCount) / PrevFallbackSampleCount);

                PrevFallbackSampleCount *= PrevFallbackRejectionFactor;
            }
            #endif

            BlendFinal = CurrentFrameSampleCount / (CurrentFrameSampleCount + PrevFallbackSampleCount);

            // 추가(Add)의 블렌딩(Blending)가중치(Weight).
            #if 1
            {
                BlendFinal = lerp(BlendFinal, max(taa_half(0.2), BlendFinal), saturate(OutputPixelVelocity * rcp(taa_half(40.0))));
            }
            #endif

            // 플리커링 .
            #if 1
            {
                taa_half DistToClamp = min( abs(LumaHistory - LumaMin), abs(LumaHistory - LumaMax) ) / max3( LumaHistory, LumaFiltered, taa_half(1e-4) );
                BlendFinal *= taa_half(0.2) + taa_half(0.8) * saturate(taa_half(0.5) * DistToClamp);
            }
            #endif
            
            // 의 .
            #if 1
            {
                BlendFinal = max( BlendFinal, saturate( taa_half(0.01) * LumaHistory / abs( LumaFiltered - LumaHistory ) ) );
            }
            #endif

            // 는 의 1/4.
            BlendFinal = bIsResponsiveAAPixel ? taa_half(1.0/4.0) : BlendFinal;

            // 기각/디스카드(Rejection)히스토리 데이터(History Data).
            {
                PrevFallbackSampleCount *= TotalRejection;
                BlendFinal = lerp(1.0, BlendFinal, TotalRejection);
            }

            FinalFallbackWeight = saturate(CurrentFrameSampleCount + PrevFallbackSampleCount);
            
            #if 1
                FinalFallbackWeight = saturate(floor(255.0 * (CurrentFrameSampleCount + PrevFallbackSampleCount)) * rcp(255.0));
            #endif
        }
        #endif

        {
            taa_half FilterWeight = HdrWeight4(FilteredInputColor);
            taa_half ClampedHistoryWeight = HdrWeight4(ClampedFallbackColor);

            taa_half2 Weights = WeightedLerpFactors(ClampedHistoryWeight, FilterWeight, BlendFinal);

            FinalFallbackColor = ClampedFallbackColor * Weights.x + FilteredInputColor * Weights.y;
        }
    }

    // 갱신(Update)픽셀(Pixel).
    taa_subpixel_details FinalSubpixelDetails;
    {
        taa_half2 dKO = taa_half2(PPCo - PPCk);

        bool bUpdate = all(abs(dKO) < 0.5 * (InputInfo_ViewportSize.x * HistoryInfo_ViewportSizeInverse.x));

        FinalSubpixelDetails = PrevSubpixelDetails;

        taa_subpixel_payload ParallaxFactorBits = ParallaxFactorTexture[InputPixelPos] & SUB_PIXEL_PARALLAX_FACTOR_BIT_MASK;

        {
            const uint ParallaxFactorMask = (
                (SUB_PIXEL_PARALLAX_FACTOR_BIT_MASK << (SUB_PIXEL_PARALLAX_FACTOR_BIT_OFFSET + 0 * SUB_PIXEL_BIT_COUNT)) | 
                (SUB_PIXEL_PARALLAX_FACTOR_BIT_MASK << (SUB_PIXEL_PARALLAX_FACTOR_BIT_OFFSET + 1 * SUB_PIXEL_BIT_COUNT)) | 
                (SUB_PIXEL_PARALLAX_FACTOR_BIT_MASK << (SUB_PIXEL_PARALLAX_FACTOR_BIT_OFFSET + 2 * SUB_PIXEL_BIT_COUNT)) | 
                (SUB_PIXEL_PARALLAX_FACTOR_BIT_MASK << (SUB_PIXEL_PARALLAX_FACTOR_BIT_OFFSET + 3 * SUB_PIXEL_BIT_COUNT)) | 
                0x0);
            
            // 리셋/초기화(Reset)계수.
            FLATTEN
            if (bOffScreen)
            {
                FinalSubpixelDetails = FinalSubpixelDetails & ~ParallaxFactorMask;
            }
        }

        FLATTEN
        if (bUpdate)
        {
            bool2 bBool = dKO < 0.0;

            uint SubpixelId = dot(uint2(bBool), uint2(1, SUB_PIXEL_GRID_SIZE));
            uint SubpixelShift = SubpixelId * SUB_PIXEL_BIT_COUNT;

            taa_subpixel_payload SubpixelPayload = (ParallaxFactorBits << SUB_PIXEL_PARALLAX_FACTOR_BIT_OFFSET);

            FinalSubpixelDetails = (FinalSubpixelDetails & (~(SUB_PIXEL_BIT_MASK << SubpixelShift))) | (SubpixelPayload << SubpixelShift);
        }
    }

    // 계산/산출(Calculate)출력.
    taa_half3 FinalOutputColor;
    taa_half FinalOutputValidity;
    {
        taa_half OutputBlend = ComputeFallbackContribution(FinalHistoryValidity);

        FinalOutputValidity = lerp(taa_half(1.0), saturate(FinalHistoryValidity), OutputBlend);

        taa_half3 NormalizedFinalHistoryMoment1 = taa_half3(FinalHistoryMoment1 * float(SafeRcp(FinalHistoryValidity)));

        taa_half FallbackWeight = HdrWeight4(FinalFallbackColor);
        taa_half Moment1Weight = HdrWeight4(NormalizedFinalHistoryMoment1);

        taa_half2 Weights = WeightedLerpFactors(FallbackWeight, Moment1Weight, OutputBlend);

        #if DEBUG_FALLBACK_BLENDING
            taa_half3 FallbackColor = taa_half3(1, 0.25, 0.25);
            taa_half3 HighFrequencyColor = taa_half3(0.25, 1, 0.25);

            FinalOutputColor = FinalFallbackColor * Weights.x * FallbackColor + NormalizedFinalHistoryMoment1 * Weights.y * HighFrequencyColor;
        #elif DEBUG_LOW_FREQUENCY_REJECTION
            taa_half3 DebugColor = lerp(taa_half3(1, 0.5, 0.5), taa_half3(0.5, 1, 0.5), LowFrequencyRejection);
            
            FinalOutputColor = FinalFallbackColor * Weights.x * DebugColor + NormalizedFinalHistoryMoment1 * Weights.y * DebugColor;
        #else
            FinalOutputColor = FinalFallbackColor * Weights.x + NormalizedFinalHistoryMoment1 * Weights.y;
        #endif
    }

    ISOLATE
    {
        uint LocalGroupThreadIndex = GetGroupThreadIndex(GroupThreadIndex, GroupWaveIndex);

        taa_short2 LocalHistoryPixelPos = (
            taa_short2(GroupId) * taa_short2(TILE_SIZE, TILE_SIZE) +
            Map8x8Tile2x2Lane(LocalGroupThreadIndex));
            
        LocalHistoryPixelPos = InvalidateOutputPixelPos(LocalHistoryPixelPos, HistoryInfo_ViewportMax);

        // 출력의 히스토리 데이터(History Data).
        {
            #if CONFIG_ENABLE_STOCASTIC_QUANTIZATION
            {
                uint2 Random = Rand3DPCG16(int3(LocalHistoryPixelPos, View.StateFrameIndexMod8)).xy;
                float2 E = Hammersley16(0, 1, Random);

                FinalHistoryMoment1 = QuantizeForFloatRenderTarget(FinalHistoryMoment1, E.x, HistoryQuantizationError);
                FinalFallbackColor = QuantizeForFloatRenderTarget(FinalFallbackColor, E.x, HistoryQuantizationError);
            }
            #endif

            FinalFallbackColor = -min(-FinalFallbackColor, taa_half(0.0));
            FinalHistoryMoment1 = -min(-FinalHistoryMoment1, taa_half(0.0));
            FinalFallbackColor = min(FinalFallbackColor, taa_half(Max10BitsFloat));
            FinalHistoryMoment1 = min(FinalHistoryMoment1, taa_half(Max10BitsFloat));
            
            HistoryOutput_Textures_0[LocalHistoryPixelPos] = FinalFallbackColor;
            HistoryOutput_Textures_1[LocalHistoryPixelPos] = FinalHistoryMoment1;
            HistoryOutput_Textures_2[LocalHistoryPixelPos] = taa_half2(FinalFallbackWeight, FinalHistoryValidity);
            HistoryOutput_Textures_3[LocalHistoryPixelPos] = FinalSubpixelDetails;

            #if DEBUG_OUTPUT
            {
                DebugOutput[LocalHistoryPixelPos] = Debug;
            }
            #endif
        }

        // 출력의 씬 컬러(SceneColor).
        {
            taa_half3 OutputColor = FinalOutputColor;
                
            OutputColor = -min(-OutputColor, taa_half(0.0));
            OutputColor = min(OutputColor, taa_half(Max10BitsFloat));

            SceneColorOutput[LocalHistoryPixelPos] = OutputColor;
        }
    }
}

기존 TAA와 비교하여 TSR은 현재 및 과거 고주파, 저주파, 시차 계수, 재투영 및 기타 데이터를 포함하여 많은 데이터를 추가하는 것을 볼 수 있습니다. 이 정보를 기반으로 과거 데이터를 연속적으로 삭제하거나 복원하고, 현재 프레임의 혼합 가중치를 생성하고, 최종적으로 앤티앨리어싱 후 장면 색상과 과거 프레임 데이터를 계산합니다.

위 코드는 TSR의 마지막 단계에서 과거 데이터를 업데이트하는 코드일 뿐입니다. 이 단계에 필요한 데이터를 생성하는 데는 여러 단계가 있습니다. 이 글에서는 이를 분석하지 않고 독자들이 스스로 공부하도록 맡기겠습니다.

6.6.2 지층

저자는 Strata의 관련 코드를 대략적으로 살펴보았습니다. Strata는 UE4의 Material Layer와 비슷한 것 같지만 주로 Nanite 지오메트리의 재질 투영, 혼합, 빛과 그림자 처리에 사용됩니다. Strata에는 전용 재료, 재료 노드, 셰이딩 모델, 시각화 모드 및 셰이더 처리 모듈이 있습니다. 그러나 현재 EA 버전은 아직 시험 단계에 있으며 많은 제한 사항이 있습니다. Strata와 관련된 주요 문서는 다음과 같습니다.

  • Strata.h/cpp

  • StrataMaterial.h/cpp

-StrataDefinitions.h

  • MaterialExpressionStrata.h

  • Strata.ush

  • BasePassPixelShader.usf

  • DeferredLightPixelShaders.usf

  • 장면 렌더링 파이프라인 및 조명과 관련된 코드입니다.

관심 있는 학생들은 관련 소스코드를 스스로 공부할 수 있습니다.

6.7 이 기사 요약

이 글에서는 주로 UE5의 에디터 기능인 Nanite, Lumen 및 관련 렌더링 기술에 대해 설명합니다. 하지만 UE5의 큰 변화로 인해 모든 기술적 사항을 다룰 수는 없습니다. 이 기사에 언급된 기술 외에도 실제로 다루지 않은 기술이 많이 있습니다. 이를 위해서는 관심 있는 독자가 스스로 UE의 소스 코드를 탐색해야 합니다.

UE5 EA 단계에서는 Nanite와 Lumen 모두 결함이 많습니다. 예를 들어 Nanite는 정적 개체만 지원하고 Lumen에는 소음과 빛 누출이 있고 TSR 깜박임과 흐림, 그림자 정확도 부족(아래 그림) 등 많은 기존 기능이 지원되지 않습니다.

렌즈가 물체에 충분히 가까울 때 발생하는 물체 흐림 및 그림자 아티팩트.

현재 UE5에는 결함이 많지만 햇빛과 비를 받은 묘목입니다. Epic Game에서 세심한 재배를 거쳐 시간이 지남에 따라 결국 잎이 우뚝 솟은 나무로 성장하여 UE 엔진과 관련된 모든 계층을 보호하게 될 것입니다. UE5는 정말 No.1!!!

특별 지침

  • 모든 참고문헌의 저자에게 감사드립니다. 일부 사진은 참고 자료와 인터넷에서 가져온 것이므로 삭제되었습니다.

  • 이 시리즈의 기사는 저자가 직접 작성한 것이며 블로그에만 게시됩니다. 이 글의 링크를 공유하셔도 좋지만 무단 전재는 허용되지 않습니다!

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.








📚 참고 자료 및 공식 링크 (References)


🔗 연관 지식 베이스 (Wiki & Tools)

Based on the legendary technical series by Timlly (0向往0 / 毛星云)