Minimal AgX Implementation
By Benjamin Wrensch
Summary: Contains a minimal implementation of Troy Sobotka's AgX display transform, which is easy to drop into any existing codebase - especially for testing purposes.
While working on the following more extensive blog post on color grading and while building a shadertoy in this context, I thought it would be nice to have a minimal version of Troy Sobotka's AgX [1] display transform that is easy to drop into any existing codebase - especially for testing purposes.
AgX's picture formation process works by applying the following steps:
- A matrix transformation on the input data (inset)
- An encoding to log2 space followed by the application of a sigmoid curve
- An optional ASC-CDL-based look transform; I've included the default looks "Golden" and "Punchy"
- The inverse of the input matrix transform (outset) followed by the fitting EOTF
The OCIO reference implementation of AgX uses a lookup table for the sigmoid curve, which is backed by a relatively complex tunable function. I replaced the lookup table and function with a simple polynomial approximation to limit the code footprint to a minimum.
The following graphic shows a comparison between the approximation and the reference.
Close enough, in my opinion, with a mean squared error of roughly 3.6705141e-06. If you want to get even closer, I've added a 7th-order variant with an MSE of around 1.85907662e-06 to the appendix of this post.
You can grab the whole source of the implementation from the following shadertoy [2]:
The code is also available in the appendix of this post - easy to understand and integrate.
Enjoy!
Appendix
AgX Minimal Source
// 0: Default, 1: Golden, 2: Punchy #define AGX_LOOK 0 // Mean error^2: 3.6705141e-06 vec3 agxDefaultContrastApprox(vec3 x) { vec3 x2 = x * x; vec3 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; } vec3 agx(vec3 val) { const mat3 agx_mat = mat3( 0.842479062253094, 0.0423282422610123, 0.0423756549057051, 0.0784335999999992, 0.878468636469772, 0.0784336, 0.0792237451477643, 0.0791661274605434, 0.879142973793104); const float min_ev = -12.47393f; const float max_ev = 4.026069f; // Input transform (inset) val = agx_mat * val; // 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; } vec3 agxEotf(vec3 val) { const mat3 agx_mat_inv = mat3( 1.19687900512017, -0.0528968517574562, -0.0529716355144438, -0.0980208811401368, 1.15190312990417, -0.0980434501171241, -0.0990297440797205, -0.0989611768448433, 1.15107367264116); // Inverse input transform (outset) val = agx_mat_inv * val; // sRGB IEC 61966-2-1 2.2 Exponent Reference EOTF Display val = pow(val, vec3(2.2)); return val; } vec3 agxLook(vec3 val) { const vec3 lw = vec3(0.2126, 0.7152, 0.0722); float luma = dot(val, lw); // Default vec3 offset = vec3(0.0); vec3 slope = vec3(1.0); vec3 power = vec3(1.0); float sat = 1.0; #if AGX_LOOK == 1 // Golden slope = vec3(1.0, 0.9, 0.5); power = vec3(0.8); sat = 0.8; #elif AGX_LOOK == 2 // Punchy slope = vec3(1.0); power = vec3(1.35, 1.35, 1.35); sat = 1.4; #endif // ASC CDL val = pow(val * slope + offset, power); return luma + sat * (val - luma); } // Sample usage void main(/*...*/) { // ... value = agx(value); value = agxLook(value); // Optional value = agxEotf(value); // ... }
7th Order Polynomial Approximation
// Mean error^2: 1.85907662e-06 vec3 agxDefaultContrastApprox(vec3 x) { vec3 x2 = x * x; vec3 x4 = x2 * x2; vec3 x6 = x4 * x2; return - 17.86 * x6 * x + 78.01 * x6 - 126.7 * x4 * x + 92.06 * x4 - 28.72 * x2 * x + 4.361 * x2 - 0.1718 * x + 0.002857; }
Further Reading
- [1] AgX by Troy Sobotka
https://github.com/sobotka/AgX - [2] AgX Minimal Shadertoy
https://www.shadertoy.com/view/cd3XWr
Changelog
- No changes (yet)