Skip to content

Instantly share code, notes, and snippets.

@nxrighthere
Last active April 23, 2024 23:22
Show Gist options
  • Save nxrighthere/eb208dae8b66dbe452af223f276e46cc to your computer and use it in GitHub Desktop.
Save nxrighthere/eb208dae8b66dbe452af223f276e46cc to your computer and use it in GitHub Desktop.
AgX tonemapping for Unreal Engine 5
// See image comparison https://imgur.com/a/9L2P7GJ
// Read details https://iolite-engine.com/blog_posts/minimal_agx_implementation
// Usage:
// 1. Open "Project Settings" and change "Working Color Space" to "sRGB / Rec709"
// 2. Open `Engine\Shaders\Private\PostProcessTonemap.usf` file
// 3. Find `half3 OutDeviceColor = ColorLookupTable(FinalLinearColor);` line
// 4. Replace it with `half3 OutDeviceColor = ApplyAgX(FinalLinearColor);` line
// 5. Find `half3 ColorLookupTable( half3 LinearColor )` function
// 6. After the scope of the function, add the code below and run `RecompileShaders Changed` from console
// 7. Restart the editor
// 8. Adjust "Exposure Compensation" under "Lens \ Exposure" in "Post Process Volume"
// AGX BEGIN
//
// MIT License
//
// Copyright (c) 2024 Missing Deadlines (Benjamin Wrensch)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// All values used to derive this implementation are sourced from Troy’s initial AgX implementation/OCIO config file available here:
// https://github.com/sobotka/AgX
// 0: Default, 1: Golden, 2: Punchy
#define AGX_LOOK 2
// Mean error^2: 3.6705141e-06
float3 AgxDefaultContrastApprox(float3 x)
{
const float3 x2 = x * x;
const float3 x4 = x2 * x2;
return + 15.5 * x4 * x2
- 40.14 * x4 * x
+ 31.96 * x4
- 6.868 * x2 * x
+ 0.4298 * x2
+ 0.1191 * x
- 0.00232;
}
float3 Agx(float3 val)
{
const float3x3 agx_mat = float3x3(
0.842479062253094, 0.0423282422610123, 0.0423756549057051,
0.0784335999999992, 0.878468636469772, 0.0784336,
0.0792237451477643, 0.0791661274605434, 0.879142973793104);
// DEFAULT_LOG2_MIN = -10.0
// DEFAULT_LOG2_MAX = +6.5
// MIDDLE_GRAY = 0.18
// log2(pow(2, VALUE) * MIDDLE_GRAY)
// Adjusted for Unreal's zero exposure compensation
const float min_ev = -12.47393f; // Default: -12.47393f;
const float max_ev = 0.526069f; // Default: 4.026069f;
// Input transform (inset)
val = mul(val, agx_mat);
// Log2 space encoding
val = clamp(log2(val), min_ev, max_ev);
val = (val - min_ev) / (max_ev - min_ev);
// Apply sigmoid function approximation
val = AgxDefaultContrastApprox(val);
return val;
}
float3 AgxEotf(float3 val)
{
const float3x3 agx_mat_inv = float3x3(
1.19687900512017, -0.0528968517574562, -0.0529716355144438,
-0.0980208811401368, 1.15190312990417, -0.0980434501171241,
-0.0990297440797205, -0.0989611768448433, 1.15107367264116);
// Inverse input transform (outset)
val = mul(val, agx_mat_inv);
// sRGB IEC 61966-2-1 2.2 Exponent Reference EOTF Display
// NOTE: We're linearizing the output here. Comment/adjust when
// *not* using a sRGB render target
val = pow(val, 2.2);
return val;
}
float3 AgxLook(float3 val)
{
const float3 lw = float3(0.2126, 0.7152, 0.0722);
const float luma = dot(val, lw);
// Default
const float3 offset = float3(0.0, 0.0, 0.0);
float3 slope = float3(1.0, 1.0, 1.0);
float3 power = float3(1.0, 1.0, 1.0);
float sat = 1.0;
#if AGX_LOOK == 1
// Golden
slope = float3(1.0, 0.9, 0.5);
power = float3(0.8, 0.8, 0.8);
sat = 0.8;
#elif AGX_LOOK == 2
// Punchy
slope = float3(1.0, 1.0, 1.0);
power = float3(1.35, 1.35, 1.35);
sat = 1.4;
#endif
// ASC CDL
val = pow(val * slope + offset, power);
return luma + sat * (val - luma);
}
half3 ApplyAgX(half3 LinearColorRec709)
{
LinearColorRec709 = Agx(LinearColorRec709);
LinearColorRec709 = AgxLook(LinearColorRec709);
LinearColorRec709 = AgxEotf(LinearColorRec709);
return LinearColorRec709;
}
// AGX END
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment