【目标】
UE4的Tonemapper
【思路】
1 UE3中关键字
- USE_TONEMAPPERTYPE
- ShadersUberPostProcessBlendPixelShader.usf
- TonemapAndGammaCorrect
2 UE3的算法

half3 TonemapAndGammaCorrect(half3 LinearColor){ half A = (half)ImageAdjustments2.x; half B = (half)ImageAdjustments2.y;
half3 GammaColor;
#if USE_TONEMAPPERTYPE == 0 // no tonemapper { GammaColor = pow(LinearColor, 1.0f / 2.2f); } #elif USE_TONEMAPPERTYPE == 1 // filmic approximation (s-curve, contrast enhances, small toe, changes saturation and hue) // simple but effective tonemapper // outputs in 0..1 range (saturate()) // clamps negative colors to 0 (abs()) { GammaColor = LinearColor / abs(LinearColor + A) * B; }
#elif USE_TONEMAPPERTYPE == 2 // neutral soft white clamp (experimental, not optimized yet) // outputs in 0..1 range (saturate()) // clamps negataive colors to 0 (abs()) { // ToeFactor 0 = non means linear .. 1 = full (filmic look) half ToeFactor = (half)ImageAdjustments3.x; half LinearSteepness = (half)ImageAdjustments2.w; // value below we have linear adjustments to the sigmoid curve that we don't to above the value half FunctionSplitPos = ImageAdjustments2.z;
half3 GammaColorOldTonemapperTonemapperStartingFromLinear = LinearColor / abs(LinearColor + A) * B;
// efficient 3 component implementation of: LinearColor >= FunctionSplitPos half3 SplitMask = saturate((LinearColor - FunctionSplitPos) * 10000.0f);
const half3 GammaColorNotTonemapped = pow(LinearColor * LinearSteepness, 1.0f / 2.2f);
half3 FlatGammaColor = lerp(GammaColorNotTonemapped, GammaColorOldTonemapperTonemapperStartingFromLinear, SplitMask);
GammaColor = lerp(FlatGammaColor, GammaColorOldTonemapperTonemapperStartingFromLinear, ToeFactor); }#endif
// in all cases it's good to clamp into the 0..1 range (e.g for LUT color grading) GammaColor = saturate(GammaColor);
return GammaColor;}
变量定义
// xy=Grain Offset to have animated noise, z=unused, w=ImageGrainScale// Note: ImageAdjustments1.xy is a random float2 in the range 0..1float4 ImageAdjustments1;// TonemapperFunction(x) = x / (x + A) * B// x=A, y=B, z=split pos, w=LinearSteepnessfloat4 ImageAdjustments2;// x=tonemapper toe factor (0=linear..1=full toe)float4 ImageAdjustments3;
3 UE4相关
ACES Gets in the Games
The Motion Picture Academy’s Academy Color Encoding System is now widely used across the movie industry but perhaps surprisingly, it’s also gaining traction in the gaming world. To learn more, Digital Cinema Report recently spoke with Brian Karis at Cary, North Carolina-based Epic Games. The company is known for its industry-leading Unreal Engine, a ground breaking game engine that can be used by other game developers who do not want to develop their own engine.
Digital Cinema Report: Tell us what your role at Epic Games is?
Brian Karis: I'm a senior graphics programmer on the rendering team here at Epic. I work on many different aspects of the renderer.
DCR: What made you decide to incorporate ACES?
BK: A couple of years ago I tackled changing our tone mapper from a proprietary ad hoc curve with some artist parameters to a new curve that better simulated film response and could be configured to fit a number of different film responses. During that effort I learned a ton about color science and physical film. I also came upon ACES and decided it should be the foundation for our color handling. Right now, we do all rendering in Rec. 709 space but do the grading in ACEScg and then use an Output Transform. I ended up creating a parametric curve that combines the Reference Rendering Transform (RRT) and Output Device Transform (ODT) for 100 nit monitors that allows the artist to get different film stock looks than the ACES standard look but our defaults match ACES. So currently the default tone mapper and color grading in Unreal Engine 4 (UE4) is ACES.
DCR: What advantages do you see in using ACES within a gaming engine?
BK: What advantages there are is a large question but I'll touch on a few things. In my opinion the biggest impact is from doing tone mapping in ACEScg space. Regardless of the curve, that alone makes colors look so much better and more realistic. The specific curve of ACES matches film characteristics but honestly it is just a choice of look amongst many options. Having the RRT and ODT separated is a really good idea and one I wish I had embraced earlier. If I had, I could have had my parametric curve work for HDR displays as well. That now needs a bit of a rethink. Matching a standard that we can load up in Nuke or other packages has proven useful so far and I expect will be ever more useful in the future as UE4 becomes more used by film and VFX companies.
For HDR displays we use the ACES ODTs and disable those controls. I'd like to make that work in the future. We are also interested in adding support for rendering in ACEScg space. I recently engaged with the larger ACES community in helping write a retrospective and suggestions for improvements for future versions.
4 youtube介绍
https://www.youtube.com/watch?v=A-wectYNfRQ
Filmic Tonemapper | Feature Highlight | Unreal Engine










5 UE4 Shader流向
TonemapCommon.usf FilmSlope ->FLUTBlenderPS.FilmSlope
FilmToneMap函数
/*============================================// Uncharted settingsSlope = 0.63;Toe = 0.55;Shoulder = 0.47;BlackClip= 0;WhiteClip = 0.01;
// HP settingsSlope = 0.65;Toe = 0.63;Shoulder = 0.45;BlackClip = 0;WhiteClip = 0;
// Legacy settingsSlope = 0.98;Toe = 0.3;Shoulder = 0.22;BlackClip = 0;WhiteClip = 0.025;
// ACES settingsSlope = 0.88;Toe = 0.55;Shoulder = 0.26;BlackClip = 0;WhiteClip = 0.04;===========================================*/
float FilmSlope;float FilmToe;float FilmShoulder;float FilmBlackClip;float FilmWhiteClip;
half3 FilmToneMap( half3 LinearColor ) { const float3x3 sRGB_2_AP0 = mul( XYZ_2_AP0_MAT, mul( D65_2_D60_CAT, sRGB_2_XYZ_MAT ) ); const float3x3 sRGB_2_AP1 = mul( XYZ_2_AP1_MAT, mul( D65_2_D60_CAT, sRGB_2_XYZ_MAT ) ); const float3x3 AP1_2_sRGB = mul( XYZ_2_sRGB_MAT, mul( D60_2_D65_CAT, AP1_2_XYZ_MAT ) ); #if !ES2_PROFILE //disabling this part for mobile, Adreno devices can't handle it (UE-40689) float3 ACESColor = mul( sRGB_2_AP0, float3(LinearColor) );
// --- Red modifier --- // const float RRT_RED_SCALE = 0.82; const float RRT_RED_PIVOT = 0.03; const float RRT_RED_HUE = 0; const float RRT_RED_WIDTH = 135;
float saturation = rgb_2_saturation( ACESColor ); float hue = rgb_2_hue( ACESColor ); float centeredHue = center_hue( hue, RRT_RED_HUE ); float hueWeight = Square( smoothstep( 0, 1, 1 - abs( 2 * centeredHue / RRT_RED_WIDTH ) ) ); ACESColor.r += hueWeight * saturation * (RRT_RED_PIVOT - ACESColor.r) * (1. - RRT_RED_SCALE);
// Use ACEScg primaries as working space float3 WorkingColor = mul( AP0_2_AP1_MAT, ACESColor );#else // Use ACEScg primaries as working space float3 WorkingColor = mul( sRGB_2_AP1, float3(LinearColor) );#endif
WorkingColor = max( 0, WorkingColor );
// Pre desaturate WorkingColor = lerp( dot( WorkingColor, AP1_RGB2Y ), WorkingColor, 0.96 ); const half ToeScale = 1 + FilmBlackClip - FilmToe; const half ShoulderScale = 1 + FilmWhiteClip - FilmShoulder; const float InMatch = 0.18; const float OutMatch = 0.18;
float ToeMatch; if( FilmToe > 0.8 ) { // 0.18 will be on straight segment ToeMatch = ( 1 - FilmToe - OutMatch ) / FilmSlope + log10( InMatch ); } else { // 0.18 will be on toe segment
// Solve for ToeMatch such that input of InMatch gives output of OutMatch. const float bt = ( OutMatch + FilmBlackClip ) / ToeScale - 1; ToeMatch = log10( InMatch ) - 0.5 * log( (1+bt)/(1-bt) ) * (ToeScale / FilmSlope); }
float StraightMatch = ( 1 - FilmToe ) / FilmSlope - ToeMatch; float ShoulderMatch = FilmShoulder / FilmSlope - StraightMatch; half3 LogColor = log10( WorkingColor ); half3 StraightColor = FilmSlope * ( LogColor + StraightMatch ); half3 ToeColor = ( -FilmBlackClip ) + (2 * ToeScale) / ( 1 + exp( (-2 * FilmSlope / ToeScale) * ( LogColor - ToeMatch ) ) ); half3 ShoulderColor = ( 1 + FilmWhiteClip ) - (2 * ShoulderScale) / ( 1 + exp( ( 2 * FilmSlope / ShoulderScale) * ( LogColor - ShoulderMatch ) ) );
ToeColor = LogColor < ToeMatch ? ToeColor : StraightColor; ShoulderColor = LogColor > ShoulderMatch ? ShoulderColor : StraightColor;
half3 t = saturate( ( LogColor - ToeMatch ) / ( ShoulderMatch - ToeMatch ) ); t = ShoulderMatch < ToeMatch ? 1 - t : t; t = (3-2*t)*t*t; half3 ToneColor = lerp( ToeColor, ShoulderColor, t );
// Post desaturate ToneColor = lerp( dot( float3(ToneColor), AP1_RGB2Y ), ToneColor, 0.93 );
// Returning positive AP1 values return max( 0, ToneColor );
//ToneColor = mul( AP1_2_sRGB, ToneColor );
//return saturate( ToneColor ); //return max( 0, ToneColor );}
6 UnrealEngine4EngineShadersACES.usf 定义一些要用到的常量
https://github.com/ampas/aces-dev/tree/v1.0
UnrealEngine4EngineShadersPostProcessTonemap.usf
UnrealEngine4EngineShadersTonemapCommon.usf
7 计算流程
- PostProcessCombineLUTs.usf MainPS
8 https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
【步骤】
1 修改 EngineShadersUberPostProcessBlendPixelShader.usf
elif USE_TONEMAPPERTYPE == 3 { //float a = 2.51f; //float b = 0.03f; //float c = 2.43f; //float d = 0.59f; //float e = 0.14f; //return saturate((x*(a*x+b))/(x*(c*x+d)+e)); GammaColor = saturate((LinearColor*(2.51f*LinearColor+0.03f))/(LinearColor*(2.43f*LinearColor+0.59f)+0.14f)); }
2 修改DevelopmentSrcEngineClassesUberPostProcessEffect.uc 添加类型
/** Allows to specify the tone mapper function which maps HDR colors into the LDR color range. */var(Tonemapper) enum ETonemapperType{ Tonemapper_Off<DisplayName=Off>, Tonemapper_ACES<DisplayName=ACES>,
Tonemapper_Customizable<DisplayName=Customizable>, Tonemapper_Filmic<DisplayName=Filmic>,
} TonemapperType;
3 FUberPostProcessSceneProxy.Render
#define VARIATION1(A) VARIATION2(A,0) VARIATION2(A,1) VARIATION2(A,2) VARIATION2(A,3)
4 UberPostProcessEffect.cpp
#define VARIATION1(A) VARIATION2(A,0) VARIATION2(A,1) VARIATION2(A,2) VARIATION2(A,3)
运行 ACES效果

5 UE4算法
#include "ACES.usf"
...
EngineShadersUberPostProcessBlendPixelShader.usf
【运行】

8
FUberPostProcessSceneProxy.Render
#define VARIATION1(A) VARIATION2(A,0) VARIATION2(A,1) VARIATION2(A,2) VARIATION2(A,3) VARIATION2(A,4) VARIATION2(A,5) VARIATION2(A,6) VARIATION2(A,7)
UberPostProcessEffect.cpp
#define VARIATION1(A) VARIATION2(A,0) VARIATION2(A,1) VARIATION2(A,2) VARIATION2(A,3) VARIATION2(A,4) VARIATION2(A,5) VARIATION2(A,6) VARIATION2(A,7)
语法报错嵌套太深 感觉超过48层就报错了
只能采用传另外一个参数
ImageAdjustments3只用的第一个Float
9 FUberPostProcessSceneProxy 中添加属性
UINT TonemapperACESType;
FUberPostProcessSceneProxy.FUberPostProcessSceneProxy
FUberPostProcessSceneProxy(const UUberPostProcessEffect* InEffect,const FPostProcessSettings* WorldSettings, UINT InColorGradingCVar, UINT InTonemapperType,UINT InTonemapperACESType, UBOOL bInMotionBlur, UBOOL bInImageGrain,const FCameraInfo& CI) : FDOFAndBloomPostProcessSceneProxy(InEffect, WorldSettings,CI) , MotionBlurSoftEdgeKernelSize(InEffect->MotionBlurSoftEdgeKernelSize) , TonemapperToeFactor(InEffect->TonemapperToeFactor) , TonemapperRange(InEffect->TonemapperRange) , ColorGradingCVar(InColorGradingCVar) , TonemapperType(InTonemapperType) , TonemapperACESType(InTonemapperACESType)
10 ue3DevelopmentSrcEngineClassesUberPostProcessEffect.uc
var(Tonemapper) enum EACESType{ ACES_One<DisplayName=ACES>, ACES_Simple<DisplayName=Simple>, ACES_Uncharted<DisplayName=Uncharted>, ACES_HP<DisplayName=HP>, ACES_Legacy<DisplayName=Legacy>, } ACESType;
11 UUberPostProcessEffect.CreateSceneProxy
return new FUberPostProcessSceneProxy(this, WorldSettings, GColorGrading, LocalTonemapperType,ACESType, bLocalMotionBlur, bEnableImageGrain,CI);
12 Shader赋值FUberPostProcessSceneProxy.RenderVariationFullRes.{.SetPixelShaderValue
SetPixelShaderValue( BlendPixelShader->GetPixelShader(), BlendPixelShader->ImageAdjustments3Parameter, FVector4(Clamp(TonemapperToeFactor, 0.0f, 1.0f), TonemapperACESType, 0, 0));
13 EngineShadersUberPostProcessBlendPixelShader.usf
#elif USE_TONEMAPPERTYPE == 3 {
if (ACESType == 1) { // Simple ACES //float a = 2.51f; //float b = 0.03f; //float c = 2.43f; //float d = 0.59f; //float e = 0.14f; //return saturate((x*(a*x+b))/(x*(c*x+d)+e)); GammaColor = saturate(( LinearColor * (2.51f * LinearColor + 0.03f )) / ( LinearColor * ( 2.43f * LinearColor + 0.59f ) + 0.14f )); } else { if (ACESType == 0) { // ACES settings FilmSlope = 0.88; FilmToe = 0.55; FilmShoulder = 0.26; FilmBlackClip = 0; FilmWhiteClip = 0.04; } else if (ACESType == 2) { // Uncharted settings FilmSlope = 0.63; FilmToe = 0.55; FilmShoulder = 0.47; FilmBlackClip= 0; FilmWhiteClip = 0.01; } else if (ACESType == 3) { // HP settings FilmSlope = 0.65; FilmToe = 0.63; FilmShoulder = 0.45; FilmBlackClip = 0; FilmWhiteClip = 0; } else if (ACESType == 4) { // Legacy settings FilmSlope = 0.98; FilmToe = 0.3; FilmShoulder = 0.22; FilmBlackClip = 0; FilmWhiteClip = 0.025; } GammaColor = FilmToneMap(LinearColor); } }#endif
14 添加指令
UUberPostProcessEffect.CreateSceneProxy.
CVar = GConsoleManager->FindConsoleVariable(TEXT("ACESType")); Value = CVar->GetInt(); if (Value >= ACES_One && Value < ACES_MAX) { LocalACESType = Value; }
CreateConsoleVariables
GConsoleManager->RegisterConsoleVariable(TEXT("ACESType"), -1, TEXT("Allows to override which ACESType function (during the post processing stage to transform HDR to LDR colors) is used:
") TEXT("-1: use what is specified elsewhere (default)
") TEXT(" 0: ACES settings
") TEXT(" 1: Simple ACES
") TEXT(" 2: Uncharted settings
") TEXT(" 3: HP settings"), TEXT(" 4: Legacy settings"), ECVF_Cheat);
指令
" 0: ACES settings"
" 1: Simple ACES"
" 2: Uncharted settings"
" 3: HP settings"
" 4: Legacy settings"