From 263f4c480c65b4ca5c120c44b81dd246be4464e5 Mon Sep 17 00:00:00 2001 From: Jakub Svoboda <132791205+rolledhand@users.noreply.github.com> Date: Sun, 31 May 2026 20:05:23 +0200 Subject: [PATCH 1/3] Houdini 20.5: harden primitive user data rates Signed-off-by: Jakub Svoboda <132791205+rolledhand@users.noreply.github.com> --- lib/rendering/geom/PrimitiveUserData.cc | 179 ++++++++++++------------ 1 file changed, 89 insertions(+), 90 deletions(-) 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 - From f030beafeeec87ec36f07e13a0050de183174326 Mon Sep 17 00:00:00 2001 From: Jakub Svoboda <132791205+rolledhand@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:04:29 +0200 Subject: [PATCH 2/3] Fix ARM half conversion in RenderOutputWriter Signed-off-by: Jakub Svoboda <132791205+rolledhand@users.noreply.github.com> --- lib/rendering/rndr/RenderOutputWriter.cc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 From e403e8021ca760580d09a9a648a1fd9731a27de2 Mon Sep 17 00:00:00 2001 From: Jakub Svoboda <132791205+rolledhand@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:06:07 +0200 Subject: [PATCH 3/3] Add config-driven OCIO texture source handling Signed-off-by: Jakub Svoboda <132791205+rolledhand@users.noreply.github.com> --- dso/map/Image/ImageMap.cc | 5 +- dso/map/Image/ImageMap.json | 7 + dso/map/UsdUVTexture/UsdUVTexture.cc | 32 ++- dso/map/UsdUVTexture/UsdUVTexture.json | 7 + lib/rendering/shading/BasicTexture.cc | 220 +++++++++++++++++++ lib/rendering/shading/BasicTexture.h | 2 +- lib/rendering/shading/CMakeLists.txt | 3 + lib/rendering/shading/UdimTexture.cc | 182 +++++++++++++++ lib/rendering/shading/UdimTexture.h | 2 +- lib/rendering/shading/ispc/BasicTexture.isph | 2 +- lib/rendering/shading/ispc/UdimTexture.isph | 2 +- 11 files changed, 458 insertions(+), 6 deletions(-) diff --git a/dso/map/Image/ImageMap.cc b/dso/map/Image/ImageMap.cc index cd68bbab..a3e45bd4 100644 --- a/dso/map/Image/ImageMap.cc +++ b/dso/map/Image/ImageMap.cc @@ -129,6 +129,7 @@ ImageMap::update() if (needsUpdate || hasChanged(attrTexture) || hasChanged(attrGamma) || + hasChanged(attrSourceColorSpace) || hasChanged(attrWrapAround) || hasChanged(attrUseDefaultColor) || hasChanged(attrDefaultColor)) { @@ -137,6 +138,7 @@ ImageMap::update() sLogEventRegistry, get(attrTexture), static_cast(get(attrGamma)), + get(attrSourceColorSpace), wrapS, wrapT, get(attrUseDefaultColor), @@ -162,12 +164,14 @@ 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 +419,3 @@ ImageMap::applyColorCorrection(Color& result) const } //--------------------------------------------------------------------------- - diff --git a/dso/map/Image/ImageMap.json b/dso/map/Image/ImageMap.json index 833e18a6..46994627 100644 --- a/dso/map/Image/ImageMap.json +++ b/dso/map/Image/ImageMap.json @@ -85,6 +85,13 @@ }, "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, 'raw' or 'data' to disable source conversion, or an explicit color-space name from the active OCIO config. When OCIO conversion is active, the legacy gamma mode is bypassed." + }, "attrOffset": { "name": "offset", "type": "Vec2f", diff --git a/dso/map/UsdUVTexture/UsdUVTexture.cc b/dso/map/UsdUVTexture/UsdUVTexture.cc index f588a841..6b52ab8a 100644 --- a/dso/map/UsdUVTexture/UsdUVTexture.cc +++ b/dso/map/UsdUVTexture/UsdUVTexture.cc @@ -21,6 +21,28 @@ static ispc::StaticUsdUVTextureData sStaticUsdUVTextureData; //---------------------------------------------------------------------------- +namespace { + +std::string +sourceColorSpaceFromUsdEnum(const int sourceColorSpace, const std::string& overrideValue) +{ + if (!overrideValue.empty()) { + return overrideValue; + } + + switch (sourceColorSpace) { + case ispc::TEXTURE_GAMMA_OFF: + return "raw"; + case ispc::TEXTURE_GAMMA_ON: + return "sRGB"; + case ispc::TEXTURE_GAMMA_USD: + default: + return "auto"; + } +} + +} // namespace + RDL2_DSO_CLASS_BEGIN(UsdUVTexture, scene_rdl2::rdl2::Map) public: @@ -73,6 +95,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 +128,8 @@ UsdUVTexture::update() if (needsUpdate || hasChanged(attrFile) || + hasChanged(attrSourceColorSpace) || + hasChanged(attrSourceColorSpaceOverride) || hasChanged(attrWrapS) || hasChanged(attrWrapT) || hasChanged(attrFallback)) { @@ -111,6 +138,7 @@ UsdUVTexture::update() sLogEventRegistry, filename, static_cast(get(attrSourceColorSpace)), + sourceColorSpace, wrapS, wrapT, true, // use default/fallback color @@ -135,12 +163,15 @@ 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..b3db35ab 100644 --- a/dso/map/UsdUVTexture/UsdUVTexture.json +++ b/dso/map/UsdUVTexture/UsdUVTexture.json @@ -94,6 +94,13 @@ "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." + }, + "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 the active OCIO file rules, 'raw' or 'data' to disable source conversion, or an explicit color-space name from the active OCIO config. When OCIO conversion is active, the legacy gamma mode is bypassed." } } } diff --git a/lib/rendering/shading/BasicTexture.cc b/lib/rendering/shading/BasicTexture.cc index 31c070c4..7f8f6953 100644 --- a/lib/rendering/shading/BasicTexture.cc +++ b/lib/rendering/shading/BasicTexture.cc @@ -14,6 +14,10 @@ #include #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 @@ -23,8 +27,14 @@ #include +#include +#include #include #include +#include +#include + +namespace OCIO = OCIO_NAMESPACE; namespace moonray { namespace shading { @@ -72,6 +82,192 @@ getOIIOWrap(WrapType wrapType) { } // namespace +namespace { + +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 +isRawColorSpace(const std::string& value) +{ + const std::string key = normalized(value); + return key.empty() || key == "raw" || key == "data" || key == "none"; +} + +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 { + std::string name = colorSpaceName(config->getColorSpace(token.c_str())); + if (!name.empty()) { + return name; + } + } catch (const OCIO::Exception&) { + } + return {}; +} + +std::string +roleColorSpace(const OCIO::ConstConfigRcPtr& config, const char* role) +{ + return resolveColorSpace(config, role ? role : ""); +} + +std::string +renderingColorSpace(const OCIO::ConstConfigRcPtr& config) +{ + for (const char* role : {OCIO::ROLE_RENDERING, + OCIO::ROLE_SCENE_LINEAR, + "default_float", + "reference", + OCIO::ROLE_DEFAULT}) { + std::string name = roleColorSpace(config, role); + if (!name.empty()) { + return name; + } + } + return {}; +} + +std::string +sourceColorSpaceForTexture(const OCIO::ConstConfigRcPtr& config, + const std::string& filename, + const std::string& authoredSource) +{ + std::string source = trim(authoredSource); + const std::string key = normalized(source); + + if (isRawColorSpace(source)) { + return {}; + } + + if (key == "auto") { + if (!config) { + return {}; + } + try { + const char* resolved = config->getColorSpaceFromFilepath(filename.c_str()); + if (resolved && !isRawColorSpace(resolved)) { + return resolveColorSpace(config, resolved); + } + } catch (const OCIO::Exception&) { + return {}; + } + return {}; + } + + if (config) { + std::string resolved = resolveColorSpace(config, source); + if (!resolved.empty() && !isRawColorSpace(resolved)) { + return resolved; + } + } + return {}; +} + +OCIO::ConstCPUProcessorRcPtr +createTextureProcessor(const std::string& filename, + const std::string& sourceColorSpace, + std::string* diagnostic) +{ + if (diagnostic) { + diagnostic->clear(); + } + + OCIO::ConstConfigRcPtr config; + try { + config = OCIO::GetCurrentConfig(); + } catch (const OCIO::Exception& e) { + if (diagnostic) { + *diagnostic = std::string("OCIO config load failed: ") + e.what(); + } + return {}; + } + + const std::string source = sourceColorSpaceForTexture(config, filename, sourceColorSpace); + if (source.empty()) { + const std::string key = normalized(trim(sourceColorSpace)); + if (diagnostic && !isRawColorSpace(sourceColorSpace) && key != "auto") { + std::ostringstream out; + out << "unsupported source_color_space=\"" << sourceColorSpace + << "\" for active OCIO config; source conversion disabled"; + *diagnostic = out.str(); + } + return {}; + } + + const std::string target = renderingColorSpace(config); + if (target.empty() || source == target) { + return {}; + } + + try { + OCIO::ConstProcessorRcPtr processor = + config->getProcessor(source.c_str(), target.c_str()); + if (diagnostic) { + std::ostringstream out; + out << "source=" << source << " target=" << target; + *diagnostic = out.str(); + } + return processor ? processor->getDefaultCPUProcessor() : OCIO::ConstCPUProcessorRcPtr(); + } catch (const OCIO::Exception& e) { + if (diagnostic) { + std::ostringstream out; + out << "failed source=" << source << " target=" << target << ": " << e.what(); + *diagnostic = out.str(); + } + } + return {}; +} + +void +applyOcioProcessor(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 + static ispc::BASIC_TEXTURE_StaticData sBasicTextureStaticData; class BasicTexture::Impl @@ -131,6 +327,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 +336,7 @@ class BasicTexture::Impl std::string &errorMsg) { init(); + mSourceColorSpace = sourceColorSpace; mIspc.mUseDefaultColor = useDefaultColor; mIspc.mDefaultColor.r = defaultColor.r; @@ -148,6 +346,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]); @@ -211,6 +411,18 @@ class BasicTexture::Impl mIspc.mTextureOptions = (intptr_t) mTextureOpt; mIspc.mApplyGamma = getApplyGamma(gammaMode, spec.nchannels); mIspc.mIs8bit = (spec.format == OIIO::TypeDesc::UINT8); + std::string ocioDiagnostic; + mOcioProcessor = createTextureProcessor(filename, mSourceColorSpace, &ocioDiagnostic); + if (!ocioDiagnostic.empty() && + (ocioDiagnostic.find("unsupported") == 0 || + ocioDiagnostic.find("failed") == 0 || + ocioDiagnostic.find("OCIO config load failed") == 0)) { + scene_rdl2::logging::Logger::warn("ImageMap OCIO: ", ocioDiagnostic); + } + mIspc.mOcioProcessor = reinterpret_cast(mOcioProcessor.get()); + if (mOcioProcessor) { + mIspc.mApplyGamma = false; + } mIspc.mIsValid = true; if (mIspc.mUseDefaultColor) { @@ -263,6 +475,7 @@ class BasicTexture::Impl tmp[2] = tmp[2] > 0.0f ? powf(tmp[2], 2.2f) : 0.0f; // don't gamma the alpha channel } + applyOcioProcessor(mIspc.mOcioProcessor, tmp); result[0] = tmp[0]; result[1] = tmp[1]; result[2] = tmp[2]; @@ -287,7 +500,9 @@ class BasicTexture::Impl mIspc.mApplyGamma = false; mIspc.mIs8bit = false; + mIspc.mOcioProcessor = 0; mIspc.mIsValid = false; + mOcioProcessor.reset(); mWidth = 0; mHeight = 0; mPixelAspectRatio = 0.0f; @@ -310,6 +525,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; @@ -332,6 +549,7 @@ BasicTexture::~BasicTexture() bool BasicTexture::update(const std::string &filename, ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -341,6 +559,7 @@ BasicTexture::update(const std::string &filename, { return mImpl->update(filename, gammaMode, + sourceColorSpace, wrapS, wrapT, useDefaultColor, @@ -429,6 +648,7 @@ void CPP_oiioTexture(const ispc::BASIC_TEXTURE_Data *tx, result[2] = pow(result[2], 2.2f); // don't gamma the alpha channel } + applyOcioProcessor(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..91e9da58 100644 --- a/lib/rendering/shading/BasicTexture.h +++ b/lib/rendering/shading/BasicTexture.h @@ -24,6 +24,7 @@ class BasicTexture { bool update(const std::string &filename, ispc::TEXTURE_GammaMode gammaMode, + const std::string& sourceColorSpace, WrapType wrapS, WrapType wrapT, bool useDefaultColor, @@ -63,4 +64,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..c9bac7fd 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) @@ -116,6 +118,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/UdimTexture.cc b/lib/rendering/shading/UdimTexture.cc index 9af3ab09..7058c406 100644 --- a/lib/rendering/shading/UdimTexture.cc +++ b/lib/rendering/shading/UdimTexture.cc @@ -14,6 +14,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 @@ -27,11 +29,14 @@ #include #include +#include #include #include #include #include +namespace OCIO = OCIO_NAMESPACE; + namespace moonray { namespace shading { @@ -117,6 +122,166 @@ getUdimFilenames(const std::string& filename, } // namespace +namespace { + +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 +isRawColorSpace(const std::string& value) +{ + const std::string key = normalized(value); + return key.empty() || key == "raw" || key == "data" || key == "none"; +} + +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 { + std::string name = colorSpaceName(config->getColorSpace(token.c_str())); + if (!name.empty()) { + return name; + } + } catch (const OCIO::Exception&) { + } + return {}; +} + +std::string +roleColorSpace(const OCIO::ConstConfigRcPtr& config, const char* role) +{ + return resolveColorSpace(config, role ? role : ""); +} + +std::string +renderingColorSpace(const OCIO::ConstConfigRcPtr& config) +{ + for (const char* role : {OCIO::ROLE_RENDERING, + OCIO::ROLE_SCENE_LINEAR, + "default_float", + "reference", + OCIO::ROLE_DEFAULT}) { + std::string name = roleColorSpace(config, role); + if (!name.empty()) { + return name; + } + } + return {}; +} + +std::string +sourceColorSpaceForTexture(const OCIO::ConstConfigRcPtr& config, + const std::string& filename, + const std::string& authoredSource) +{ + std::string source = trim(authoredSource); + const std::string key = normalized(source); + + if (isRawColorSpace(source)) { + return {}; + } + + if (key == "auto") { + if (!config) { + return {}; + } + try { + const char* resolved = config->getColorSpaceFromFilepath(filename.c_str()); + if (resolved && !isRawColorSpace(resolved)) { + return resolveColorSpace(config, resolved); + } + } catch (const OCIO::Exception&) { + } + return {}; + } + + if (config) { + std::string resolved = resolveColorSpace(config, source); + if (!resolved.empty() && !isRawColorSpace(resolved)) { + return resolved; + } + } + return {}; +} + +OCIO::ConstCPUProcessorRcPtr +createTextureProcessor(const std::string& filename, + const std::string& sourceColorSpace) +{ + OCIO::ConstConfigRcPtr config; + try { + config = OCIO::GetCurrentConfig(); + } catch (const OCIO::Exception&) { + return {}; + } + + const std::string source = sourceColorSpaceForTexture(config, filename, sourceColorSpace); + if (source.empty()) { + return {}; + } + + const std::string target = renderingColorSpace(config); + if (target.empty() || source == target) { + return {}; + } + + try { + OCIO::ConstProcessorRcPtr processor = + config->getProcessor(source.c_str(), target.c_str()); + return processor ? processor->getDefaultCPUProcessor() : OCIO::ConstCPUProcessorRcPtr(); + } catch (const OCIO::Exception&) { + } + return {}; +} + +void +applyOcioProcessor(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 + static ispc::UDIM_TEXTURE_StaticData sUdimTextureStaticData; class UdimTexture::Impl @@ -195,6 +360,7 @@ class UdimTexture::Impl 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 +369,7 @@ class UdimTexture::Impl std::string &errorMsg) { init(); + mSourceColorSpace = sourceColorSpace; mIspc.mUseDefaultColor = useDefaultColor; mIspc.mDefaultColor.r = defaultColor.r; @@ -211,6 +378,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)) { @@ -276,6 +445,11 @@ class UdimTexture::Impl } mIspc.mApplyGamma = applyGamma; mIspc.mIs8bit = mIs8bit; + mOcioProcessor = createTextureProcessor(filename, mSourceColorSpace); + mIspc.mOcioProcessor = reinterpret_cast(mOcioProcessor.get()); + if (mOcioProcessor) { + mIspc.mApplyGamma = false; + } tbb::mutex errorMutex; @@ -366,6 +540,7 @@ class UdimTexture::Impl tmp[2] = tmp[2] > 0.0f ? powf(tmp[2], 2.2f) : 0.0f; // don't gamma the alpha channel } + applyOcioProcessor(mIspc.mOcioProcessor, tmp); result[0] = tmp[0]; result[1] = tmp[1]; result[2] = tmp[2]; @@ -396,7 +571,9 @@ class UdimTexture::Impl mIspc.mApplyGamma = false; mIspc.mIs8bit = false; + mIspc.mOcioProcessor = 0; mIspc.mIsValid = false; + mOcioProcessor.reset(); } bool @@ -592,6 +769,8 @@ 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; @@ -631,6 +810,7 @@ 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, @@ -642,6 +822,7 @@ UdimTexture::update(scene_rdl2::rdl2::Shader *shader, logEventRegistry, filename, gammaMode, + sourceColorSpace, wrapS, wrapT, useDefaultColor, @@ -770,6 +951,7 @@ void CPP_oiioUdimTexture(const ispc::UDIM_TEXTURE_Data *tx, result[2] = result[2] > 0.0f ? powf(result[2], 2.2f) : 0.0f; // don't gamma the alpha channel } + applyOcioProcessor(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..036af6e8 100644 --- a/lib/rendering/shading/UdimTexture.h +++ b/lib/rendering/shading/UdimTexture.h @@ -24,6 +24,7 @@ class UdimTexture { 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 +74,3 @@ void CPP_oiioUdimTexture(const ispc::UDIM_TEXTURE_Data* tx, } // namespace shading } // namespace moonray - diff --git a/lib/rendering/shading/ispc/BasicTexture.isph b/lib/rendering/shading/ispc/BasicTexture.isph index 4e339fa2..3f3cdc67 100644 --- a/lib/rendering/shading/ispc/BasicTexture.isph +++ b/lib/rendering/shading/ispc/BasicTexture.isph @@ -36,6 +36,7 @@ struct BASIC_TEXTURE_Data uniform intptr_t mTextureOptions; uniform bool mApplyGamma; uniform bool mIs8bit; + uniform intptr_t mOcioProcessor; }; Col4f @@ -65,4 +66,3 @@ BASIC_TEXTURE_getDimensions( float BASIC_TEXTURE_getPixelAspectRatio(const uniform BASIC_TEXTURE_Data * uniform tx); - diff --git a/lib/rendering/shading/ispc/UdimTexture.isph b/lib/rendering/shading/ispc/UdimTexture.isph index 90dbf68b..a9bb9695 100644 --- a/lib/rendering/shading/ispc/UdimTexture.isph +++ b/lib/rendering/shading/ispc/UdimTexture.isph @@ -39,6 +39,7 @@ struct UDIM_TEXTURE_Data uniform intptr_t mTextureOptions; uniform bool mApplyGamma; uniform bool mIs8bit; + uniform intptr_t mOcioProcessor; }; // returns -1 if out of range @@ -71,4 +72,3 @@ UDIM_TEXTURE_getPixelAspectRatio( const uniform UDIM_TEXTURE_Data * uniform tx, int udim); -