Skip to content

Commit

Permalink
add HDR Output Transforms (and inverses) and their supporting ACESlib…
Browse files Browse the repository at this point in the history
… functions

* add `ACESlib.SSTS.ctl` which contains the code for the Single Stage Tone Scale function used in the Output Transforms
* add `ACESlib.OutputTransforms.ctl` which contains the modules needed for the parameterized Output Transforms
* update `ACESlib.RRT_Common.ctl` with rrt_sweeteners() subfunction which allows a single function call to from Output Transforms to get a match to the current RRT code that is not the tone scale itself
* add a few examples of parameterized Output Transforms
    - Dolby Cinema: P3D65, 108nit peak luminance, 7.2nit mid-gray luminance, PQ encoded
    - HLG 1000nit: Rec2020, 1000nit peak luminance, 15nit mid-gray luminance, HLG encoded
    - PQ 1000nit: Rec2020, 1000nit peak luminance, 15nit mid-gray luminance, PQ encoded
    - PQ 2000nit: Rec2020, 2000nit peak luminance, 15nit mid-gray luminance, PQ encoded
    - PQ 4000nit: Rec2020, 4000nit peak luminance, 15nit mid-gray luminance, PQ encoded
  • Loading branch information
scottdyer committed Jun 21, 2018
1 parent e40c920 commit 5d198cb
Show file tree
Hide file tree
Showing 13 changed files with 1,647 additions and 0 deletions.
365 changes: 365 additions & 0 deletions transforms/ctl/lib/ACESlib.OutputTransforms.ctl
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@

// <ACEStransformID>ACESlib.OutputTransforms.a1.1</ACEStransformID>
// <ACESuserName>ACES 1.0 Lib - Output Transforms</ACESuserName>

//
// Contains functions used for forward and inverse Output Transforms (RRT+ODT)
//



import "ACESlib.Transform_Common";
import "ACESlib.RRT_Common";
import "ACESlib.ODT_Common";
import "ACESlib.SSTS";



float[3] limit_to_primaries
(
float XYZ[3],
Chromaticities LIMITING_PRI
)
{
float XYZ_2_LIMITING_PRI_MAT[4][4] = XYZtoRGB( LIMITING_PRI, 1.0);
float LIMITING_PRI_2_XYZ_MAT[4][4] = RGBtoXYZ( LIMITING_PRI, 1.0);

// XYZ to limiting primaries
float rgb[3] = mult_f3_f44( XYZ, XYZ_2_LIMITING_PRI_MAT);

// Clip any values outside the limiting primaries
float limitedRgb[3] = clamp_f3( rgb, 0., 1.);

// Convert limited RGB to XYZ
return mult_f3_f44( limitedRgb, LIMITING_PRI_2_XYZ_MAT);
}

float[3] dark_to_dim( float XYZ[3])
{
float xyY[3] = XYZ_2_xyY(XYZ);
xyY[2] = clamp( xyY[2], 0., HALF_POS_INF);
xyY[2] = pow( xyY[2], DIM_SURROUND_GAMMA);
return xyY_2_XYZ(xyY);
}

float[3] dim_to_dark( float XYZ[3])
{
float xyY[3] = XYZ_2_xyY(XYZ);
xyY[2] = clamp( xyY[2], 0., HALF_POS_INF);
xyY[2] = pow( xyY[2], 1./DIM_SURROUND_GAMMA);
return xyY_2_XYZ(xyY);
}

float[3] outputTransform
(
float in[3],
float Y_MIN,
float Y_MID,
float Y_MAX,
Chromaticities DISPLAY_PRI,
Chromaticities LIMITING_PRI,
int EOTF,
int SURROUND,
bool STRETCH_BLACK = true,
bool D60_SIM = false,
bool LEGAL_RANGE = false
)
{
float XYZ_2_DISPLAY_PRI_MAT[4][4] = XYZtoRGB( DISPLAY_PRI, 1.0);

/*
NOTE: This is a bit of a hack - probably a more direct way to do this.
TODO: Fix in future version
*/
TsParams PARAMS_DEFAULT = init_TsParams( Y_MIN, Y_MAX);
float expShift = log2(inv_ssts(Y_MID, PARAMS_DEFAULT))-log2(0.18);
TsParams PARAMS = init_TsParams( Y_MIN, Y_MAX, expShift);

// RRT sweeteners
float rgbPre[3] = rrt_sweeteners( in);

// Apply the tonescale independently in rendering-space RGB
float rgbPost[3] = ssts_f3( rgbPre, PARAMS);

// At this point data encoded AP1, scaled absolute luminance (cd/m^2)

/* Scale absolute luminance to linear code value */
float linearCV[3] = Y_2_linCV_f3( rgbPost, Y_MAX, Y_MIN);

// Rendering primaries to XYZ
float XYZ[3] = mult_f3_f44( linearCV, AP1_2_XYZ_MAT);

// Apply gamma adjustment to compensate for dim surround
/*
NOTE: This is more or less a placeholder block and is largely inactive
in its current form. This section currently only applies for SDR, and
even then, only in very specific cases.
In the future it is fully intended for this module to be updated to
support surround compensation regardless of luminance dynamic range. */
/*
TOD0: Come up with new surround compensation algorithm, applicable
across all dynamic ranges and supporting dark/dim/normal surround.
*/
if (SURROUND == 0) { // Dark surround
/*
Current tone scale is designed for dark surround environment so no
adjustment is necessary.
*/
} else if (SURROUND == 1) { // Dim surround
// INACTIVE for HDR and crudely implemented for SDR (see comment below)
if ((EOTF == 1) || (EOTF == 2) || (EOTF == 3)) {
/*
This uses a crude logical assumption that if the EOTF is BT.1886,
sRGB, or gamma 2.6 that the data is SDR and so the SDR gamma
compensation factor from v1.0 will apply.
*/
XYZ = dark_to_dim( XYZ); /*
This uses a local dark_to_dim function that is designed to take in
XYZ and return XYZ rather than AP1 as is currently in the functions
in 'ACESlib.ODT_Common.ctl' */
}
} else if (SURROUND == 2) { // Normal surround
// INACTIVE - this does NOTHING
}

// Gamut limit to limiting primaries
// NOTE: Would be nice to just say
// if (LIMITING_PRI != DISPLAY_PRI)
// but you can't because Chromaticities do not work with bool comparison operator
// For now, limit no matter what.
XYZ = limit_to_primaries( XYZ, LIMITING_PRI);

// Apply CAT from ACES white point to assumed observer adapted white point
// TODO: Needs to expand from just supporting D60 sim to allow for any
// observer adapted white point.
if (D60_SIM == false) {
if ((DISPLAY_PRI.white[0] != AP0.white[0]) &
(DISPLAY_PRI.white[1] != AP0.white[1])) {
float CAT[3][3] = calculate_cat_matrix( AP0.white, DISPLAY_PRI.white);
XYZ = mult_f3_f33( XYZ, D60_2_D65_CAT);
}
}

// CIE XYZ to display encoding primaries
linearCV = mult_f3_f44( XYZ, XYZ_2_DISPLAY_PRI_MAT);

// Scale to avoid clipping when device calibration is different from D60.
// To simulate D60, unequal code values are sent to the display.
// TODO: Needs to expand from just supporting D60 sim to allow for any
// observer adapted white point.
if (D60_SIM == true) {
/* TODO: The scale requires calling itself. Scale is same no matter the luminance.
Currently precalculated for D65, DCI. If DCI, roll_white_fwd is used also.
This needs a more complex algorithm to handle all cases.
*/
float SCALE = 1.0;
if ((DISPLAY_PRI.white[0] == 0.3127) &
(DISPLAY_PRI.white[1] == 0.329)) { // D65
SCALE = 0.96362;
}
else if ((DISPLAY_PRI.white[0] == 0.314) &
(DISPLAY_PRI.white[1] == 0.351)) { // DCI
linearCV[0] = roll_white_fwd( linearCV[0], 0.918, 0.5);
linearCV[1] = roll_white_fwd( linearCV[1], 0.918, 0.5);
linearCV[2] = roll_white_fwd( linearCV[2], 0.918, 0.5);
SCALE = 0.96;
}
linearCV = mult_f_f3( SCALE, linearCV);
}


// Clip values < 0 (i.e. projecting outside the display primaries)
// NOTE: P3 red and values close to it fall outside of Rec.2020 green-red
// boundary
linearCV = clamp_f3( linearCV, 0., HALF_POS_INF);

// EOTF
// 0: ST-2084 (PQ)
// 1: BT.1886 (Rec.709/2020 settings)
// 2: sRGB (mon_curve w/ presets)
// moncurve_r with gamma of 2.4 and offset of 0.055 matches the EOTF found in IEC 61966-2-1:1999 (sRGB)
// 3: gamma 2.6
// 4: linear (no EOTF)
// 5: HLG
float outputCV[3];
if (EOTF == 0) { // ST-2084 (PQ)
// NOTE: This is a kludgy way of ensuring a PQ code value of 0. Ideally,
// luminance would map directly to code value, but colorists don't like
// that. Might just need the tonescale to go darker so that darkest
// values through the tone scale quantize to code value of 0.
if (STRETCH_BLACK == true) {
outputCV = Y_2_ST2084_f3( clamp_f3( linCV_2_Y_f3(linearCV, Y_MAX, 0.0), 0.0, HALF_POS_INF) );
} else {
outputCV = Y_2_ST2084_f3( linCV_2_Y_f3(linearCV, Y_MAX, Y_MIN) );
}
} else if (EOTF == 1) { // BT.1886 (Rec.709/2020 settings)
outputCV = bt1886_r_f3( linearCV, 2.4, 1.0, 0.0);
} else if (EOTF == 2) { // sRGB (mon_curve w/ presets)
outputCV = moncurve_r_f3( linearCV, 2.4, 0.055);
} else if (EOTF == 3) { // gamma 2.6
outputCV = pow_f3( linearCV, 1./2.6);
} else if (EOTF == 4) { // linear
outputCV = linCV_2_Y_f3(linearCV, Y_MAX, Y_MIN);
} else if (EOTF == 5) { // HLG
// NOTE: HLG just maps ST.2084 output to HLG encoding.
// TODO: Restructure if/else tree to minimize this redundancy.
if (STRETCH_BLACK == true) {
outputCV = Y_2_ST2084_f3( clamp_f3( linCV_2_Y_f3(linearCV, Y_MAX, 0.0), 0.0, HALF_POS_INF) );
}
else {
outputCV = Y_2_ST2084_f3( linCV_2_Y_f3(linearCV, Y_MAX, Y_MIN) );
}
outputCV = ST2084_2_HLG_1000nits_f3( outputCV);
}

if (LEGAL_RANGE == true) {
outputCV = fullRange_to_smpteRange_f3( outputCV);
}

return outputCV;
}

float[3] invOutputTransform
(
float in[3],
float Y_MIN,
float Y_MID,
float Y_MAX,
Chromaticities DISPLAY_PRI,
Chromaticities LIMITING_PRI,
int EOTF,
int SURROUND,
bool STRETCH_BLACK = true,
bool D60_SIM = false,
bool LEGAL_RANGE = false
)
{
float DISPLAY_PRI_2_XYZ_MAT[4][4] = RGBtoXYZ( DISPLAY_PRI, 1.0);

/*
NOTE: This is a bit of a hack - probably a more direct way to do this.
TODO: Update in accordance with forward algorithm.
*/
TsParams PARAMS_DEFAULT = init_TsParams( Y_MIN, Y_MAX);
float expShift = log2(inv_ssts(Y_MID, PARAMS_DEFAULT))-log2(0.18);
TsParams PARAMS = init_TsParams( Y_MIN, Y_MAX, expShift);

float outputCV[3] = in;

if (LEGAL_RANGE == true) {
outputCV = smpteRange_to_fullRange_f3( outputCV);
}

// Inverse EOTF
// 0: ST-2084 (PQ)
// 1: BT.1886 (Rec.709/2020 settings)
// 2: sRGB (mon_curve w/ presets)
// moncurve_r with gamma of 2.4 and offset of 0.055 matches the EOTF found in IEC 61966-2-1:1999 (sRGB)
// 3: gamma 2.6
// 4: linear (no EOTF)
// 5: HLG
float linearCV[3];
if (EOTF == 0) { // ST-2084 (PQ)
if (STRETCH_BLACK == true) {
linearCV = Y_2_linCV_f3( ST2084_2_Y_f3( outputCV), Y_MAX, 0.);
} else {
linearCV = Y_2_linCV_f3( ST2084_2_Y_f3( outputCV), Y_MAX, Y_MIN);
}
} else if (EOTF == 1) { // BT.1886 (Rec.709/2020 settings)
linearCV = bt1886_f_f3( outputCV, 2.4, 1.0, 0.0);
} else if (EOTF == 2) { // sRGB (mon_curve w/ presets)
linearCV = moncurve_f_f3( outputCV, 2.4, 0.055);
} else if (EOTF == 3) { // gamma 2.6
linearCV = pow_f3( outputCV, 2.6);
} else if (EOTF == 4) { // linear
linearCV = Y_2_linCV_f3( outputCV, Y_MAX, Y_MIN);
} else if (EOTF == 5) { // HLG
outputCV = HLG_2_ST2084_1000nits_f3( outputCV);
if (STRETCH_BLACK == true) {
linearCV = Y_2_linCV_f3( ST2084_2_Y_f3( outputCV), Y_MAX, 0.);
} else {
linearCV = Y_2_linCV_f3( ST2084_2_Y_f3( outputCV), Y_MAX, Y_MIN);
}
}

// Un-scale
if (D60_SIM == true) {
/* TODO: The scale requires calling itself. Need an algorithm for this.
Scale is same no matter the luminance.
Currently using precalculated values for D65, DCI.
If DCI, roll_white_fwd is used also.
*/
float SCALE = 1.0;
if ((DISPLAY_PRI.white[0] == 0.3127) &
(DISPLAY_PRI.white[1] == 0.329)) { // D65
SCALE = 0.96362;
linearCV = mult_f_f3( 1./SCALE, linearCV);
}
else if ((DISPLAY_PRI.white[0] == 0.314) &
(DISPLAY_PRI.white[1] == 0.351)) { // DCI
SCALE = 0.96;
linearCV[0] = roll_white_rev( linearCV[0]/SCALE, 0.918, 0.5);
linearCV[1] = roll_white_rev( linearCV[1]/SCALE, 0.918, 0.5);
linearCV[2] = roll_white_rev( linearCV[2]/SCALE, 0.918, 0.5);
}

}

// Encoding primaries to CIE XYZ
float XYZ[3] = mult_f3_f44( linearCV, DISPLAY_PRI_2_XYZ_MAT);

// Undo CAT from assumed observer adapted white point to ACES white point
if (D60_SIM == false) {
if ((DISPLAY_PRI.white[0] != AP0.white[0]) &
(DISPLAY_PRI.white[1] != AP0.white[1])) {
float CAT[3][3] = calculate_cat_matrix( AP0.white, DISPLAY_PRI.white);
XYZ = mult_f3_f33( XYZ, invert_f33(D60_2_D65_CAT) );
}
}

// Apply gamma adjustment to compensate for dim surround
/*
NOTE: This is more or less a placeholder block and is largely inactive
in its current form. This section currently only applies for SDR, and
even then, only in very specific cases.
In the future it is fully intended for this module to be updated to
support surround compensation regardless of luminance dynamic range. */
/*
TOD0: Come up with new surround compensation algorithm, applicable
across all dynamic ranges and supporting dark/dim/normal surround.
*/
if (SURROUND == 0) { // Dark surround
/*
Current tone scale is designed for dark surround environment so no
adjustment is necessary.
*/
} else if (SURROUND == 1) { // Dim surround
// INACTIVE for HDR and crudely implemented for SDR (see comment below)
if ((EOTF == 1) || (EOTF == 2) || (EOTF == 3)) {
/*
This uses a crude logical assumption that if the EOTF is BT.1886,
sRGB, or gamma 2.6 that the data is SDR and so the SDR gamma
compensation factor from v1.0 will apply.
*/
XYZ = dim_to_dark( XYZ); /*
This uses a local dim_to_dark function that is designed to take in
XYZ and return XYZ rather than AP1 as is currently in the functions
in 'ACESlib.ODT_Common.ctl' */
}
} else if (SURROUND == 2) { // Normal surround
// INACTIVE - this does NOTHING
}

// XYZ to rendering primaries
linearCV = mult_f3_f44( XYZ, XYZ_2_AP1_MAT);

float rgbPost[3] = linCV_2_Y_f3( linearCV, Y_MAX, Y_MIN);

// Apply the inverse tonescale independently in rendering-space RGB
float rgbPre[3] = inv_ssts_f3( rgbPost, PARAMS);

// RRT sweeteners
float aces[3] = inv_rrt_sweeteners( rgbPre);

return aces;
}
Loading

0 comments on commit 5d198cb

Please sign in to comment.