| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- forward
- unrealengine
- shader
- GPU
- Shadow
- DX12
- wave
- scalar
- 번역
- atmospheric
- optimization
- ShadowMap
- RayTracing
- vulkan
- texture
- Study
- ue4
- SGPR
- deferred
- VGPR
- rendering
- UE5
- Wavefront
- DirectX12
- SIMD
- hzb
- scattering
- Graphics
- GPU Driven Rendering
- Nanite
- Today
- Total
RenderLog
[UE5] Auto Exposure (1/2) 본문
[UE5] Auto Exposure (1/2)
최초 작성 : 2022-11-28
마지막 수정 : 2022-11-28
최재호
Updated 2022-12-15 : Weighted geometric mean 을 사용한다는 내용 추가
목차
1. 환경
2. 목표
3. 내용
3.1. 전체 분석할 렌더 프레임 확인
3.2. ExposureScale 구하는 식 확인
3.3. BasicEyeAdaptationSetup 단계
3.4. BasicEyeAdaptation (CS)
3.5. EnequeueCopy(EyeAdaptation) 단계
3.6. Tonemapping 단계
4. 레퍼런스
1. 환경
Unreal Engine 5.0.3 (ue5-release branch d9d435c9c280b99a6c679b517adedd3f4b02cfd7)
UE4.26.2 코드와 거의 유사하므로 UE4에 익숙하시면 보시는데 문제가 없을 것 같습니다.
개인적으로 분석한 내용이라 틀린 점이 있을 수 있습니다. 그런 부분은 알려주시면 감사하겠습니다.
이번 글은 한 번에 많은 길이의 코드를 분석하는 부분이 종종 등장합니다. 그래서 글을 2개 띄우고 한쪽은 설명 부분을 한쪽은 코드 이미지를 최대화해서 보는 것을 추천합니다.
2. 목표
UE5의 Auto Exposure 에 대해서 알아봅시다. Auto exposure 는 주어진 장면의 라이트 상태에 따라서 적절한 장면의 Exposure를 선택합니다. 그리고 인간의 눈이 라이트 밝기에 반응하듯이 시간에 따라 서서히 Exposure가 변화하는 것을 시뮬레이션하는 것입니다.
이번 글에서는 Auto Exposure Basic 모드에 대해서 알아봅시다. 그리고 다음 글에서 계속해서 Auto Exposure Histogram 방식을 알아볼 예정입니다.
3. 내용
오늘 알아볼 Auto Exposure Basic 방식은 PostProcess Settings에서 설정할 수 있습니다. 아래 그림1에 있는 데이터들을 조작하여 Auto Exposure 를 설정합니다. 각 항목에 대한 자세한 설명은 레퍼런스2 를 참고해주세요.

3.1. 전체 분석할 렌더 프레임 확인
렌더독으로 오늘 분석해볼 렌더 커맨드들을 찍어서 어떤 항목들이 있는지 확인해봅시다. 오늘 볼 모든 과정은 PostProcess 에서 처리됩니다.
1. BasicEyeAdaptationSetup 단계입니다. SceneColor 렌더타겟을 다운스케일 하는 도중에 처리됩니다. 여기서는 SceneColor 의 log2(Luminance) 를 구합니다. log2(Luminance)를 구하는 이유는 Exposure 계산에 Geometric mean(기하평균) 을 활용하기 때문입니다. 이 부분은 ExposureScale 구하는 식을 설명을 할 때 알아보도록 합시다.
2. BasicEyeAdaptation 단계는 SceneColor 렌더타겟의 Luminance(Geometric mean) 을 사용하여 TargetExposureScale값을 구하고, OldExposureScale 값에서 TargetExposureScale 값으로 시간에 따라 서서히 변할 수 있도록 Eye adaptation 까지 적용합니다. 이 과정에서 결과로 나오는 ExposureScale 값은 float 값이며 Tonemap 단계에서 사용됩니다.
3. 바로 전 과정에서 구한 ExposureScale 값을 CPU 쪽에서 사용할 수 있도록 Readback 합니다. 이 값은 PreExposure 라는 기능에 사용하게 됩니다(PC 에만 사용됨).
4. 현재 적용해야 할 ExposureScale 값을 전체 SceneColor 값에 각각 곱해줍니다. 그 결과 Exposure 가 적절히 보정된 이미지를 얻을 수 있습니다.

3.2. ExposureScale 구하는 식 확인
계속하기 전에 ExposureScale 값을 구하는 식을 확인해봅시다.
ExposureScale 를 구하는 데는 여러 가지 방식이 있지만, 보통은 Reinhard 가 제시한 Geometric mean 을 사용한 방식(레퍼런스3)을 활용합니다. Geometric mean 을 구하는 식을 먼저 알아보고, 차례로 ExposureScale 식을 봅시다.




위의 식을 사용하여 우리는 아래의 순서대로 ExposureScale 을 구합니다.
- 이미지 각 픽셀의 Luminance 를 구합니다.
- 이미지 모든 픽셀을 사용하여 Luminance geometric mean(기하평균) 를 구합니다.
- ExposureScale = Middle Grey(0.18) / (Luminance geometric mean) 을 얻습니다.
Middle grey 와 Key Value
- ExposureScale 에서 Middle Grey(0.18) 를 Key Value 라고 부릅니다. 이미지에서 Key 는 이미지가 밝은지, 보통인지, 어두운지를 나타냅니다. 또한 (KeyValue / Luminance geometric mean) 식의 의미는 Luminance의 기하평균값을 KeyValue에 매핑시키겠다는 의미입니다. 평균값이 큰 KeyValue에 매핑되면 밝아지고, 평균 값이 값이 낮은 KeyValue에 매핑되면 어두워질 것입니다. 일반적인 장면을 담은 이미지는 기본값으로 Middle grey 를 사용하여 평균값이 중간 정도의 톤에 매핑되도록 합니다. 이 Key Value 를 조정하면 원하는 이미지의 Exposure를 얻어낼 수 있습니다.
- Middle Grey 0.18로 사용하는 이유는 검정과 흰색의 중간 정도로 지각되는 컬러이기 때문입니다. 사진이나 인쇄에서는 Middle grey 가시광선의 18% reflectance 에 해당하기 때문에 0.18 를 사용합니다. (레퍼런스6 참고)
3.3. BasicEyeAdaptationSetup 단계
1. 먼저 SceneColor 에 대해서 Luminance 를 구합니다. 일반적인 RGB 컬러의 Luminance 를 구하는 식이 아닌 RGB 채널 각각 1/3 의 가중치를 가지는 형태로 Luminance 를 구한 것을 볼 수 있습니다. Luminance 를 구한 후 log2(Luminance)를 사용합니다. log2 를 사용하는 것은 Geometric mean 을 구하기 위해서 미리 계산해 두는 것입니다.
2. 바로 전 과정에서 구한 log2(Luminance) 를 렌더타겟의 w 채널에 저장합니다. 이때 Histogram 관련 Bias 와 Scale 값을 적용하는데 Basic 에서는 이 부분들을 다음 단계에서 상쇄시킨 후 계산하므로 무시하고 봐도 상관없습니다. 상쇄되는 부분은 BasicEyeAdaptation 에서 보도록 합시다.

3.4. BasicEyeAdaptation (CS) 단계
ComputeShader 방식의 작동을 확인할 예정입니다. 이 단계에서는 이미지의 Geometric Mean(기하평균) 을 구하고, 그것으로 ExposureScale 값을 구한 다음, Eye Adaptation 이 진행될 수 있도록 적절히 ExposureScale 값을 시간 흐름에 따라 점진적으로 증가시키도록 하는 작업을 수행합니다.
1. TGSIZE 는 전체 이미지를 16 * 16 개의 SubRect 영역으로 쪼개기 위해 사용됩니다. Compute Shader 의 각 Thread 당 한 개의 SubRect를 담당하기 때문에 numthreads(TGSIZE, TGSIZE, 1) 인 것을 확인할 수 있습니다. uint GIndex : SV_GroupIndex 는 현재 수행 중인 Thread 의 인덱스, uint2 GTId : SV_GroupThreadID 는 현재 수행중인 Thread 의 (x, y, z) 모양의 인덱스입니다. (그림9 를 참고해주세요)
2. 이미지의 SubRect 의 크기를 결정합니다. 이것을 통해 각 스레드당 처리하게 될 픽셀 수를 결정합니다. 계속해서 ComputeWeightTextureAverageAlphaSubRegion 함수를 수행합니다. 이 함수에서는 각각의 스레드가 자신이 담당하는 SubRect 의 값을 모두 더합니다. 이때 더해지는 값은 그림7의 BasicEyeAdaptationSetupPS 과정에서 저장해둔 log2(Luminance) * Weight 값입니다. 함수 내부에 자세한 처리는 그림10 을 참고해주세요.
3. 이 과정은 비트 연산을 통해서 SubRectValueWeights 배열에 있는 모든 값을 더해주는 작업을 수행합니다. GIndex ^ 1 는 바로 옆의 인덱스를 가리키고, GIndex ^ 2 는 현재 인덱스 기준 2 칸 옆에 있는 값을 가리킵니다. 이 과정을 모두 마치면 배열 내의 모든 엘리먼트에는 배열의 모든 값을 합산한 결과를 동일하게 가집니다.
4. SubrectValueWeight[0] 의 x 에는 이미지의 모든 log2(Luminance) * Weight 의 합, y 에는 TotalWeight(픽셀당 Weight 가 1인경우 총 이미지의 픽셀 수)가 담겨있습니다. 배열의 모든 값이 모든 배열의 값을 서로 합산한 결과를 가지고 있기 때문에 어떤 위치에 값을 사용하던지 상관없습니다. 이렇게 Weight 를 반영한 Geometric mean 을 Weighted geometric mean 이라 부르며, 식은 레퍼런스9 를 참고해주세요.
5. Histogram 값을 Bias 하고 Scale 하는 부분은 이전의 BasicEyeAdaptationSetupPS 과정인 그림7의 2 과정에서 수행한 것을 되돌리는 것이기 때문에 무시해도 됩니다. 그리고 exp2(LogLumAve) 를 해주어 Geometric mean 을 구해냅니다 (그림4 의 식 참고). 추가로 OneOverPreExposure 함수를 곱해주는데, 이것 또한 무시해도 됩니다. OneOverPreExposure 를 곱해주는 이유는 SceneColor 렌더타겟을 구성할 때 PreExposure 값을 곱해주기 때문입니다. PreExposure 는 이전 프레임의 ExposureScale 값이며, 이 값을 곱해주게 되면 아주 밝은 픽셀의 경우 Exposure 조정으로 값이 낮아질 수 있으며 그 결과 너무 높은 값의 연산으로 인해 발생하는 계산 오류를 피할 수 있기 때문입니다. (레퍼런스2 참고)
6. ExposureCompensationSettings 는 노출을 조정하기 위한 값입니다. 이 값이 0이면 노출 보정이 없으며, 1, 2는 각각 2배 4배 밝아지며, -1, -2는 각각 2배 4배 어두워집니다. 0은 노출보정이 없는 것입니다. (레퍼런스2 참고). ExposureCompensationCurve 의 경우 그림1에서 사용하는 커브입니다. 현재는 커브를 설정하지 않아 1이 됩니다. GreyMult 의 경우 0.18 이 됩니다. 이 값은 이전 식에서 본 Middle grey 값입니다.
7. 5번 과정에서 구한 Luminance geometric mean 을 최소, 최대값이 넘지 않도록 클램핑 해줍니다.
8. 바로 전 프레임에서 사용한 Luminance geometric mean 값을 얻어옵니다. ExposureScale 를 저장할 때는 ExposureCompensationSettings / Luminance geometric mean 을 저장했기 때문에 적절한 값을 곱하고 역수를 취해서 Luminance geometric mean 값을 얻어옵니다.
9. 이전 프레임과 현재 프레임에서 사용한 Luminance geometric mean 를 사용하여 Eye adaptation 을 적용합니다. 그리고 Luminance geometric mean 이 가질 수 있는 최대, 최소 값을 넘지 않도록 클램핑 해줍니다.
10. 마지막으로 ExposureScale 값을 만들어줍니다. (ExposureCompensationSettings / Luminance geometric mean) 을 저장합니다. 여기서 사용된 Luminance geometric mean 는 이전과 현재 프레임을 사용하여 Eye adaptation 이 적용된 것입니다. 이렇게 구해진 값은 SceneColor 에 그대로 곱하는 것으로 이미지의 Exposure 를 보정할 수 있습니다.
11. 마지막으로 최종 결과를 텍스쳐에 저장합니다. 1x1 텍스쳐기 때문에 (0, 0) 위치에 저장합니다. 한 번만 저장하면 되기 때문에 총 256 스레드 중 맨 첫 번째 스레드만 결과 텍스쳐 기록합니다.


그림8에서 봤던 ComputeWeightedTextureAverageAlphasubRegion 함수를 자세히 알아봅시다.
1. 입력받는 인자들은 w 채널에 log2(Luminance)를 갖고 있는 Texture, 그리고 SubRect 크기와 전체 Rect 의 크기입니다.
2. SubRect의 각 픽셀들을 UV 로 변환하기 위해서 InvRectWidth, InvRectHeight 를 구합니다. 그리고 Value 와 WeightTotal 을 선언합니다. Value 는 SubRect 내의 log2(Luminance) 의 총합, 그리고 WeightTotal 은 SubRect 내의 Weight 의 총합인데 이때 Weight 정보는 Metering Mask 에서 얻어옵니다. Metering Mask 는 이미지에서 ExposureScale 에 영향을 주는 범위를 Mask Texture 로 나타냅니다. 그림11을 참고해주세요.
3. 이제 SubRect 의 각 픽셀들을 계산합니다. 여기서는 Metering Mask Texture로부터 현재 픽셀의 가중치를 얻습니다.
4. Weight 값을 누적시킨 후, SceneColor 텍스쳐의 w 채널에 저장된 log2(Luminance)를 Weight 값과 곱해서 누적시킵니다. 이렇게 3, 4 과정을 SubRect 내의 모든 픽셀에 적용합니다.
5. 적용 결과를 float2 로 만들어 리턴하며, x 에는 log(Luminance) * Weight 의 총합을 그리고 y는 TotalWeight 를 저장하여 리턴합니다.


그림8에서 봤던 ComputeEyeAdaptation 함수를 자세히 알아봅시다. 이 함수는 ExposureScale 을 시간에 따라 서서히 변하게 해주는 Eye adaptation 을 처리합니다.
1. 여기서 Exposure 는 Luminance geometric mean 입니다. OldExposure 는 이전 프레임에 구한 것이고, TargetExposure 는 이번 프레임 구한 것입니다. Old 에서 Target 으로 보간은 log2 로 변환하여 작업하고, 마지막에 다시 exp2 를 사용하여 원래 값으로 되돌립니다. 그리고 LogDiff 를 구합니다.
2. LogDiff 변수를 사용하여 Adaptation Speed 를 결정합니다. 밝기가 올라갈 때와 내려갈 때 서로 다른 속도를 지정할 수 있습니다. AdaptationSpeed 의 경우 Linear 방식, M 의 경우 Exponent 방식으로 보간하는 데 사용하는 요소입니다. AbsLogDiff 도 구하는데, 이 값은 임계값(StartDistance) 값 보다 큰 경우 더 빠르게 Adaptation 이 진행되는 Linear 방식으로 처리할 수 있도록 하는데 사용됩니다.
3. Exponential, Linear 두 방식에 대한 보간을 모두 구합니다. 그리고 AbsLogDiff 가 임계값(StartDistance) 를 넘는 경우 Linear 그렇지 않는 경우 Exponential 방식의 Adaptation 을 선택합니다.
3-1. Linear 방식으로 Adaptation 진행 시 단순히 FrameTime * AdaptationSpeed 를 OldExposure에 더하거나 빼줍니다.
3-2. Exponential 방식의 경우 OldExposure 에 (TargetExposure - OldExposure) * Factor * M 의 식을 사용합니다. Factor는 그림13 의 그래프와 같이 TargetExposure 좀 더 서서히 근접하며, 이런 특성 때문에 FrameTime 출렁이더라도 Linear 보다는 안정적으로 TargetExposure 에 근접할 것입니다. M 값은 Scale 값이며 특수한 목적으로 추가로 곱해져 있습니다. M의 용도는 Linear 와 Exponential 사이에 전환을 부드럽게 해주는 것입니다.
3-3. 이 코드는 쉐이더가 아닌 CPU 측에서 계산되어 쉐이더의 ExponentialUpM, ExponentialDownM 에 전달되는 값입니다. 주석의 설명과 같이 임계값인 StartDistance 를 기준으로 Adaptation 방식인 Linear 와 Exponential 방식이 전환됩니다. 이 때 StartDistance 에서 Linear 와 Exponential 값 변화량이 크게 다르다면, StartDistance 를 지날 때(즉, Linear <-> Exponential 방식이 전환될 때) Expsoure 값이 눈에 띄게 변할 것입니다. 이런 부분을 완화해주는 M 값을 유도합니다. 주석의 가운데 식의 좌측항은 AdaptationSpeed * FrameTime 으로 Linear 방식이며, 우측은 Exponential 방식입니다. 그런데 3-2 의 쉐이더 코드와 다른 점은 (TargetExpsoure - OldExposure) 대신 AdaptationSpeed(속도) * StartT(시간) 를 사용하는 점입니다. 이것은 "속도=이동거리/시간" 식을 사용하여 (TargetExposure - OldExposure) 식을 분해한 것이며, 이것을 사용하여 Linear 식의 AdaptationSpeed 항을 상쇄시키는 것입니다. 이렇게 하여 M 값이 정해집니다.
4. Adaptation 을 처리한 후 다시 exp2 를 취해줘서 Luminance geometric mean 값으로 되돌립니다.
5. 이 부분은 Eye Adaptation 을 하지 않을때 ForceTarget 변수가 1이 됩니다. 현재는 Eye Adaptation 을 사용하므로 이 부분은 무시해도 됩니다.
6. 최종적으로 Eye Adaptation 을 적용한 결과를 리턴합니다.


3.5. EnequeueCopy(EyeAdaptation) 단계
EnequeueCopy(EyeAdaptation) 단계입니다. 앞에서 구한 ExposureScale 값을 Readback 하여 읽어 들입니다. 이 값은 다음 프레임의 PreExposure 에 사용됩니다.

3.6. Tonemapping 단계
이제 Tonemapping 과정에서 Exposure 를 적용하는 부분을 확인해봅시다.
1. Basic 방식에서는 EyeAdaptation Texture 의 x 값만 사용합니다. 이 부분의 값이 어떻게 이동하는지 확인해 봅시다. ExpsoureScaleVignette 에 ExposureScale 값이 저장됩니다. Eye Adaptation 1x1 Texture Fetch 는 단일 값이기 때문에 버택스 쉐이더에서 샘플링한 후 픽셀 쉐이더로 넘겨도 문제없을 것입니다. 그래서 TonemapCommonVS 에서 Fetch 하고 그 결과를 Pixel Shader 에서 사용하는 형태로 작업되었습니다.
2. TonemapCommonPS 로 이동합니다. ExpsoureScaleVignette 가 1번 과정에서 얻어온 값입니다.
3. USE_GAMMA_ONLY 를 사용하는 경우 SceneColor.rgb 에 ExposureScale 을 곱하여 노출 보정을 적용합니다.
4. 그렇지 않은 경우 LinearColor 에 ExposureScale 을 곱하여 노출 보정을 적용합니다.

4. 레퍼런스
1. https://github.com/EpicGames/UnrealEngine/commit/d9d435c9c280b99a6c679b517adedd3f4b02cfd7
2. https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/PostProcessEffects/AutomaticExposure/
3. Photographic Tone Reproduction For Digital Images
4. https://www.landonlehman.com/post/average-logarithms-and-the-geometric-mean/
5. https://knarkowicz.wordpress.com/2016/01/09/automatic-exposure/
6. https://en.wikipedia.org/wiki/Middle_gray
7. https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/sv-dispatchthreadid
8. https://docs.unrealengine.com/5.0/en-US/auto-exposure-in-unreal-engine/
9. https://en.wikipedia.org/wiki/Weighted_geometric_mean
'UE4 & UE5 > Rendering' 카테고리의 다른 글
| [UE5] D3D12 ResourceAllocation 리뷰 (1/2) (0) | 2023.08.23 |
|---|---|
| [UE5] Auto Exposure (2/2) - Histogram (0) | 2022.12.15 |
| [UE5] Occlusion Culling (0) | 2022.10.08 |
| [UE5] Dynamic Shadow (2/2) - Spot/Point/Rect light (1) | 2022.05.07 |
| [UE5] Dynamic Shadow (1/2) - Directional light(CSM) (1) | 2022.05.02 |