diff --git a/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst b/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst index c4cba43d7b..9ca0501a23 100644 --- a/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst +++ b/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst @@ -36,6 +36,8 @@ FIXED_FUNCTION_ACES_GAMUT_COMP_13 : ACES 1.3 Parametric Gamut Compression (expects ACEScg values) + FIXED_FUNCTION_PQ_TO_LINEAR : SMPTE ST 2084:2014 EOTF Linearization Equation + .. py:method:: name() -> str :property: @@ -104,6 +106,11 @@ :value: + .. py:attribute:: FixedFunctionStyle.FIXED_FUNCTION_PQ_TO_LINEAR + :module: PyOpenColorIO + :value: + + .. py:property:: FixedFunctionStyle.value :module: PyOpenColorIO diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp index 067d051477..e4802d812c 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp @@ -1210,60 +1210,32 @@ Renderer_PQ_TO_LINEAR::Renderer_PQ_TO_LINEAR(ConstFixedFunctionOpDataRcPtr & /*d void Renderer_PQ_TO_LINEAR::apply(const void *inImg, void *outImg, long numPixels) const { - // TODO optimize + /// TODO: This is a short, proof-of-concept implementation, needs optimization. + using namespace ST_2084; const float *in = (const float *)inImg; float *out = (float *)outImg; - for (long idx = 0; idx < numPixels; ++idx, out += 4, in += 4) + for (long idx = 0; idx < numPixels; ++idx) { - // (0..1) values will be pass through - // output values are scaled by 100 to convert nits/10,000 into nits/100 - - // R - { - float r = in[0]; - if ((r <= 0.0f) || (r >= 1.0f)) - { - out[0] = r * 100.0f; - } - else - { - const float x = std::pow(r, 1.f / m2); - out[0] = 100.0f * std::pow(std::max(0.f, x - c1) / (c2 - c3 * x), 1.f / m1); - }; - } - - // G - { - float g = in[1]; - if ((g <= 0.0f) || (g >= 1.0f)) - { - out[1] = g * 100.0f; - } - else - { - const float x = std::pow(g, 1.f / m2); - out[1] = 100.0f * std::pow(std::max(0.f, x - c1) / (c2 - c3 * x), 1.f / m1); - }; - } - - // B + // RGB + for (int ch = 0; ch < 3; ++ch) { - float b = in[2]; - if ((b <= 0.0f) || (b >= 1.0f)) + float v = *(in++); + if ((v <= 0.0f) /*|| (v >= 1.0f)*/) { - out[2] = b * 100.0f; + //*(out++) = v * 100.0f; + *(out++) = 0.0f; } else { - const float x = std::pow(b, 1.f / m2); - out[2] = 100.0f * std::pow(std::max(0.f, x - c1) / (c2 - c3 * x), 1.f / m1); + const float x = std::pow(v, 1.f / m2); + *(out++) = 100.0f * std::pow(std::max(0.f, x - c1) / (c2 - c3 * x), 1.f / m1); }; } - // A - out[3] = in[3]; + // Alpha + *(out++) = *(in++); } } @@ -1279,63 +1251,32 @@ void Renderer_LINEAR_TO_PQ::apply(const void *inImg, void *outImg, long numPixel const float* in = (const float*)inImg; float* out = (float*)outImg; - // Input is in nits/100, convert to [0,1], where 1 is 10000 nits. + // TODO: This is a short, proof of concept implementation, needs optimization. - for (long idx = 0; idx < numPixels; ++idx, out += 4, in += 4) + // Input is in nits/100, convert to [0,1], where 1 is 10000 nits. + for (long idx = 0; idx < numPixels; ++idx) { - // R - { - float r = 0.01f * in[0]; - if (r < 0.0f || r > 1.0f) - { - out[0] = r; - } - else - { - const float L = std::max(0.0f, r); - const float y = std::pow(L, m1); - const float ratpoly = (c1 + c2 * y) / (1.f + c3 * y); - const float N = std::pow(std::max(0.f, ratpoly), m2); - out[0] = N; - } - } - - // G - { - float g = 0.01f * in[1]; - if (g < 0.0f || g > 1.0f) - { - out[1] = g; - } - else - { - const float L = std::max(0.0f, g); - const float y = std::pow(L, m1); - const float ratpoly = (c1 + c2 * y) / (1.f + c3 * y); - const float N = std::pow(std::max(0.f, ratpoly), m2); - out[1] = N; - } - } - - // B + // RGB + for(int ch = 0; ch < 3; ++ch) { - float b = 0.01f * in[2]; - if (b < 0.0f || b > 1.0f) + float v = *(in++) * 0.01f; + if (v < 0.0f /*|| v > 1.0f*/) { - out[2] = b; + //*(out++) = v; + *(out++) = 0.0f; } else { - const float L = std::max(0.0f, b); + const float L = std::max(0.0f, v); const float y = std::pow(L, m1); const float ratpoly = (c1 + c2 * y) / (1.f + c3 * y); const float N = std::pow(std::max(0.f, ratpoly), m2); - out[2] = N; + *(out++) = N; } } - //A - out[3] = in[3]; + // Alpha + *(out++) = *(in++); }; } diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp index d4e78f171c..20dd7e96af 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp @@ -286,7 +286,8 @@ FixedFunctionOpData::Style FixedFunctionOpData::ConvertStyle(FixedFunctionStyle } case FIXED_FUNCTION_PQ_TO_LINEAR: { - return FixedFunctionOpData::PQ_TO_LINEAR; + return isForward ? FixedFunctionOpData::PQ_TO_LINEAR : + FixedFunctionOpData::LINEAR_TO_PQ; } } diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp index 30089f09c2..9ef37537bd 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp @@ -523,6 +523,56 @@ void Add_LUV_TO_XYZ(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) ss.newLine() << pxl << ".rgb.g = Y;"; } + +namespace +{ + namespace ST_2084 + { + static constexpr double m1 = 0.25 * 2610. / 4096.; + static constexpr double m2 = 128. * 2523. / 4096.; + static constexpr double c2 = 32. * 2413. / 4096.; + static constexpr double c3 = 32. * 2392. / 4096.; + static constexpr double c1 = c3 - c2 + 1.; + } +} // anonymous + +void Add_PQ_TO_LINEAR(GpuShaderCreatorRcPtr& shaderCreator, GpuShaderText& ss) +{ + using namespace ST_2084; + const std::string pxl(shaderCreator->getPixelName()); + + // TODO: this still clamps negative inputs + + // x = max(min(x, vec3(1.)), vec3(0.)); + // x = pow(x, vec3(1. / m2)); + // vec3 v = 1. * pow(max(vec3(0.), x - vec3(c1)) / (vec3(c2) - c3 * x), vec3(1. / m1)); + + ss.newLine() << ss.float3Decl("x") << " = " << pxl << ".rgb;"; + ss.newLine() << "x = max(x, " << ss.float3Const(0.0) << ");"; + ss.newLine() << "x = pow(x, "<< ss.float3Const(1.0 / m2) << ");"; + ss.newLine() << pxl << ".rgb = 100. * pow(max(" << ss.float3Const(0.0) << ", x - " << ss.float3Const(c1) << ") / (" + << ss.float3Const(c2) << " - " << c3 << " * x), " << ss.float3Const(1.0 / m1) << ");"; +} + +void Add_LINEAR_TO_PQ(GpuShaderCreatorRcPtr& shaderCreator, GpuShaderText& ss) +{ + using namespace ST_2084; + const std::string pxl(shaderCreator->getPixelName()); + + // TODO: this still clamps negative inputs + + // double L = std::max(0., input * 0.01); + // double y = std::pow(L, m1); + // double ratpoly = (c1 + c2 * y) / (1. + c3 * y); + // double N = std::pow(std::max(0., ratpoly), m2); + + ss.newLine() << ss.float3Decl("L") << " = max(vec3(0.), 0.01 * " << pxl << ".rgb);"; + ss.newLine() << ss.float3Decl("y") << " = pow(L, " << ss.float3Const(m1) << ");"; + ss.newLine() << ss.float3Decl("ratpoly") << " = (" << ss.float3Const(c1) << " + " << c2 << " * y) / (" + << ss.float3Const(1.0) << " + " << c3 << " * y);"; + ss.newLine() << pxl << ".rgb = pow(max(" << ss.float3Const(0.0) << ", ratpoly), " << ss.float3Const(m2) << ");"; +} + void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, ConstFixedFunctionOpDataRcPtr & func) { @@ -670,14 +720,17 @@ void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, case FixedFunctionOpData::LUV_TO_XYZ: { Add_LUV_TO_XYZ(shaderCreator, ss); + break; } case FixedFunctionOpData::PQ_TO_LINEAR: { - // TODO: Add function + Add_PQ_TO_LINEAR(shaderCreator, ss); + break; } case FixedFunctionOpData::LINEAR_TO_PQ: { - // TODO: Add function + Add_LINEAR_TO_PQ(shaderCreator, ss); + break; } } diff --git a/src/bindings/python/PyTypes.cpp b/src/bindings/python/PyTypes.cpp index 760d715587..96f485ad05 100644 --- a/src/bindings/python/PyTypes.cpp +++ b/src/bindings/python/PyTypes.cpp @@ -583,6 +583,9 @@ void bindPyTypes(py::module & m) DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_ACES_GAMUTMAP_07)) .value("FIXED_FUNCTION_ACES_GAMUT_COMP_13", FIXED_FUNCTION_ACES_GAMUT_COMP_13, DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_ACES_GAMUT_COMP_13)) + .value("FIXED_FUNCTION_PQ_TO_LINEAR", FIXED_FUNCTION_PQ_TO_LINEAR, + DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_PQ_TO_LINEAR)) + .export_values(); py::enum_( diff --git a/tests/gpu/FixedFunctionOp_test.cpp b/tests/gpu/FixedFunctionOp_test.cpp index 2ed4bdc1a3..7f77fa6adb 100644 --- a/tests/gpu/FixedFunctionOp_test.cpp +++ b/tests/gpu/FixedFunctionOp_test.cpp @@ -505,3 +505,26 @@ OCIO_ADD_GPU_TEST(FixedFunction, style_XYZ_TO_LUV_inv) test.setErrorThreshold(1e-5f); } + +OCIO_ADD_GPU_TEST(FixedFunction, style_PQ_TO_LINEAR_fwd) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_PQ_TO_LINEAR); + func->setDirection(OCIO::TRANSFORM_DIR_FORWARD); + + test.setTestWideRange(false); + test.setProcessor(func); + test.setErrorThreshold(1e-4f); + test.setRelativeComparison(true); // since output will be 0..100, let's set the relative epsilon. +} + +OCIO_ADD_GPU_TEST(FixedFunction, style_PQ_TO_LINEAR_inv) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_PQ_TO_LINEAR); + func->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + test.setTestWideRange(false); + test.setProcessor(func); + test.setErrorThreshold(1e-5f); +}