ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] Auto Exposure (2/2) - Histogram
    UE4 & UE5/Rendering 2022. 12. 15. 07:00

    [UE5] Auto Exposure (2/2) - Histogram

    최초 작성 : 2022-12-15
    마지막 수정 : 2022-12-15
    최재호

     

    목차

    1. 환경
    2. 목표
    3. 내용
    3.1. 전체 분석할 렌더 프레임 확인
    3.2. Histogram Atomic 733x603 (CS) 단계
    3.3. Histogram Convert 단계
    3.4. HistogramEyeAdaptation (CS) 단계
    3.5. EnqueueCopy(EyeAdaptation) 단계와 Tonemapping 단계
    4. 레퍼런스

    1. 환경

    Unreal Engine 5.0.3 (ue5-release branch d9d435c9c280b99a6c679b517adedd3f4b02cfd7)

    UE4.26.2 코드와 거의 유사하므로 UE4에 익숙하시면 보시는데 문제가 없을 것 같습니다.
    개인적으로 분석한 내용이라 틀린 점이 있을 수 있습니다. 그런 부분은 알려주시면 감사하겠습니다.

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

    이전 글인 [UE5] Auto Exposure (1/2) 를 기반으로 하는 내용이기 때문에 먼저 보시는 것을 추천드립니다.

    2. 목표

    이전 글에서는 Auto Exposure Basic 모드에 대해서 알아봤습니다. Auto Exposure Basic 방식은 전체 픽셀의 Luminance 의 기하평균을 구해서 노출을 조정하는 방식이었습니다.

    이 방식의 단점은 의도치 않게 너무 밝거나 어두운 노출을 사용할 수 있다는 점입니다. 빛이 하나도 없는 어두운 방안에 작은 창문을 통해서 빛이 들어오고 있다고 합시다. 이 경우 이미지의 대부분의 밝기가 어둡기 때문에 이미지가 어둡다고 판단하고 Auto Exposure 는 밝기를 계속해서 올릴 것입니다. 이렇게 되면 화면에서 적은 영역이지만 라이팅이 되어서 적절한 밝기로 보여야 할 사물들이 과도하게 밝아질 수 있습니다. (그림1 참고)

    이 부분을 개선한 것이 Histogram 방식이며, 오늘은 이 방식에 대해서 알아봅시다.

    그림1. Basic 과 Histogram mode 의 Auto Exposure 차이

     

    3. 내용

    오늘 알아볼 Auto Exposure Histogram 방식은 PostProcess Settings 에서 설정할 수 있습니다. 아래 그림2에 있는 데이터들을 조작하여 Auto Exposure 를 설정합니다. 대부분의 항목이 Basic 에서 사용되었으며, 추가된 부분은 Advance 에 있는 4가지 항목입니다. 이 부분은 코드를 분석하며 차차 알아봅시다.

    기하평균(Geometric mean)을 활용하여 적절한 Exposure 값을 구하는 것은 동일하므로, 기하평균에 대한 것은 이전 글(레퍼런스 2)을 참고해주세요.

    그림2. Auto Exposure Histogram 방식에 필요한 프로퍼티

     

    3.1. 전체 분석할 렌더 프레임 확인

    렌더독으로 오늘 분석해볼 렌더 커맨드들을 찍어서 어떤 항목들이 있는지 확인해봅시다. 오늘 볼 모든 과정은 PostProcess 에서 처리됩니다.
    1. ClearTextureUint 는 Histogram 을 저장할 텍스쳐를 0으로 Clear 합니다. 128x1 크기며, R32_UINT 입니다.
    2. Histogram Atomic 에서는 Histogram 데이터를 바로 전에 클리어한 텍스쳐에 채웁니다. 이 텍스쳐의 x 축의 인덱스는 log2(Luminance) 값을 기준으로 결정되며, 텍스쳐에 저장될 데이터는 log2(Luminance) 의 Weight 값이 저장됩니다.
    3. Histogram Convert 에서는 이전 패스에서 만든 히스토그램(R32_UINT) 타입의 결과를 16x2 인 R32G32B32A32 로 변환하고, Weight 값을 노멀라이즈 합니다.
    4. HistogramEyeAdaptation 는 실제 사용할 ExposureScale 값을 구해냅니다.
    5. 바로 전 과정에서 구한 ExposureScale 값을 CPU 쪽에서 사용할 수 있도록 Readback 합니다. 이 값은 PreExposure 라는 기능에 사용하게 됩니다(PC 에만 사용됨).
    6. 현재 적용해야 할 ExposureScale 값을 전체 SceneColor 값에 각각 곱해줍니다. 그 결과 Exposure 가 적절히 보정된 이미지를 얻을 수 있습니다.

    그림3. HistogramEyeAdaptation 과정을 보여주는 렌더독 렌더커맨드 캡쳐


    ClearTextureUInt 는 단순히 텍스쳐의 데이터를 0으로 Clear 하는 연산이므로 바로 다음 커맨드로 넘어가 봅시다.

    3.2. Histogram Atomic 733x603 (CS) 단계

    먼저 이 단계의 렌더커맨드인 vkCmdDispatch(603, 1, 1) 를 봅시다(그림3 참고). 603 개의 WorkGroup 을 사용하는데 이 값이 Luminance 를 계산할 입력 텍스쳐의 SizeY 와 동일합니다. 즉, 각각의 WorkGroup 은 한 개의 입력 텍스쳐 행을 담당하게 됩니다.

    여기서 사용하는 프리프로세서는 다음과 같은 값을 설정합니다. THREADGROUP_SIZEX=64, HISTOGRAM_SIZE=64, HISTOGRAMS_PER_THREAD=1 입니다.

    위 사항을 염두해두고 계속해서 코드를 봅시다.

    1. WorkGroup 당 Thread 는 64 개로 설정된 것을 볼 수 있습니다. 64 개의 Thread 가 입력 텍스쳐의 1개의 Row 를 담당하여 Histogram 데이터를 채울 것입니다.
    2. SV_GroupID 는 현재 실행 중인 WorkGroup 의 Index, SV_GroupThreadID 는 WorkGroup 내에서 현재 실행중인 Thread 의 Id 입니다. SV_* 에 대한 자세한 설명은 레퍼런스4 를 참고해주세요.
    3. WorkGroup 내에서만 공유되는 LDS 배열을 초기화합니다. ClearLDS 함수에서 HISTOGRAMS_PER_THREAD 는 1이므로 무시해도 될 것입니다. 그리고 HISTOGRAM_SIZE 는 64 입니다. WorkGroup 내에서만 사용할 LDS 메모리는 PerThreadGroupHistogramLD32 입니다. 언리얼 기본 설정이 32 bit 기준이라 이 기준으로 코드를 보겠습니다. 마지막으로 GroupMemoryBarrierWithGroupSync() 함수를 호출하여 WorkGroup 내에 모든 thread 가 이 지점에 도달할 때까지 기다립니다.
    4. ViewportMin 을 offset 으로 사용합니다만 현재는 0이라 무시해도 됩니다. y 값을 GroupId.x 로 설정하는데, 이 ComputeShader 는 WorkGroup 당 1개의 Row 를 담당하기 때문에 GroupId.x 를 입력 텍스쳐의 y 값으로 둘 수 있을 것입니다.
    5. 이제 0~ViewportSize.x 만큼 for loop 를 수행하는데, x 값을 THREADGROUP_SIZEX=64 만큼씩 이동시킵니다. 왜 그런지 계속해서 다음 코드를 봅시다.
    6. 이번에 사용할 TexelPos 를 구성합니다. 여기서 GroupThreadId.x 에 for 에서 사용하는 x 값 그리고 offset 값을 사용합니다. offset 값은 0일 것이므로 무시하겠습니다. 이 코드는 입력 텍스쳐 Row 의 각 픽셀을 처리하는 THREADGROUP_SIZEX=64 개의 Thread 가 각 픽셀을 접근하는 코드입니다. 첫 번째 thread 는 0, 64, 128... 그리고 두번째 thread 는 1, 65, 129... 세번째 thread 는 2, 66, 130... 일 것입니다. 모든 스레드가 작업을 가능하면 균등하게 가져가기 위해서 이런 형태로 TexelPos를 얻어오는 것으로 보입니다.
    7. 이제 현재 위치의 텍셀을 기준으로 Histogram 을 만들어냅니다. Histogram 은 총 HISTOGRAM_SIZE=64개로 나뉘어있는 배열입니다. 배열의 각 요소들을 Bucket 이라고 부르며, 각 Bucket 은 log(Luminance)의 0~1 의 범위를 HISTOGRAM_SIZE=64 의 범위로 쪼개서 사용합니다. 여기서 log(Luminance)는 배열의 Index, 그리고 Weight 는 배열의 값에 대응됩니다. 현재 텍셀이 2개의 Bucket 에 걸쳐져 있는 경우가 있을 수 있으므로, Bucket과 Weight 를 2개씩 가집니다. 이 함수의 내부는 그림5 에서 확인할 예정입니다.
    8. 이 부분은 이전글(레퍼런스2)의 MeterMask 에 대한 내용입니다. 화면에서 원하는 영역만 Auto Exposure 연산에 참여시키고 싶을 때 사용하는 Mask 를 얻습니다. 그리고 이 값을 ScreenWeight 에 담습니다.
    9. 얻은 Bucket 과 Weight 정보를 WorkGroup 내에 있는 LDS 에 먼저 씁니다. 함수 내부 자세한 내용은 그림6에서 확인할 수 있습니다. 담당하는 Row 의 모든 픽셀에 대해 작업이 완료되는 WorkGroup 내에 Thread 가 실행을 마칠 수 있도록 GroupMemoryBarrierWithGroupSync() 를 호출합니다.
    10. 마지막으로 각 WorkGroup 들이 갖고 있는 LDS 데이터를 최종 결과 텍스쳐에 담습니다. 이 텍스쳐는 128x1 크기에 R32_UINT 포맷입니다.

    그림4. SceneColor 의 log2(Luminance)를 사용하여 Histogram 을 구성하는 MainAtomicCS 함수


    CalculateBucketsAndWeights 함수는 입력 텍스쳐의 텍셀로부터 Histogram 의 어느 버킷에 얼마의 가중치를 저장해야 되는지를 판단하는 함수입니다.
    1. 입력 텍스쳐의 텍셀을 얻습니다. PreExposure 처리는 이전 글(레퍼런스 2)에서 설명했 듯 정밀도 문제를 해결하는데 도움을 주는 기능입니다. 하지만 Auto Exposure 연산에 영향을 주지 않으므로 생략하도록 하겠습니다.
    2. 1에서 얻어온 SceneColor 값을 기준으로 Luminance 값을 얻어옵니다.
    3. 2에서 생성한 Luminance 의 log2(Luminance)를 구한 후 HistogramScale과 HistogramBias 를 적용해줍니다. 이 두 값을 사용하여 Histogram 저장될 Histogram 의 Bucket 위치를 조정할 수 있습니다.
    4. log2(Luminance) 를 0~1 사이 값으로 잘라낸 뒤 0~63 의 인덱스를 가질 수 있도록 해줍니다.
    5. log2(Luminance)가 2개의 Bucket 에 걸쳐져 있을 수 있기 때문에 Bucket0 의 바로 우측에 있는 Bucket1 도 사용합니다. 그리고 Histogram 배열의 크기를 넘지 않도록 적절히 처리해줍니다.
    6. frac 함수를 사용하여 Bucket 의 소수점을 얻어내어, Bucket0, Bucket1 에 log2(Luminance)가 각각 얼마나 걸쳐져 있는지 얻습니다. 이 값을 Weight0, Weight1 로 설정합니다.
    7. Histogram 의 가장 첫 번째 Bucket 을 사용할지 말지를 결정합니다. 검은색 배경을 가진 경우 이 부분이 영향을 주지 못하도록 할 수 있습니다. r.EyeAdaptation.BlackHistogramBucketInfluence 콘솔 명령어를 통해서 이 값을 설정할 수 있고, 기본 값은 0으로 Histogram 의 가장 첫번째 Bucket 을 사용하지 않습니다.

    그림5. SceneColor 의 텍셀을 사용하여 Histogram 의 Bucket 과 Weight 값을 구하는 CalculateBucketsAndWeights 함수

    ScatterWeightsToLDS 함수는 WorkGroup 내에서 Thread 들이 처리한 Bucket과 Weight 정보를 LDS 에 저장합니다.
    1. Bucket0, Bucket1 에 각각 Weight 값을 저장합니다. Weight 값에는 MeterMask 에서 얻어오는 ScreenWeight 값을 적용하여 저장합니다. 또한 이 Float 값을 UINT32 값으로 저장합니다. 이 부분은 atomic 연산을 위해서 uint32 으로 변환하는 것으로 보입니다.
    2. Wave Intrinsic 을 이용하여 이런 InterlockedAdd 함수를 최적화하는 부분입니다. 어떻게 최적화하는지 확인해 봅시다.
    3. Weight0*ScreenWeight 를 uint 로 변환하여 w 값이 저장합니다. 그리고 현재 Wavefront(or warp) 내에서 실행 중인 총 Wave 수를 얻습니다.
    4. ComputeUnit 내에서 사용 중인 모든 Active lane 이 동일하게 조건식을 true 로 평가하는지 확인하는 함수가 바로 WaveActiveAllTrue 입니다. 여기서 WaveActiveAllTure 에 전달된 것은 Bucket0 의 인덱스와 Bucket0 의 Weight 값이 동일한지 확인하는 것입니다. 이 두 가지 값이 같다면 Bucket1 과 그 Weight 값도 동일할 것입니다.
    5. 모든 Active lane 의 값이 동일하다면 Bucket0, Bucket1에 (w * LaneCountPerWave) 로 한번에 모든 Active lane 의 weight 값을 더해줄 수 있습니다. WaveGetLaneIndex() 는 lane 의 0번만 동작하도록 하는 것이며, (w * LaneCountPerWave) 로 모든 lane 의 정보를 한번의 atomic 연산으로 저장하기 때문에 0번 lane 이 대표로 이 작업을 수행합니다. 만약 총 Active lane 의 수가 64 개고, 모든 Weight 값이 동일하면 InterlockedAdd 함수는 Bucket0, Bucket1 에 대해서 단 2번만 호출될 것입니다. 그렇지 않고 64 개의 Lane 이 모두 다른 Bucket 이나 Weight 값을 가진다면 총 64 번의 InterlockedAdd 함수가 호출될 것입니다.

    그림6. LDS 에 Bucket 과 Weight 값을 저장하는 ScatterWeightsToLDS 함수


    GatherWeightsFromLDSToTempTexture 함수는 LDS 에 저장된 Histogram 값을 결과 텍스쳐에 저장합니다.
    1. HISTOGRAMS_PER_THREAD=1 이기 때문에 t 값은 무시해도 좋습니다. GroupThreadId 는 WorkGroup 내의 ThreadId 라고 하였습니다. 이것은 64이며, HISTOGRAM_SIZE=64 와 동일합니다. 각각의 Thread 들이 자신의 Id 와 일치하는 Bucket 에 대한 정보를 Texture 에 저장할 것입니다.
    2. 인자로 전달받은 GroupThreadId 를 Bucket0 으로 사용합니다. Bucket0 에 저장된 Weight0 을 얻어옵니다.
    3. Bucket0 과 Weight0 을 InterlockedAddToTexture 에 전달합니다.
    4. Texture 에 InterlockedAdd 함수를 사용하여 atomic 하게 weight 값을 더해줍니다. 이때 Histogram 당 2개의 텍셀을 사용하여 저장합니다. 첫 번째 텍셀은 weight 값을 누적시킨 값이고 두 번째 텍셀은 오버 플로우 여부를 저장하며, 오버플로우 시 1을 증가시킵니다. 이런 이유로 처음 Histogram texture clear 시킬 때 128x1 크기의 R32_UINT 를 사용했던 것입니다. 텍스쳐 크기가 128 이지만 실제로 Histogram 크기는 64 라는 것을 다시 알 수 있습니다.

    그림7. LDS 의 Histogram 데이터를 출력 텍스쳐에 저장하는 함수

     

    3.3. Histogram Convert 단계

    Histogram Convert 단계는 이전 과정에서 구한 Histogram 128x1 크기 R32_UINT 데이터를 16x2 크기 R32G32B32A32_FLOAT 타입 텍스쳐로 변환합니다. 이전 과정을 봤을 때 Histogram은 1개의 Histogram 을 표현하기 위해서 2개의 텍셀을 사용한다고 봤었습니다. 그럼 총 64 개의 Histogram 요소가 있습니다. 이것을 텍셀당 4개씩 담아주면 128/4=16 크기로 둘 수 있을 것입니다. 그럼 계속해서 코드를 보겠습니다.
    1. GroupThreadId 는 현재 WorkGroup 에서 ThreadId 입니다. 자세한 사항은 레퍼런스4를 참고해주세요.
    2. NormalizeFactor 는 Histogram을 만들 때 사용한 이미지의 총 텍셀 개수의 역수입니다.
    3. HISTOGRAMS_PER_THREAD=1 이기 때문에 t는 무시할 수 있습니다. index 는 GroupThreadId 에 따라 결정됩니다. 텍셀당 4개의 Weight 값을 저장할 것이므로 index 는 HISTOGRAM_SIZE / 4 까지만 사용합니다.
    4. 우리는 언리얼 기본 설정인 32Bbit 를 사용하기 때문에 이쪽 코드를 보겠습니다. HistogramScatter32Texture 는 128x1 크기에 R32_UINT 이며, Histogram 요소당 2개의 텍셀을 사용한다고 했습니다. 여기서 ValLow 는 32 bit 로 커버 가능한 값이 있고, ValHigh 는 32 bit 값 오버플로 된 경우의 데이터가 저장되어있습니다. 여기서는 이 값을 얻어옵니다.
    5. 바로 전 과정에서 얻어온 값을 원래 값인 float weight 값으로 변경합니다.
    6. 결과 텍스쳐(16x2 크기의 R32G32B32A32_FLOAT)에 Histogram Weight 값을 저장합니다. 이때 고려해야 할 부분이 하나 있습니다. HistogramScatter32Texture 에 들어있는 Weight 값은 텍셀당 1 Weight 를 가지는 값을 기준으로 계산되었습니다. 여기서는 모든 텍셀 Weight 의 총합이 1 로 노멀라이즈 될 수 있도록 NormalizeFactor 를 곱해서 저장합니다. 이전 글(레퍼런스2)의 Geometric mean 과는 식이 조금 다르게 보이는데 왜 이렇게 하는지는 그림9 에서 계속해서 봅시다. Val *= 0.5 식의 경우 주석에 나와있듯이 제거된 패스와의 호환을 위해서 추가한 코드로 보입니다. 이 코드로 인해서 Histogram 의 TotalWeight 값은 1이 아닌 0.5 로 노멀라이즈 됩니다.

    그림8. Histogram 의 Weight 를 노멀라이즈하고, R32B32G32A32_Float 텍스쳐 포맷으로 저장하는 HistogramConvertCS 함수


    아래 그림은 Histogram Convert 단계에서 왜 NormalizeFactor 가 들어갔는지 확인하기 위해서 추가한 그림입니다. Histogram 의 각각의 Index 는 log2(Luminance) 를 [0, 1] 로 Clamp 한 뒤, 64개로 분할한 것입니다. Histogram 은 현재 어느 Index 에 있는 log2(lum) 가 가중치를 가장 크게 가져가는지를 나타내는 데이터입니다. Histogram Convert 단계 후의 모습인 그림9의 위쪽 그림을 보면, 적당히 log2(Luminance)가 여러 인덱스로 나뉘어져 있을 수 있습니다. 각각의 Histogram 요소는 Weight 를 의미하고 최대 값은 1일 것입니다. 만약 완전히 검정색만 있는 이미지의 Histogram 이라고 한다면 모든 log2(lum) 값이 0일 것입니다. 이때는 0번 위치에 있는 log2(lum)[0] 만 사용될 것입니다. 이 경우 log2(lum)[0] * 1 이 되어야 우리가 원하는 결과를 얻을 수 있을 것입니다. 그래서 Histogram Convert 단계에서 NormalizeFactor 곱해주는 것입니다. 염두해둘 점은 그림8의 6에서 보았듯 Val *= 0.5 코드로 인해서 TotalWeight 는 1이 아닌 0.5로 노멀라이즈 된다는 점입니다. 하지만 그림11의 8에서 보겠지만 weighted geometric mean 을 사용하기 때문에 0.5로 노멀라이즈 되더라도 그림9의 아래 그림처럼 log2(lum)[0] 의 값만 사용하는 경우도 문제없다는 것을 알 수 있습니다. 이 부분은 차차 확인해봅시다.

    그림9. Histogram 의 Weight 값을 Normalize 하는 이유를 설명하기 위한 이미지

     

    3.4. HistogramEyeAdaptation (CS) 단계

    EyeAdaptationCS 함수는 이전 글(레퍼런스2)와 거의 유사합니다. AverageSceneLuminance 를 구하고 점진적으로 TargetExposure 로 변화시켜서 EyeAdaptation 효과를 적용합니다. 이 Compute Shader 는 WorkGroup 1개 Thread 1 개로 수행되는 단일 작업입니다.
    1. WorkGroup과 Thread 가 1개이기 때문에 DispatchThreadId 는 0일 것입니다.
    2. ComputeEyeAdaptationExposure 에서는 Histogram Convert 과정에서 만든 HistogramTexture 를 사용하여 AverageSceneLuminance 를 구해냅니다. ComputeEyeAdaptationExposure 함수는 그림11 에서 자세히 봅시다.
    3. 이전 글(레퍼런스2)에서 했던 것과 같은 ExposureScale 과정을 구하는 부분입니다. 중복되는 내용이라 여기서 다시 설명하지 않겠습니다.
    4. 이번에 적용할 ExposureScale 을 결과 텍스쳐에 기록합니다.

    그림10. Histogram 으로 부터 AverageSceneLuminance 를 구하고 EyeAdaptation 을 적용한 뒤 ExposureScale 을 구하는 EyeAdaptationCS 함수


    ComputeEyeAdaptationExposure 함수에 대해서 자세히 알아봅시다.
    1. ComputeEyeAdaptationExposure 함수입니다. 여기서는 AverageSceneLuminance 를 구합니다.
    2. ComputeHistogramSum 은 HistogramTexture 의 모든 Histogram 의 Weight 를 더합니다. 이 전체 TotalWeight 값을 기준으로 MinFractionSum, MaxFractionSum 값을 구하는 데 사용합니다.
    3. 실제로 AverageSceneLuminance 을 구하는 함수입니다. 이전 과정에서 구한 HistogramSum 을 사용하여 MinFractionSum, MaxFractionSum 을 구하며, ExposureLowPercent, ExposureHighPercent 를 HistogramSum 에 곱하여 이 값들을 생성합니다. 이 값들을 사용하여 (MinFractionSum, MaxFractionSum) 사이에 있는 Weight 값들만 사용하게 됩니다. 그래서 ExposureLowPercent 값이 높을수록 낮은 log2(Luminance) 값을 더 많이 무시하고, ExposureHighPercent 가 낮을수록 높은 log2(Luminance) 값을 더 많이 무시하게 됩니다.
    4. GetHistogramBucket 함수를 사용하여 i 인덱스에 있는 Histogram 의 Weight 값을 구하여 LocalValue 에 담습니다.
    5. 총 Weight 에서 MinFractionSum 이하는 사용하지 않도록 해주는 부분입니다. 그러기 위해서 LocalValue 에 MinFractionSum or LocalValue 만큼 감소시킵니다. 그 결과 MinFractionSum 이 0이 될 때까지 LocalValue 를 사용할 수 없을 것입니다. LocalValue 를 감소시킨 다음 MinFractionSum과 MaxFractionSum 에도 동일한 값을 감소시킵니다. 왜냐하면 이번에 LocalValue 에 감소시킨 Weight 는 이미 TotalWeight 에서 사용한 부분이기 때문에 남은 TotalWeight 에 대해서만 고려할 수 있도록 MinFractionSum, MaxFractionSum 를 수정해야하기 때문입니다.
    6. 총 Weight 에서 MaxFractionSum 이상은 사용하지 않도록 해주는 부분입니다. LocalValue 를 MaxFactionSum 이 0이 될때까지 사용하도록 해줍니다. 여기서도 MaxFractionSum 에 사용한 Weight 값을 누적하여 감소시켜서 남은 TotalWeight 에 대해서만 고려할 수 있도록 MaxFractionSum 을 수정해줍니다.
    7. ComputeLogLuminanceFromHistogramPosition 함수로 현재 i 인덱스가 log2(Luminance)로 얼마의 값을 가지는지를 계산합니다. ComputeLogLuminanceFromHistogramPosition 함수 내부로 들어가면 HistogramBias 와 HistogramScale 를 역으로 적용합니다(그림5의 3 의 ComputeHistogramPositionFromLogLumiance 의 반대이기 때문에 역으로 적용)
    8. Histogram 의 각 요소의 log2(Luminance) 와 weight 값의 Weighted sum 을 구하여 SumWithoutOutliners.x 에 담습니다. SumWithoutOutliners.y 에는 Weight 의 총합을 담습니다.
    9. 최종적으로 exp2(Weighted sum / total weight) 를 구한 후 결과를 리턴합니다. 이 식은 log2(Luminance) 에 대한 Weighted sum 을 사용하였기 때문에 Weighted geometric mean 식을 사용한 것이라 볼 수 있습니다. Weighted geometric mean 의 식은 레퍼런스5 를 참고해주세요.

    그림11. Histogram 으로 부터 AverageLuminance 를 구하는 ComputeEyeAdaptationExposure 함수

     

    3.5. EnqueueCopy(EyeAdaptation) 단계와 Tonemapping 단계

    이 두 단계는 이전 글에서 봤던 Basic 방식과 동일합니다. CPU 에서 ExposureScale 값을 쓰기 위해서 EyeAdaptation 값을 Readback 하고, Tonemapping 단계에서는 전체 픽셀에 ExposureScale 값을 곱해줍니다. 자세한 내용은 레퍼런스2를 참고해주세요.

    4. 레퍼런스

    1. https://github.com/EpicGames/UnrealEngine/commit/d9d435c9c280b99a6c679b517adedd3f4b02cfd7
    2. https://scahp.tistory.com/105
    3. https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/PostProcessEffects/AutomaticExposure/
    4. https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/sv-dispatchthreadid
    5. https://en.wikipedia.org/wiki/Weighted_geometric_mean
    6. https://knarkowicz.wordpress.com/2016/01/09/automatic-exposure/

     

    댓글

Designed by Tistory & scahp.