diff --git a/dso/map/Image/ImageMap.cc b/dso/map/Image/ImageMap.cc index cd68bbab..b12d7b8b 100644 --- a/dso/map/Image/ImageMap.cc +++ b/dso/map/Image/ImageMap.cc @@ -128,7 +128,7 @@ ImageMap::update() } if (needsUpdate || hasChanged(attrTexture) || - hasChanged(attrGamma) || + hasChanged(attrSourceColorSpace) || hasChanged(attrWrapAround) || hasChanged(attrUseDefaultColor) || hasChanged(attrDefaultColor)) { @@ -136,7 +136,7 @@ ImageMap::update() if (!mUdimTexture->update(this, sLogEventRegistry, get(attrTexture), - static_cast(get(attrGamma)), + get(attrSourceColorSpace), wrapS, wrapT, get(attrUseDefaultColor), @@ -161,13 +161,13 @@ ImageMap::update() } if (needsUpdate || hasChanged(attrTexture) || - hasChanged(attrGamma) || + hasChanged(attrSourceColorSpace) || hasChanged(attrWrapAround) || hasChanged(attrUseDefaultColor) || hasChanged(attrDefaultColor)) { std::string errorStr; if (!mTexture->update(get(attrTexture), - static_cast(get(attrGamma)), + get(attrSourceColorSpace), wrapS, wrapT, get(attrUseDefaultColor), @@ -415,4 +415,3 @@ ImageMap::applyColorCorrection(Color& result) const } //--------------------------------------------------------------------------- - diff --git a/dso/map/Image/ImageMap.json b/dso/map/Image/ImageMap.json index 833e18a6..75713aa5 100644 --- a/dso/map/Image/ImageMap.json +++ b/dso/map/Image/ImageMap.json @@ -73,17 +73,12 @@ "group": "Blur", "comment": "Amount to offset derivatives which controls mipmap selection. This can be used to provide a cheap, albiet low quality, blur. Note: Setting this value to -1.0 effectively disables mip mapping and the highest resolution will be used." }, - "attrGamma": { - "name": "gamma", - "type": "Int", - "default": "2", - "flags": "FLAGS_ENUMERABLE", - "enum": { - "off": "0", - "on": "1", - "auto": "2" - }, - "comment": "If this is set to 'on' or 'auto' and the 'texture' file is 8-bit, then a power of 2.2 will be applied to the RGB channels of the image." + "attrSourceColorSpace": { + "name": "source_color_space", + "label": "source color space", + "type": "String", + "default": "\"auto\"", + "comment": "OCIO source color space for the texture. Use 'auto' to apply the active OCIO file rules. Explicit values are resolved as color-space names, roles, or aliases from the active OCIO config." }, "attrOffset": { "name": "offset", diff --git a/dso/map/UsdUVTexture/UsdUVTexture.cc b/dso/map/UsdUVTexture/UsdUVTexture.cc index f588a841..45cd32ac 100644 --- a/dso/map/UsdUVTexture/UsdUVTexture.cc +++ b/dso/map/UsdUVTexture/UsdUVTexture.cc @@ -21,6 +21,30 @@ static ispc::StaticUsdUVTextureData sStaticUsdUVTextureData; //---------------------------------------------------------------------------- +namespace { + +constexpr int sSourceColorSpaceRaw = 0; +constexpr int sSourceColorSpaceSrgb = 1; + +std::string +sourceColorSpaceFromUsdEnum(const int sourceColorSpace, const std::string& overrideValue) +{ + if (!overrideValue.empty()) { + return overrideValue; + } + + switch (sourceColorSpace) { + case sSourceColorSpaceRaw: + return "raw"; + case sSourceColorSpaceSrgb: + return "sRGB"; + default: + return "auto"; + } +} + +} // namespace + RDL2_DSO_CLASS_BEGIN(UsdUVTexture, scene_rdl2::rdl2::Map) public: @@ -73,6 +97,9 @@ UsdUVTexture::update() const std::string filename = get(attrFile); const std::size_t udimPos = filename.find(""); const bool areWeAUdim = udimPos != std::string::npos; + const std::string sourceColorSpace = + sourceColorSpaceFromUsdEnum(get(attrSourceColorSpace), + get(attrSourceColorSpaceOverride)); const scene_rdl2::rdl2::SceneVariables &sv = getSceneClass().getSceneContext()->getSceneVariables(); mIspc.mFatalColor = asIspc(sv.get(scene_rdl2::rdl2::SceneVariables::sFatalColor)); @@ -103,6 +130,8 @@ UsdUVTexture::update() if (needsUpdate || hasChanged(attrFile) || + hasChanged(attrSourceColorSpace) || + hasChanged(attrSourceColorSpaceOverride) || hasChanged(attrWrapS) || hasChanged(attrWrapT) || hasChanged(attrFallback)) { @@ -110,7 +139,7 @@ UsdUVTexture::update() if (!mUdimTexture->update(this, sLogEventRegistry, filename, - static_cast(get(attrSourceColorSpace)), + sourceColorSpace, wrapS, wrapT, true, // use default/fallback color @@ -135,12 +164,14 @@ UsdUVTexture::update() } if (needsUpdate || hasChanged(attrFile) || + hasChanged(attrSourceColorSpace) || + hasChanged(attrSourceColorSpaceOverride) || hasChanged(attrWrapS) || hasChanged(attrWrapT) || hasChanged(attrFallback)) { if (!mTexture->update(filename, - static_cast(get(attrSourceColorSpace)), + sourceColorSpace, wrapS, wrapT, true, // use default/fallback color @@ -242,4 +273,3 @@ UsdUVTexture::sample(const scene_rdl2::rdl2::Map *self, rgb = rgb * me->get(attrScale) + me->get(attrBias); *sample = rgb; } - diff --git a/dso/map/UsdUVTexture/UsdUVTexture.json b/dso/map/UsdUVTexture/UsdUVTexture.json index fc642b23..fa536321 100644 --- a/dso/map/UsdUVTexture/UsdUVTexture.json +++ b/dso/map/UsdUVTexture/UsdUVTexture.json @@ -93,7 +93,14 @@ "sRGB": "1", "auto": "3" }, - "comment": "Flag indicating the color space in which the source texture is encoded. If set to auto, gamma correction will be applied if the images is not single channel." + "comment": "USD sourceColorSpace policy. raw is treated as USD data, sRGB is resolved through the active OCIO config, and auto uses OCIO file rules." + }, + "attrSourceColorSpaceOverride": { + "name": "source_color_space", + "label": "source color space override", + "type": "String", + "default": "\"\"", + "comment": "Optional OCIO source color-space override. Empty preserves the USD sourceColorSpace enum. Use 'auto' to apply active OCIO file rules, or author a color-space name, role, or alias from the active OCIO config. Data color spaces bypass conversion because OCIO marks them as data." } } } diff --git a/lib/rendering/geom/PrimitiveUserData.cc b/lib/rendering/geom/PrimitiveUserData.cc index 7c78cc8f..6a51ec89 100644 --- a/lib/rendering/geom/PrimitiveUserData.cc +++ b/lib/rendering/geom/PrimitiveUserData.cc @@ -14,6 +14,23 @@ using scene_rdl2::logging::Logger; namespace moonray { namespace geom { +namespace { + +bool +validRate(const scene_rdl2::rdl2::SceneObject* geometry, + const std::string& keyName, + AttributeRate rate) +{ + if (rate != AttributeRate::RATE_UNKNOWN) { + return true; + } + Logger::warn(geometry->getName(), '.', keyName, + ": skipping primitive attribute with invalid rate/count"); + return false; +} + +} // namespace + // These first two internal pickRate functions take an explicit rate. // If the explicit rate is set to "auto" we fall back to the public // pickRate function which guesses the rate based on the number of @@ -126,34 +143,9 @@ pickRate(const scene_rdl2::rdl2::SceneObject* object, return AttributeRate::RATE_VARYING; } - // Pick one that fits. Tried in assumed largest->smallest order. Some geometry - // may produce a different order but it is probably ok that the interpolation - // guess is not the closest one. Also 1 always turns into constant even if others - // have counts of 1. - size_t best; - AttributeRate rate; - if (rates.faceVaryingCount > 1 && size > rates.faceVaryingCount) { - best = rates.faceVaryingCount; - rate = AttributeRate::RATE_FACE_VARYING; - } else if (rates.vertexCount > 1 && size > rates.vertexCount) { - best = rates.vertexCount; - rate = AttributeRate::RATE_VERTEX; - } else if (rates.varyingCount > 1 && size > rates.varyingCount) { - best = rates.varyingCount; - rate = AttributeRate::RATE_VARYING; - } else if (rates.uniformCount > 1 && size > rates.uniformCount) { - best = rates.uniformCount; - rate = AttributeRate::RATE_UNIFORM; - } else if (rates.partCount > 1 && size > rates.partCount) { - best = rates.partCount; - rate = AttributeRate::RATE_PART; - } else { - best = 1; - rate = AttributeRate::RATE_CONSTANT; - } - - Logger::warn(object->getName(), '.', keyName, ": invalid size ", size, " truncated to ", best); - return rate; + Logger::warn(object->getName(), '.', keyName, ": invalid size ", size, + " does not match any primitive attribute rate"); + return AttributeRate::RATE_UNKNOWN; } bool sizeCheck(const scene_rdl2::rdl2::SceneObject* object, @@ -192,26 +184,28 @@ processArbitraryData(const scene_rdl2::rdl2::SceneObject* geometry, std::vector data(constData.begin(), constData.end()); - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - data.size(), - rates), - std::move(data)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + data.size(), + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(data)); + } } if (userData->hasIntData()) { shading::TypedAttributeKey key(userData->getIntKey()); scene_rdl2::rdl2::IntVector data = userData->getIntValues(); - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - data.size(), - rates), - std::move(data)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + data.size(), + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(data)); + } } { @@ -231,27 +225,29 @@ processArbitraryData(const scene_rdl2::rdl2::SceneObject* geometry, samples[1].size() : size0; - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - size0, - size1, - rates), - std::move(samples)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + size0, + size1, + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(samples)); + } } } if (userData->hasStringData()) { shading::TypedAttributeKey key(userData->getStringKey()); scene_rdl2::rdl2::StringVector data = userData->getStringValues(); - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - data.size(), - rates), - std::move(data)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + data.size(), + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(data)); + } } { @@ -271,14 +267,15 @@ processArbitraryData(const scene_rdl2::rdl2::SceneObject* geometry, samples[1].size() : size0; - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - size0, - size1, - rates), - std::move(samples)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + size0, + size1, + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(samples)); + } } } @@ -299,14 +296,15 @@ processArbitraryData(const scene_rdl2::rdl2::SceneObject* geometry, samples[1].size() : size0; - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - size0, - size1, - rates), - std::move(samples)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + size0, + size1, + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(samples)); + } } } @@ -327,14 +325,15 @@ processArbitraryData(const scene_rdl2::rdl2::SceneObject* geometry, samples[1].size() : size0; - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - size0, - size1, - rates), - std::move(samples)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + size0, + size1, + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(samples)); + } } } @@ -355,14 +354,15 @@ processArbitraryData(const scene_rdl2::rdl2::SceneObject* geometry, samples[1].size() : size0; - primitiveAttributeTable.addAttribute(key, - pickRate(geometry, - explicitRate, - key.getName(), - size0, - size1, - rates), - std::move(samples)); + AttributeRate rate = pickRate(geometry, + explicitRate, + key.getName(), + size0, + size1, + rates); + if (validRate(geometry, key.getName(), rate)) { + primitiveAttributeTable.addAttribute(key, rate, std::move(samples)); + } } } } @@ -370,4 +370,3 @@ processArbitraryData(const scene_rdl2::rdl2::SceneObject* geometry, } // namespace geom } // namespace moonray - diff --git a/lib/rendering/rndr/RenderOutputWriter.cc b/lib/rendering/rndr/RenderOutputWriter.cc index 092ba219..420a352c 100644 --- a/lib/rendering/rndr/RenderOutputWriter.cc +++ b/lib/rendering/rndr/RenderOutputWriter.cc @@ -1568,10 +1568,10 @@ float RenderOutputWriter::htof(const unsigned short h) { -#if defined(__ARM_NEON__) // TODO: Verify this - float output; - vst1q_f32(&output, vcvt_f32_f16(vld1_u16(&h))); - return output; +#if defined(__ARM_NEON__) + __fp16 input; + std::memcpy(static_cast(&input), static_cast(&h), sizeof(input)); + return static_cast(input); #else return _cvtsh_ss(h); // Convert half 16bit float to full 32bit float #endif @@ -1583,10 +1583,11 @@ unsigned short RenderOutputWriter::ftoh(const float f) { -#if defined(__ARM_NEON__) // TODO: Verify this - __fp16 output; - vst1_f16(&output, vcvt_f16_f32(vld1q_f32(&f))); - return output; +#if defined(__ARM_NEON__) + __fp16 h = static_cast<__fp16>(f); + unsigned short output; + std::memcpy(static_cast(&output), static_cast(&h), sizeof(output)); + return output; #else return _cvtss_sh(f, 0); // Convert full 32bit float to half 16bit float // An immediate value controlling rounding using bits : 0=Nearest diff --git a/lib/rendering/shading/BasicTexture.cc b/lib/rendering/shading/BasicTexture.cc index 31c070c4..1c62d3a0 100644 --- a/lib/rendering/shading/BasicTexture.cc +++ b/lib/rendering/shading/BasicTexture.cc @@ -3,6 +3,7 @@ #include "BasicTexture.h" +#include "TextureColorManagement.h" #include #include @@ -14,6 +15,8 @@ #include #include +#include + #ifdef __ARM_NEON__ // This works around OIIO including x86 based headers due to detection of SSE // support due to sse2neon.h being included elsewhere @@ -25,6 +28,7 @@ #include #include +#include namespace moonray { namespace shading { @@ -130,7 +134,7 @@ class BasicTexture::Impl bool update(const std::string &filename, - ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -139,6 +143,7 @@ class BasicTexture::Impl std::string &errorMsg) { init(); + mSourceColorSpace = sourceColorSpace; mIspc.mUseDefaultColor = useDefaultColor; mIspc.mDefaultColor.r = defaultColor.r; @@ -148,6 +153,8 @@ class BasicTexture::Impl mIspc.mFatalColor.g = fatalColor.g; mIspc.mFatalColor.b = fatalColor.b; mIspc.mIsValid = false; + mIspc.mOcioProcessor = 0; + mOcioProcessor.reset(); mTextureHandles.assign(1, nullptr); mIspc.mTextureHandles = reinterpret_cast(&mTextureHandles[0]); @@ -209,8 +216,19 @@ class BasicTexture::Impl mIspc.mPixelAspectRatio = mPixelAspectRatio; mIspc.mTextureOptions = (intptr_t) mTextureOpt; - mIspc.mApplyGamma = getApplyGamma(gammaMode, spec.nchannels); - mIspc.mIs8bit = (spec.format == OIIO::TypeDesc::UINT8); + texture_color_management::ProcessorResult processor = + texture_color_management::createTextureProcessor(filename, mSourceColorSpace); + mOcioProcessor = processor.mProcessor; + if (!processor.mDiagnostic.empty()) { + if (processor.mDiagnostic.find("method=explicit:unresolved") != std::string::npos || + processor.mDiagnostic.find("method=failed") != std::string::npos || + processor.mDiagnostic.find("OCIO config load failed") != std::string::npos || + processor.mDiagnostic.find("no render/working color space") != std::string::npos || + processor.mDiagnostic.find("targetMethod=role:default") != std::string::npos) { + scene_rdl2::logging::Logger::warn("Texture OCIO: ", processor.mDiagnostic); + } + } + mIspc.mOcioProcessor = reinterpret_cast(mOcioProcessor.get()); mIspc.mIsValid = true; if (mIspc.mUseDefaultColor) { @@ -257,12 +275,7 @@ class BasicTexture::Impl ); if (res) { - if (mIspc.mApplyGamma && mIspc.mIs8bit) { // actually INVERSE gamma - tmp[0] = tmp[0] > 0.0f ? powf(tmp[0], 2.2f) : 0.0f; - tmp[1] = tmp[1] > 0.0f ? powf(tmp[1], 2.2f) : 0.0f; - tmp[2] = tmp[2] > 0.0f ? powf(tmp[2], 2.2f) : 0.0f; - // don't gamma the alpha channel - } + texture_color_management::applyProcessor(mIspc.mOcioProcessor, tmp); result[0] = tmp[0]; result[1] = tmp[1]; result[2] = tmp[2]; @@ -285,9 +298,9 @@ class BasicTexture::Impl textureSampler->unregisterMapForInvalidation(mShader); } - mIspc.mApplyGamma = false; - mIspc.mIs8bit = false; + mIspc.mOcioProcessor = 0; mIspc.mIsValid = false; + mOcioProcessor.reset(); mWidth = 0; mHeight = 0; mPixelAspectRatio = 0.0f; @@ -310,6 +323,8 @@ class BasicTexture::Impl scene_rdl2::rdl2::Shader *mShader; std::vector mTextureHandles; texture::TextureOptions mTextureOpt[QualityCount]; + OCIO::ConstCPUProcessorRcPtr mOcioProcessor; + std::string mSourceColorSpace; int mWidth; int mHeight; @@ -331,7 +346,7 @@ BasicTexture::~BasicTexture() bool BasicTexture::update(const std::string &filename, - ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -340,7 +355,7 @@ BasicTexture::update(const std::string &filename, std::string &errorMsg) { return mImpl->update(filename, - gammaMode, + sourceColorSpace, wrapS, wrapT, useDefaultColor, @@ -423,12 +438,7 @@ void CPP_oiioTexture(const ispc::BASIC_TEXTURE_Data *tx, result); if (res) { - if (tx->mApplyGamma && tx->mIs8bit) { // actually INVERSE gamma - result[0] = pow(result[0], 2.2f); - result[1] = pow(result[1], 2.2f); - result[2] = pow(result[2], 2.2f); - // don't gamma the alpha channel - } + texture_color_management::applyProcessor(tx->mOcioProcessor, result); } else { scene_rdl2::rdl2::Shader* const shader = reinterpret_cast(tx->mShader); scene_rdl2::rdl2::Shader::getLogEventRegistry().log(shader, tx->mBasicTextureStaticDataPtr->sErrorSampleFail); diff --git a/lib/rendering/shading/BasicTexture.h b/lib/rendering/shading/BasicTexture.h index 4b8f1709..43683ee0 100644 --- a/lib/rendering/shading/BasicTexture.h +++ b/lib/rendering/shading/BasicTexture.h @@ -23,7 +23,7 @@ class BasicTexture { ~BasicTexture(); bool update(const std::string &filename, - ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -63,4 +63,3 @@ void CPP_oiioTexture(const ispc::BASIC_TEXTURE_Data* tx, } // namespace shading } // namespace moonray - diff --git a/lib/rendering/shading/CMakeLists.txt b/lib/rendering/shading/CMakeLists.txt index fb5d5cbe..7bcaf676 100644 --- a/lib/rendering/shading/CMakeLists.txt +++ b/lib/rendering/shading/CMakeLists.txt @@ -5,6 +5,8 @@ add_subdirectory(ispc) set(component rendering_shading) +find_package(OpenColorIO REQUIRED) + set(installIncludeDir ${PACKAGE_NAME}/rendering/shading) set(exportGroup ${PROJECT_NAME}Targets) @@ -38,6 +40,7 @@ target_sources(${component} ShadingUtil.cc State.cc Texture.cc + TextureColorManagement.cc UdimTexture.cc Xform.cc bsdf/Bsdf.cc @@ -97,6 +100,7 @@ set_property(TARGET ${component} State.h ColorCorrect.h Iridescence.h + TextureColorManagement.h UdimTexture.h Util.h Xform.h @@ -116,6 +120,7 @@ target_link_libraries(${component} ${PROJECT_NAME}::shading_eval_ispc ${PROJECT_NAME}::shading_ispc ${PROJECT_NAME}::texturing_sampler + OpenColorIO::OpenColorIO SceneRdl2::scene_rdl2 ) diff --git a/lib/rendering/shading/TextureColorManagement.cc b/lib/rendering/shading/TextureColorManagement.cc new file mode 100644 index 00000000..5e759fd7 --- /dev/null +++ b/lib/rendering/shading/TextureColorManagement.cc @@ -0,0 +1,375 @@ +// Copyright 2023-2024 DreamWorks Animation LLC +// SPDX-License-Identifier: Apache-2.0 + +#include "TextureColorManagement.h" + +#include +#include +#include +#include + +namespace moonray { +namespace shading { +namespace texture_color_management { +namespace { + +constexpr const char* sRenderingColorSpaceEnv = "MOONRAY_OCIO_RENDERING_COLOR_SPACE"; + +std::string +trim(std::string value) +{ + auto notSpace = [](unsigned char c) { return !std::isspace(c); }; + value.erase(value.begin(), std::find_if(value.begin(), value.end(), notSpace)); + value.erase(std::find_if(value.rbegin(), value.rend(), notSpace).base(), value.end()); + return value; +} + +std::string +normalized(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char c) { + if (c == '-' || c == ' ' || c == '.') return '_'; + return static_cast(std::tolower(c)); + }); + return value; +} + +bool +isHardBypassToken(const std::string& value) +{ + const std::string key = normalized(trim(value)); + // These tokens describe data, not a color space to be resolved through + // OCIO. In particular, do not depend on a config's optional `Raw` color + // space being marked as data: scalar maps (roughness, metallic, masks, + // displacement) must preserve their sampled values exactly. + return key == "raw" || key == "data" || key == "none"; +} + +std::string +ocioPath() +{ + const char* ocio = std::getenv("OCIO"); + return ocio ? std::string(ocio) : std::string(); +} + +std::string +colorSpaceName(const OCIO::ConstColorSpaceRcPtr& colorSpace) +{ + if (!colorSpace) { + return {}; + } + const char* name = colorSpace->getName(); + return name ? std::string(name) : std::string(); +} + +std::string +resolveColorSpace(const OCIO::ConstConfigRcPtr& config, + const std::string& token) +{ + if (!config || token.empty()) { + return {}; + } + + try { + return colorSpaceName(config->getColorSpace(token.c_str())); + } catch (const OCIO::Exception&) { + } + return {}; +} + +bool +isRoleDataColorSpace(const OCIO::ConstConfigRcPtr& config, + const std::string& colorSpaceName) +{ + if (!config || colorSpaceName.empty()) { + return false; + } + + const std::string dataRole = resolveColorSpace(config, OCIO::ROLE_DATA); + return !dataRole.empty() && dataRole == colorSpaceName; +} + +bool +isDataColorSpace(const OCIO::ConstConfigRcPtr& config, + const std::string& colorSpaceName) +{ + if (!config || colorSpaceName.empty()) { + return false; + } + + if (isRoleDataColorSpace(config, colorSpaceName)) { + return true; + } + + try { + OCIO::ConstColorSpaceRcPtr colorSpace = config->getColorSpace(colorSpaceName.c_str()); + if (colorSpace) { + if (colorSpace->isData()) { + return true; + } + } + } catch (const OCIO::Exception&) { + } + + return false; +} + +std::string +roleColorSpace(const OCIO::ConstConfigRcPtr& config, const char* role) +{ + return resolveColorSpace(config, role ? role : ""); +} + +std::string +renderingColorSpace(const OCIO::ConstConfigRcPtr& config, + std::string* method) +{ + const char* authoredTarget = std::getenv(sRenderingColorSpaceEnv); + if (authoredTarget && authoredTarget[0]) { + const std::string resolved = resolveColorSpace(config, authoredTarget); + if (!resolved.empty() && !isDataColorSpace(config, resolved)) { + if (method) { + *method = std::string("render-setting:") + sRenderingColorSpaceEnv; + } + return resolved; + } + } + + const std::pair roles[] = { + {OCIO::ROLE_RENDERING, "role:rendering"}, + {OCIO::ROLE_SCENE_LINEAR, "role:scene_linear"}, + {"default_float", "role:default_float"}, + {"reference", "role:reference"}, + {OCIO::ROLE_DEFAULT, "role:default"} + }; + + for (const auto& entry : roles) { + std::string name = roleColorSpace(config, entry.first); + if (!name.empty() && !isDataColorSpace(config, name)) { + if (method) { + *method = entry.second; + } + return name; + } + } + + if (method) { + method->clear(); + } + return {}; +} + +std::string +sourceColorSpaceForTexture(const OCIO::ConstConfigRcPtr& config, + const std::string& filename, + const std::string& authoredSource, + std::string* method, + std::string* reason) +{ + const std::string source = trim(authoredSource); + const std::string key = normalized(source.empty() ? std::string("auto") : source); + + if (isHardBypassToken(key)) { + if (method) { + *method = "bypass"; + } + if (reason) { + *reason = "authored none token"; + } + return {}; + } + + if (!config) { + if (method) { + *method = "disabled"; + } + if (reason) { + *reason = "no active OCIO config"; + } + return {}; + } + + if (key == "auto") { + try { + const char* fileRuleSpace = config->getColorSpaceFromFilepath(filename.c_str()); + if (!fileRuleSpace || isHardBypassToken(fileRuleSpace)) { + if (method) { + *method = "file-rule:bypass"; + } + if (reason) { + *reason = fileRuleSpace ? fileRuleSpace : "no file-rule color space"; + } + return {}; + } + std::string resolved = resolveColorSpace(config, fileRuleSpace); + if (!resolved.empty()) { + if (isDataColorSpace(config, resolved)) { + if (method) { + *method = "file-rule:data"; + } + if (reason) { + *reason = resolved; + } + return {}; + } + if (method) { + *method = "file-rule"; + } + return resolved; + } + } catch (const OCIO::Exception& e) { + if (method) { + *method = "file-rule:error"; + } + if (reason) { + *reason = e.what(); + } + return {}; + } + + if (method) { + *method = "file-rule:unresolved"; + } + if (reason) { + *reason = "file rule returned an unresolved color space"; + } + return {}; + } + + std::string resolved = resolveColorSpace(config, source); + if (!resolved.empty()) { + if (isDataColorSpace(config, resolved)) { + if (method) { + *method = "explicit:data"; + } + if (reason) { + *reason = resolved; + } + return {}; + } + if (method) { + *method = "explicit"; + } + return resolved; + } + + if (key == "raw" || key == "data") { + const std::string dataRole = resolveColorSpace(config, OCIO::ROLE_DATA); + if (!dataRole.empty() && isDataColorSpace(config, dataRole)) { + if (method) { + *method = "explicit:data-role"; + } + if (reason) { + *reason = dataRole; + } + return {}; + } + if (method) { + *method = "explicit:unresolved"; + } + if (reason) { + *reason = "authored data token is not a name, role, or alias in the active OCIO config"; + } + return {}; + } + + if (method) { + *method = "explicit:unresolved"; + } + if (reason) { + *reason = "authored source color space is not a name, role, or alias in the active config"; + } + return {}; +} + +std::string +baseDiagnostic(const std::string& filename, + const std::string& authoredSource, + const std::string& method) +{ + std::ostringstream out; + out << "filename=\"" << filename << "\"" + << " authored=\"" << authoredSource << "\"" + << " method=" << method + << " ocio=\"" << ocioPath() << "\"" + << " version=" << OCIO::GetVersion(); + return out.str(); +} + +} // namespace + +ProcessorResult +createTextureProcessor(const std::string& filename, + const std::string& sourceColorSpace) +{ + ProcessorResult result; + + OCIO::ConstConfigRcPtr config; + try { + config = OCIO::GetCurrentConfig(); + } catch (const OCIO::Exception& e) { + result.mDiagnostic = baseDiagnostic(filename, sourceColorSpace, "disabled") + + " reason=\"OCIO config load failed: " + e.what() + "\""; + return result; + } + + std::string sourceMethod; + std::string sourceReason; + const std::string source = sourceColorSpaceForTexture(config, + filename, + sourceColorSpace, + &sourceMethod, + &sourceReason); + if (source.empty()) { + result.mDiagnostic = baseDiagnostic(filename, sourceColorSpace, sourceMethod) + + " reason=\"" + sourceReason + "\""; + return result; + } + + std::string targetMethod; + const std::string target = renderingColorSpace(config, &targetMethod); + if (target.empty()) { + result.mDiagnostic = baseDiagnostic(filename, sourceColorSpace, sourceMethod) + + " source=\"" + source + "\" reason=\"no render/working color space resolved\""; + return result; + } + + if (source == target) { + result.mDiagnostic = baseDiagnostic(filename, sourceColorSpace, "identity") + + " source=\"" + source + "\" target=\"" + target + "\" targetMethod=" + targetMethod; + return result; + } + + try { + OCIO::ConstProcessorRcPtr processor = + config->getProcessor(source.c_str(), target.c_str()); + result.mProcessor = processor ? processor->getDefaultCPUProcessor() : OCIO::ConstCPUProcessorRcPtr(); + result.mDiagnostic = baseDiagnostic(filename, sourceColorSpace, sourceMethod) + + " source=\"" + source + "\" target=\"" + target + "\" targetMethod=" + targetMethod; + return result; + } catch (const OCIO::Exception& e) { + result.mDiagnostic = baseDiagnostic(filename, sourceColorSpace, "failed") + + " source=\"" + source + "\" target=\"" + target + "\" reason=\"" + e.what() + "\""; + } + + return result; +} + +void +applyProcessor(intptr_t processorPtr, float* rgba) +{ + if (!processorPtr || !rgba) { + return; + } + const OCIO::CPUProcessor* processor = + reinterpret_cast(processorPtr); + try { + processor->applyRGB(rgba); + } catch (const OCIO::Exception&) { + } +} + +} // namespace texture_color_management +} // namespace shading +} // namespace moonray diff --git a/lib/rendering/shading/TextureColorManagement.h b/lib/rendering/shading/TextureColorManagement.h new file mode 100644 index 00000000..3dbae526 --- /dev/null +++ b/lib/rendering/shading/TextureColorManagement.h @@ -0,0 +1,29 @@ +// Copyright 2023-2024 DreamWorks Animation LLC +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include + +namespace OCIO = OCIO_NAMESPACE; + +namespace moonray { +namespace shading { +namespace texture_color_management { + +struct ProcessorResult +{ + OCIO::ConstCPUProcessorRcPtr mProcessor; + std::string mDiagnostic; +}; + +ProcessorResult createTextureProcessor(const std::string& filename, + const std::string& sourceColorSpace); + +void applyProcessor(intptr_t processorPtr, float* rgba); + +} // namespace texture_color_management +} // namespace shading +} // namespace moonray diff --git a/lib/rendering/shading/UdimTexture.cc b/lib/rendering/shading/UdimTexture.cc index 9af3ab09..39f80a37 100644 --- a/lib/rendering/shading/UdimTexture.cc +++ b/lib/rendering/shading/UdimTexture.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "UdimTexture.h" +#include "TextureColorManagement.h" #include #include @@ -127,8 +128,7 @@ class UdimTexture::Impl mErrorUdimOutOfRangeU(0), mErrorUdimOutOfRangeV(0), mErrorSampleFail(0), - mNumTextures(0), - mIs8bit(false) + mNumTextures(0) { mIspc.mShader = (intptr_t) shader; mIspc.mTextureHandles = nullptr; @@ -194,7 +194,7 @@ class UdimTexture::Impl update(scene_rdl2::rdl2::Shader *shader, scene_rdl2::rdl2::ShaderLogEventRegistry& logEventRegistry, const std::string &filename, - ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -203,6 +203,7 @@ class UdimTexture::Impl std::string &errorMsg) { init(); + mSourceColorSpace = sourceColorSpace; mIspc.mUseDefaultColor = useDefaultColor; mIspc.mDefaultColor.r = defaultColor.r; @@ -211,6 +212,8 @@ class UdimTexture::Impl mIspc.mFatalColor.r = fatalColor.r; mIspc.mFatalColor.g = fatalColor.g; mIspc.mFatalColor.b = fatalColor.b; + mIspc.mOcioProcessor = 0; + mOcioProcessor.reset(); std::vector udimFilenames; if (!getUdimFilenames(filename, udimFilenames)) { @@ -252,14 +255,10 @@ class UdimTexture::Impl mErrorUdimMissingTexture.clear(); mErrorUdimMissingTexture.resize(mNumTextures); - bool applyGamma = true; - if (prepareUdimTextureHandles(filename, udimPos, logEventRegistry, errorMsg, - gammaMode, - applyGamma, udimFilenames)) { mIspc.mIsValid = true; } @@ -274,8 +273,19 @@ class UdimTexture::Impl mTextureOpt[i].twrap = getOIIOWrap(wrapT); mTextureOpt[i].subimagename.clear(); } - mIspc.mApplyGamma = applyGamma; - mIspc.mIs8bit = mIs8bit; + texture_color_management::ProcessorResult processor = + texture_color_management::createTextureProcessor(filename, mSourceColorSpace); + mOcioProcessor = processor.mProcessor; + if (!processor.mDiagnostic.empty()) { + if (processor.mDiagnostic.find("method=explicit:unresolved") != std::string::npos || + processor.mDiagnostic.find("method=failed") != std::string::npos || + processor.mDiagnostic.find("OCIO config load failed") != std::string::npos || + processor.mDiagnostic.find("no render/working color space") != std::string::npos || + processor.mDiagnostic.find("targetMethod=role:default") != std::string::npos) { + scene_rdl2::logging::Logger::warn("UdimTexture OCIO: ", processor.mDiagnostic); + } + } + mIspc.mOcioProcessor = reinterpret_cast(mOcioProcessor.get()); tbb::mutex errorMutex; @@ -360,12 +370,7 @@ class UdimTexture::Impl scene_rdl2::math::Color4 result; if (res) { - if (mIspc.mApplyGamma && mIspc.mIs8bit) { - tmp[0] = tmp[0] > 0.0f ? powf(tmp[0], 2.2f) : 0.0f; - tmp[1] = tmp[1] > 0.0f ? powf(tmp[1], 2.2f) : 0.0f; - tmp[2] = tmp[2] > 0.0f ? powf(tmp[2], 2.2f) : 0.0f; - // don't gamma the alpha channel - } + texture_color_management::applyProcessor(mIspc.mOcioProcessor, tmp); result[0] = tmp[0]; result[1] = tmp[1]; result[2] = tmp[2]; @@ -394,9 +399,9 @@ class UdimTexture::Impl textureSampler->unregisterMapForInvalidation(mShader); } - mIspc.mApplyGamma = false; - mIspc.mIs8bit = false; + mIspc.mOcioProcessor = 0; mIspc.mIsValid = false; + mOcioProcessor.reset(); } bool @@ -416,7 +421,8 @@ class UdimTexture::Impl logEvent(mErrorUdimOutOfRangeV); return -1; } - return int(u) + int(v) * sMaxUdim; + const int tileV = int(std::max(0.0f, v - 1.0e-5f)); + return int(u) + tileV * sMaxUdim; } const ispc::UDIM_TEXTURE_Data& getUdimTextureData() const { @@ -472,9 +478,7 @@ class UdimTexture::Impl int idx, int &firstUdimChannelCount, int &firstUdimFileFormat, - std::string &errorMsg, - ispc::TEXTURE_GammaMode gammaMode, - bool& applyGamma) + std::string &errorMsg) { std::string errorString; texture::TextureSampler *textureSampler = texture::getTextureSampler(); @@ -520,13 +524,9 @@ class UdimTexture::Impl std::shared_ptr textureSystem = textureSampler->getTextureSystem(); # endif textureSystem->get_imagespec(ufilename, 0, spec); - mIs8bit = (spec.format == OIIO::TypeDesc::UINT8); mWidths[idx] = spec.width; mHeights[idx] = spec.height; - // Don't apply gamma if any of the images are single channel - applyGamma = applyGamma ? getApplyGamma(gammaMode, spec.nchannels) : false; - mPixelAspectRatios[idx] = spec.get_float_attribute("PixelAspectRatio", 1.0f); return true; @@ -537,8 +537,6 @@ class UdimTexture::Impl const std::size_t uDimPos, scene_rdl2::rdl2::ShaderLogEventRegistry& logEventRegistry, std::string &errorMsg, - ispc::TEXTURE_GammaMode gammaMode, - bool& applyGamma, std::vector& udimFilenames) { std::string udimFileName = filename; @@ -553,9 +551,7 @@ class UdimTexture::Impl idx, firstUdimChannelCount, firstUdimFileFormat, - errorMsg, - gammaMode, - applyGamma)) { + errorMsg)) { return false; } } else { @@ -592,12 +588,13 @@ class UdimTexture::Impl std::vector mTextureHandles; texture::TextureOptions mTextureOpt[QualityCount]; std::vector> mTextureOptions; + OCIO::ConstCPUProcessorRcPtr mOcioProcessor; + std::string mSourceColorSpace; std::vector mTextureHandleIndices; int mNumTextures; std::vector mWidths; std::vector mHeights; std::vector mPixelAspectRatios; - bool mIs8bit; static std::atomic mUdimMissingTextureWarningSwitch; }; @@ -630,7 +627,7 @@ bool UdimTexture::update(scene_rdl2::rdl2::Shader *shader, scene_rdl2::rdl2::ShaderLogEventRegistry& logEventRegistry, const std::string &filename, - ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -641,7 +638,7 @@ UdimTexture::update(scene_rdl2::rdl2::Shader *shader, return mImpl->update(shader, logEventRegistry, filename, - gammaMode, + sourceColorSpace, wrapS, wrapT, useDefaultColor, @@ -764,12 +761,7 @@ void CPP_oiioUdimTexture(const ispc::UDIM_TEXTURE_Data *tx, result); if (res) { - if (tx->mApplyGamma && tx->mIs8bit) { - result[0] = result[0] > 0.0f ? powf(result[0], 2.2f) : 0.0f; - result[1] = result[1] > 0.0f ? powf(result[1], 2.2f) : 0.0f; - result[2] = result[2] > 0.0f ? powf(result[2], 2.2f) : 0.0f; - // don't gamma the alpha channel - } + texture_color_management::applyProcessor(tx->mOcioProcessor, result); } else { scene_rdl2::rdl2::Shader::getLogEventRegistry().log(shader, tx->mErrorSampleFail); result[0] = result[1] = result[2] = result[3] = 0.f; diff --git a/lib/rendering/shading/UdimTexture.h b/lib/rendering/shading/UdimTexture.h index 5537d382..6bd3ef34 100644 --- a/lib/rendering/shading/UdimTexture.h +++ b/lib/rendering/shading/UdimTexture.h @@ -23,7 +23,7 @@ class UdimTexture { bool update(scene_rdl2::rdl2::Shader *shader, scene_rdl2::rdl2::ShaderLogEventRegistry& logEventRegistry, const std::string &filename, - ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -73,4 +73,3 @@ void CPP_oiioUdimTexture(const ispc::UDIM_TEXTURE_Data* tx, } // namespace shading } // namespace moonray - diff --git a/lib/rendering/shading/Util.h b/lib/rendering/shading/Util.h index 2299fa36..13ef45f2 100644 --- a/lib/rendering/shading/Util.h +++ b/lib/rendering/shading/Util.h @@ -629,26 +629,6 @@ discretize(const float t, const size_t numBins) static_cast(numBins)); } -// Determines whether or not to apply gamma correction based on the gamma mode and number of channels -finline bool -getApplyGamma(ispc::TEXTURE_GammaMode gammaMode, - int nChannels) -{ - switch(gammaMode) { - case ispc::TEXTURE_GAMMA_OFF: - return false; - case ispc::TEXTURE_GAMMA_ON: - return true; - case ispc::TEXTURE_GAMMA_AUTO: - return true; - case ispc::TEXTURE_GAMMA_USD: - return nChannels != 1; - default: - return true; - } -} - } // namespace shading } // namespace moonray - diff --git a/lib/rendering/shading/ispc/BasicTexture.isph b/lib/rendering/shading/ispc/BasicTexture.isph index 4e339fa2..c130d159 100644 --- a/lib/rendering/shading/ispc/BasicTexture.isph +++ b/lib/rendering/shading/ispc/BasicTexture.isph @@ -34,8 +34,7 @@ struct BASIC_TEXTURE_Data uniform Color mFatalColor; uniform BASIC_TEXTURE_StaticData* uniform mBasicTextureStaticDataPtr; uniform intptr_t mTextureOptions; - uniform bool mApplyGamma; - uniform bool mIs8bit; + uniform intptr_t mOcioProcessor; }; Col4f @@ -64,5 +63,3 @@ BASIC_TEXTURE_getDimensions( float BASIC_TEXTURE_getPixelAspectRatio(const uniform BASIC_TEXTURE_Data * uniform tx); - - diff --git a/lib/rendering/shading/ispc/UdimTexture.ispc b/lib/rendering/shading/ispc/UdimTexture.ispc index 3ccb65c3..46bab129 100644 --- a/lib/rendering/shading/ispc/UdimTexture.ispc +++ b/lib/rendering/shading/ispc/UdimTexture.ispc @@ -43,7 +43,7 @@ UDIM_TEXTURE_compute_udim( tx->mErrorUdimOutOfRangeV); return -1; } - return (int)st.x + (int)st.y * 10; + return (int)st.x + ((int)max(0.f, st.y - 1.0e-5f)) * 10; } Col4f @@ -134,4 +134,3 @@ UDIM_TEXTURE_getPixelAspectRatio( { return tx->mPixelAspectRatios[udim]; } - diff --git a/lib/rendering/shading/ispc/UdimTexture.isph b/lib/rendering/shading/ispc/UdimTexture.isph index 90dbf68b..a4e69d10 100644 --- a/lib/rendering/shading/ispc/UdimTexture.isph +++ b/lib/rendering/shading/ispc/UdimTexture.isph @@ -37,8 +37,7 @@ struct UDIM_TEXTURE_Data uniform Color mFatalColor; uniform UDIM_TEXTURE_StaticData* uniform mUdimTextureStaticDataPtr; uniform intptr_t mTextureOptions; - uniform bool mApplyGamma; - uniform bool mIs8bit; + uniform intptr_t mOcioProcessor; }; // returns -1 if out of range @@ -70,5 +69,3 @@ float UDIM_TEXTURE_getPixelAspectRatio( const uniform UDIM_TEXTURE_Data * uniform tx, int udim); - - diff --git a/lib/rendering/shading/ispc/Util.ispc b/lib/rendering/shading/ispc/Util.ispc index 14bcdf36..36458097 100644 --- a/lib/rendering/shading/ispc/Util.ispc +++ b/lib/rendering/shading/ispc/Util.ispc @@ -3,6 +3,3 @@ #include "Util.isph" #include - -ISPC_UTIL_EXPORT_ENUM_TO_HEADER(TEXTURE_GammaMode); - diff --git a/lib/rendering/shading/ispc/Util.isph b/lib/rendering/shading/ispc/Util.isph index c47c6670..5014e797 100644 --- a/lib/rendering/shading/ispc/Util.isph +++ b/lib/rendering/shading/ispc/Util.isph @@ -564,13 +564,3 @@ sampleSphereUniform(varying float r1, varying float r2, float &theta, float &phi return result; } - -enum TEXTURE_GammaMode -{ - TEXTURE_GAMMA_OFF = 0, // Don't apply gamma correction - TEXTURE_GAMMA_ON = 1, // Always apply gamma correction - TEXTURE_GAMMA_AUTO = 2, // Apply gamma to 8-bit textures - TEXTURE_GAMMA_USD = 3 // Apply gamma to 8-bit textures unless they are single channel -}; - - diff --git a/tests/lib/rendering/shading/CMakeLists.txt b/tests/lib/rendering/shading/CMakeLists.txt index ebb3c926..fe49f0a8 100644 --- a/tests/lib/rendering/shading/CMakeLists.txt +++ b/tests/lib/rendering/shading/CMakeLists.txt @@ -45,6 +45,7 @@ get_target_property(ISPC_TARGET_OBJECTS ${objLib} TARGET_OBJECTS) target_sources(${target} PRIVATE main.cc + TestDielectricFresnel.cc TestHair.cc # pull in our ispc object files ${ISPC_TARGET_OBJECTS} diff --git a/tests/lib/rendering/shading/TestDielectricFresnel.cc b/tests/lib/rendering/shading/TestDielectricFresnel.cc new file mode 100644 index 00000000..2708ba40 --- /dev/null +++ b/tests/lib/rendering/shading/TestDielectricFresnel.cc @@ -0,0 +1,205 @@ +// Copyright 2023-2024 DreamWorks Animation LLC +// SPDX-License-Identifier: Apache-2.0 + +#include "TestDielectricFresnel.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace moonray { +namespace shading { + +namespace { + +using scene_rdl2::math::Color; +using scene_rdl2::math::Vec3f; + +float +radicalInverseBase2(unsigned bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xaaaaaaaau) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xccccccccu) >> 2u); + bits = ((bits & 0x0f0f0f0fu) << 4u) | ((bits & 0xf0f0f0f0u) >> 4u); + bits = ((bits & 0x00ff00ffu) << 8u) | ((bits & 0xff00ff00u) >> 8u); + return static_cast(bits) * 2.3283064365386963e-10f; +} + +float +integrateDirectionalReflectance(const GGXCookTorranceBsdfLobe& lobe, + float cosThetaO, + unsigned sampleCount) +{ + const float sinThetaO = std::sqrt(std::max(0.0f, 1.0f - cosThetaO * cosThetaO)); + const Vec3f wo(sinThetaO, 0.0f, cosThetaO); + const Vec3f n(0.0f, 0.0f, 1.0f); + const BsdfSlice slice(n, wo, true, true, ispc::SHADOW_TERMINATOR_FIX_OFF); + + double sum = 0.0; + for (unsigned i = 0; i < sampleCount; ++i) { + Vec3f wi; + float pdf = 0.0f; + const Color f = lobe.sample(slice, + (static_cast(i) + 0.5f) / sampleCount, + radicalInverseBase2(i), + wi, + pdf); + if (pdf > 0.0f) { + sum += f.r / pdf; + } + } + return static_cast(sum / sampleCount); +} + +struct RoughMetrics +{ + float directionalReflectance; + float hemisphericalReflectance; + float directionalReflectanceSingleScatter; + float hemisphericalReflectanceSingleScatter; + float peakBrdf; + float halfWidthDegrees; + float totalDirectionalReflectance; + float totalHemisphericalReflectance; +}; + +RoughMetrics +measureRoughDielectric(float roughness, float ior) +{ + const Vec3f n(0.0f, 0.0f, 1.0f); + const Color favg(averageFresnelReflectance(ior)); + GGXCookTorranceBsdfLobe lobe(n, roughness, favg, Color(0.0f), 1.0f, ior, false); + GGXCookTorranceBsdfLobe singleScatterLobe(n, roughness); + DielectricFresnel fresnel(1.0f, ior); + lobe.setFresnel(&fresnel); + singleScatterLobe.setFresnel(&fresnel); + + RoughMetrics result; + result.directionalReflectance = integrateDirectionalReflectance(lobe, 1.0f, 262144u); + result.directionalReflectanceSingleScatter = + integrateDirectionalReflectance(singleScatterLobe, 1.0f, 262144u); + + constexpr unsigned outgoingSteps = 128u; + constexpr unsigned incomingSamples = 16384u; + double specHemispherical = 0.0; + double specHemisphericalSingleScatter = 0.0; + double diffuseHemispherical = 0.0; + const OneMinusRoughFresnel diffuseAttenuation(&fresnel, roughness); + for (unsigned o = 0; o < outgoingSteps; ++o) { + const float cosThetaO = (static_cast(o) + 0.5f) / outgoingSteps; + const float directional = integrateDirectionalReflectance(lobe, cosThetaO, incomingSamples); + const float directionalSingleScatter = + integrateDirectionalReflectance(singleScatterLobe, cosThetaO, incomingSamples); + specHemispherical += 2.0 * directional * cosThetaO / outgoingSteps; + specHemisphericalSingleScatter += + 2.0 * directionalSingleScatter * cosThetaO / outgoingSteps; + diffuseHemispherical += 2.0 * diffuseAttenuation.eval(cosThetaO).r * cosThetaO / outgoingSteps; + } + result.hemisphericalReflectance = static_cast(specHemispherical); + result.hemisphericalReflectanceSingleScatter = + static_cast(specHemisphericalSingleScatter); + result.totalHemisphericalReflectance = + static_cast(specHemispherical + diffuseHemispherical); + + const BsdfSlice evalSlice(n, n, false, true, ispc::SHADOW_TERMINATOR_FIX_OFF); + constexpr unsigned angularSteps = 131072u; + result.peakBrdf = 0.0f; + result.halfWidthDegrees = 90.0f; + bool foundHalfWidth = false; + for (unsigned i = 0; i <= angularSteps; ++i) { + const float theta = scene_rdl2::math::sHalfPi * i / angularSteps; + const Vec3f wi(std::sin(theta), 0.0f, std::cos(theta)); + const float value = lobe.eval(evalSlice, wi).r; + result.peakBrdf = std::max(result.peakBrdf, value); + } + const float halfPeak = 0.5f * result.peakBrdf; + for (unsigned i = 0; i <= angularSteps; ++i) { + const float theta = scene_rdl2::math::sHalfPi * i / angularSteps; + const Vec3f wi(std::sin(theta), 0.0f, std::cos(theta)); + if (lobe.eval(evalSlice, wi).r <= halfPeak) { + result.halfWidthDegrees = 90.0f * i / angularSteps; + foundHalfWidth = true; + break; + } + } + CPPUNIT_ASSERT(foundHalfWidth || result.peakBrdf == 0.0f); + + const float normalDiffuse = diffuseAttenuation.eval(1.0f).r; + result.totalDirectionalReflectance = result.directionalReflectance + normalDiffuse; + return result; +} + +} // namespace + +void +TestDielectricFresnel::testNormalIncidenceFromAir() +{ + struct TestCase + { + float ior; + float expectedF0; + }; + + constexpr std::array testCases = {{ + {1.0f, 0.0f}, + {1.1f, 0.0022675737f}, + {1.3f, 0.0170132325f}, + {1.5f, 0.04f}, + {2.0f, 0.1111111111f}, + }}; + + for (const TestCase& testCase : testCases) { + const scene_rdl2::math::Color result = + DielectricFresnel::eval(1.0f, 1.0f, testCase.ior, 1.0f); + + CPPUNIT_ASSERT_DOUBLES_EQUAL(testCase.expectedF0, result.r, 1.0e-7f); + CPPUNIT_ASSERT_DOUBLES_EQUAL(testCase.expectedF0, result.g, 1.0e-7f); + CPPUNIT_ASSERT_DOUBLES_EQUAL(testCase.expectedF0, result.b, 1.0e-7f); + } +} + +void +TestDielectricFresnel::testRoughDielectricEnergy() +{ + constexpr std::array roughnessValues = {{0.1f, 0.25f, 0.5f, 0.8f}}; + constexpr std::array iorValues = {{1.0f, 1.3f, 1.5f, 2.0f}}; + + std::cout << "\nroughness,ior,R_directional_normal,R_hemispherical," + "R_directional_single_scatter,R_hemispherical_single_scatter,peak_brdf," + "half_width_degrees,total_directional_normal,total_hemispherical\n"; + std::cout << std::fixed << std::setprecision(8); + for (float roughness : roughnessValues) { + for (float ior : iorValues) { + const RoughMetrics result = measureRoughDielectric(roughness, ior); + std::cout << roughness << ',' << ior << ',' + << result.directionalReflectance << ',' + << result.hemisphericalReflectance << ',' + << result.directionalReflectanceSingleScatter << ',' + << result.hemisphericalReflectanceSingleScatter << ',' + << result.peakBrdf << ',' + << result.halfWidthDegrees << ',' + << result.totalDirectionalReflectance << ',' + << result.totalHemisphericalReflectance << '\n'; + + CPPUNIT_ASSERT(std::isfinite(result.directionalReflectance)); + CPPUNIT_ASSERT(std::isfinite(result.hemisphericalReflectance)); + CPPUNIT_ASSERT(result.directionalReflectance >= 0.0f); + CPPUNIT_ASSERT(result.hemisphericalReflectance >= 0.0f); + CPPUNIT_ASSERT(result.directionalReflectance <= 1.001f); + CPPUNIT_ASSERT(result.hemisphericalReflectance <= 1.001f); + CPPUNIT_ASSERT(result.totalDirectionalReflectance <= 1.005f); + CPPUNIT_ASSERT(result.totalHemisphericalReflectance <= 1.005f); + } + } +} + +CPPUNIT_TEST_SUITE_REGISTRATION(moonray::shading::TestDielectricFresnel); + +} // namespace shading +} // namespace moonray diff --git a/tests/lib/rendering/shading/TestDielectricFresnel.h b/tests/lib/rendering/shading/TestDielectricFresnel.h new file mode 100644 index 00000000..394fb2aa --- /dev/null +++ b/tests/lib/rendering/shading/TestDielectricFresnel.h @@ -0,0 +1,25 @@ +// Copyright 2023-2024 DreamWorks Animation LLC +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +namespace moonray { +namespace shading { + +class TestDielectricFresnel : public CppUnit::TestFixture +{ +public: + CPPUNIT_TEST_SUITE(TestDielectricFresnel); + CPPUNIT_TEST(testNormalIncidenceFromAir); + CPPUNIT_TEST(testRoughDielectricEnergy); + CPPUNIT_TEST_SUITE_END(); + + void testNormalIncidenceFromAir(); + void testRoughDielectricEnergy(); +}; + +} // namespace shading +} // namespace moonray