diff --git a/pxr/usd/usdShade/plugInfo.json b/pxr/usd/usdShade/plugInfo.json index c7dcb46998..8b5ef61ae3 100644 --- a/pxr/usd/usdShade/plugInfo.json +++ b/pxr/usd/usdShade/plugInfo.json @@ -149,7 +149,10 @@ }, "MaterialBindingRelationships": { "doc": "All properties named 'material:binding' or in that namespace should be relationships." - }, + }, + "NormalMapTextureValidator" : { + "doc": "UsdUVTexture nodes that feed the _inputs:normals_ of a UsdPreviewSurface must ensure that the data is encoded and scaled properly. Specifically, since normals are expected to be in the range [(-1,-1,-1), (1,1,1)], the Texture node must transform 8-bit textures from their [0..1] range by setting its _inputs:scale_ to (2, 2, 2, 1) and _inputs:bias_ to (-1, -1, -1, 0). Normal map data is commonly expected to be linearly encoded. However, many image-writing tools automatically set the profile of three-channel, 8-bit images to SRGB. To prevent an unwanted transformation, the UsdUVTexture's _inputs:sourceColorSpace_ must be set to raw." + }, "ShaderSdrCompliance": { "doc": "Shader prim's input types must be conforming to their appropriate sdf types in the respective sdr shader.", "schemaTypes": [ diff --git a/pxr/usd/usdShade/testenv/testUsdShadeValidators.cpp b/pxr/usd/usdShade/testenv/testUsdShadeValidators.cpp index fb54cd9f1a..d4b41176c2 100644 --- a/pxr/usd/usdShade/testenv/testUsdShadeValidators.cpp +++ b/pxr/usd/usdShade/testenv/testUsdShadeValidators.cpp @@ -17,6 +17,7 @@ #include "pxr/usd/usd/validator.h" #include "pxr/usd/usdGeom/validatorTokens.h" #include "pxr/usd/usdGeom/scope.h" +#include "pxr/usd/usdGeom/xform.h" #include "pxr/usd/usdShade/shader.h" #include "pxr/usd/usdShade/shaderDefUtils.h" #include "pxr/usd/usdShade/tokens.h" @@ -41,17 +42,18 @@ TestUsdShadeValidators() // This should be updated with every new validator added with the // UsdShadeValidators keyword. const std::set expectedUsdShadeValidatorNames = { - UsdShadeValidatorNameTokens->encapsulationValidator, UsdShadeValidatorNameTokens->materialBindingApiAppliedValidator, UsdShadeValidatorNameTokens->materialBindingRelationships, UsdShadeValidatorNameTokens->materialBindingCollectionValidator, + UsdShadeValidatorNameTokens->normalMapTextureValidator, UsdShadeValidatorNameTokens->shaderSdrCompliance, UsdShadeValidatorNameTokens->subsetMaterialBindFamilyName, - UsdShadeValidatorNameTokens->subsetsMaterialBindFamily + UsdShadeValidatorNameTokens->subsetsMaterialBindFamily, + UsdShadeValidatorNameTokens->encapsulationValidator }; const UsdValidationRegistry& registry = - UsdValidationRegistry::GetInstance(); + UsdValidationRegistry::GetInstance(); // Since other validators can be registered with the same keywords, // our validators registered in usdShade are/may be a subset of the @@ -60,7 +62,7 @@ TestUsdShadeValidators() UsdValidatorMetadataVector metadata = registry.GetValidatorMetadataForPlugin(_tokens->usdShadePlugin); - TF_AXIOM(metadata.size() == 7); + TF_AXIOM(metadata.size() == 8); for (const UsdValidatorMetadata& metadata : metadata) { validatorMetadataNameSet.insert(metadata.name); } @@ -557,6 +559,213 @@ TestUsdShadeEncapsulationRulesValidator() } } +void ValidateError(const UsdValidationErrorVector& errors, + const TfToken& expectedErrorIdentifier, + const SdfPath& expectedPrimPath, + const std::string& expectedErrorMsg, + UsdValidationErrorType expectedErrorType = UsdValidationErrorType::Error) +{ + TF_AXIOM(errors.size() == 1); + TF_AXIOM(errors[0].GetIdentifier() == expectedErrorIdentifier); + TF_AXIOM(errors[0].GetType() == expectedErrorType); + TF_AXIOM(errors[0].GetSites().size() == 1); + TF_AXIOM(errors[0].GetSites()[0].IsValid()); + TF_AXIOM(errors[0].GetSites()[0].IsPrim()); + TF_AXIOM(errors[0].GetSites()[0].GetPrim().GetPath() == + expectedPrimPath); + TF_AXIOM(errors[0].GetMessage() == expectedErrorMsg); +} + +void +TestUsdShadeNormalMapTextureValidator() +{ + UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance(); + const UsdValidator *validator = registry.GetOrLoadValidatorByName( + UsdShadeValidatorNameTokens->normalMapTextureValidator); + TF_AXIOM(validator); + + // Create a Stage, Material, and Two Shaders (UsdPreviewSurface, + // UsdUVTexture). + UsdStageRefPtr usdStage = UsdStage::CreateInMemory(); + UsdShadeMaterial material = UsdShadeMaterial::Define(usdStage, + SdfPath("/RootMaterial")); + + const std::string usdPreviewSurfaceShaderPath = + "/RootMaterial/UsdPreviewSurface"; + UsdShadeShader usdPreviewSurfaceShader = UsdShadeShader::Define( + usdStage, SdfPath(usdPreviewSurfaceShaderPath)); + usdPreviewSurfaceShader.CreateIdAttr( + VtValue(TfToken("UsdPreviewSurface"))); + UsdPrim usdPreviewSurfaceShaderPrim = usdPreviewSurfaceShader.GetPrim(); + + UsdShadeShader usdUvTextureShader = UsdShadeShader::Define( + usdStage, SdfPath("/RootMaterial/NormalTexture")); + usdUvTextureShader.CreateIdAttr(VtValue(TfToken("UsdUVTexture"))); + + // Add initial valid file and sourceColorSpace input values. + std::string textureAssetPath = "./normalMap.jpg"; + UsdShadeInput fileInput = usdUvTextureShader.CreateInput( + TfToken("file"), SdfValueTypeNames->Asset); + fileInput.Set(SdfAssetPath(textureAssetPath)); + UsdShadeInput sourceColorSpaceInput = usdUvTextureShader.CreateInput( + TfToken("sourceColorSpace"), SdfValueTypeNames->Token); + const TfToken rawToken("raw"); + sourceColorSpaceInput.Set(rawToken); + + // Connect the output of the UsdUVTexture Shader to the normal of the + // UsdPreviewSurface Shader. + usdUvTextureShader.CreateOutput(TfToken("rgb"), SdfValueTypeNames->Float3); + UsdShadeInput normalInput = usdPreviewSurfaceShader.CreateInput( + TfToken("normal"), SdfValueTypeNames->Normal3f); + normalInput.ConnectToSource( + SdfPath("/RootMaterial/NormalTexture.outputs:rgb")); + + // Verify invalid bias & scale error, they should exists and do + // not exist at this point. + UsdValidationErrorVector errors = validator->Validate( + usdPreviewSurfaceShaderPrim); + TfToken expectedErrorIdentifier( + "usdShade:NormalMapTextureValidator.NonCompliantBiasAndScale"); + std::string expectedErrorMsg = + TfStringPrintf("UsdUVTexture prim <%s> reads 8 bit Normal Map " + "@./normalMap.jpg@, which requires that " + "inputs:scale be set to (2, 2, 2, 1) and " + "inputs:bias be set to (-1, -1, -1, 0) for proper " + "interpretation as per the UsdPreviewSurface and " + "UsdUVTexture docs.", + usdUvTextureShader.GetPath().GetText()); + ValidateError(errors, + expectedErrorIdentifier, + usdUvTextureShader.GetPath(), + expectedErrorMsg); + + // Add bias and scale, but add a non-compliant bias value. + UsdShadeInput biasInput = usdUvTextureShader.CreateInput( + TfToken("bias"), SdfValueTypeNames->Float4); + const GfVec4f compliantBias = GfVec4f(-1, -1, -1, 0); + const GfVec4f nonCompliantVector = GfVec4f(-9, -9, -9, -9); + biasInput.Set(nonCompliantVector); + UsdShadeInput scaleInput = usdUvTextureShader.CreateInput( + TfToken("scale"), SdfValueTypeNames->Float4); + const GfVec4f compliantScale = GfVec4f(2, 2, 2, 1); + scaleInput.Set(compliantScale); + + // Verify the non-compliant bias value error occurs. + errors = validator->Validate(usdPreviewSurfaceShaderPrim); + expectedErrorIdentifier = TfToken( + "usdShade:NormalMapTextureValidator.NonCompliantBiasValues"); + expectedErrorMsg = + TfStringPrintf("UsdUVTexture prim <%s> reads an 8 bit Normal " + "Map, but has non-standard inputs:bias value of " + "(%.6g, %.6g, %.6g, %.6g). inputs:bias must be set to " + "[-1,-1,-1,0] so as to fulfill the requirements " + "of the normals to be in tangent space of " + "[(-1,-1,-1), (1,1,1)] as documented in the " + "UsdPreviewSurface and UsdUVTexture docs.", + usdUvTextureShader.GetPath().GetText(), + nonCompliantVector[0], nonCompliantVector[1], + nonCompliantVector[2], nonCompliantVector[3]); + ValidateError(errors, + expectedErrorIdentifier, + usdUvTextureShader.GetPath(), + expectedErrorMsg); + + // Update to a compliant bias and a non-compliant scale value. + biasInput.Set(compliantBias); + scaleInput.Set(nonCompliantVector); + + // Verify the non-compliant scale value error occurs. + errors = validator->Validate(usdPreviewSurfaceShaderPrim); + expectedErrorIdentifier = TfToken( + "usdShade:NormalMapTextureValidator.NonCompliantScaleValues"); + expectedErrorMsg = + TfStringPrintf("UsdUVTexture prim <%s> reads an 8 bit Normal " + "Map, but has non-standard inputs:scale value " + "of (%.6g, %.6g, %.6g, %.6g). inputs:scale must " + "be set to (2, 2, 2, 1) so as fulfill the " + "requirements of the normals to be in tangent " + "space of [(-1,-1,-1), (1,1,1)] as documented in " + "the UsdPreviewSurface and UsdUVTexture docs.", + usdUvTextureShader.GetPath().GetText(), + nonCompliantVector[0], nonCompliantVector[1], + nonCompliantVector[2], nonCompliantVector[3]); + ValidateError(errors, + expectedErrorIdentifier, + usdUvTextureShader.GetPath(), + expectedErrorMsg, + UsdValidationErrorType::Warn); + + // Set a compliant scale value, and an invalid sourceColorSpace. + scaleInput.Set(compliantScale); + sourceColorSpaceInput.Set(TfToken("error")); + + // Verify the invalid sourceColorSpace error occurs. + errors = validator->Validate(usdPreviewSurfaceShaderPrim); + expectedErrorIdentifier = TfToken( + "usdShade:NormalMapTextureValidator.InvalidSourceColorSpace"); + expectedErrorMsg = + TfStringPrintf("UsdUVTexture prim <%s> that reads" + " Normal Map @%s@ should set " + "inputs:sourceColorSpace to 'raw'.", + usdUvTextureShader.GetPath().GetText(), + textureAssetPath.c_str()); + ValidateError(errors, + expectedErrorIdentifier, + usdUvTextureShader.GetPath(), + expectedErrorMsg); + + // Correct the sourceColorSpace, hook up the normal input of + // UsdPreviewSurface to a non-shader output. + sourceColorSpaceInput.Set(rawToken); + UsdGeomXform nonShaderPrim = UsdGeomXform::Define( + usdStage, SdfPath("/RootMaterial/Xform")); + UsdShadeConnectableAPI connectableNonShaderAPI(nonShaderPrim.GetPrim()); + UsdShadeOutput nonShaderOutput = connectableNonShaderAPI.CreateOutput( + TfToken("myOutput"), SdfValueTypeNames->Float3); + nonShaderOutput.Set(GfVec3f(1.0f, 2.0f, 3.0f)); + normalInput.ConnectToSource(nonShaderOutput); + + // Verify a non-shader connection error occurs. + errors = validator->Validate(usdPreviewSurfaceShaderPrim); + expectedErrorIdentifier = TfToken( + "usdShade:NormalMapTextureValidator.NonShaderConnection"); + expectedErrorMsg = + TfStringPrintf("UsdPreviewSurface.normal on prim <%s> is connected " + "to a non-Shader prim.", + usdPreviewSurfaceShaderPath.c_str()); + ValidateError(errors, + expectedErrorIdentifier, + usdPreviewSurfaceShader.GetPath(), + expectedErrorMsg); + + // Set the normal input back to a valid shader and update the file input + // to an invalid file path. + normalInput.ConnectToSource( + SdfPath("/RootMaterial/NormalTexture.outputs:rgb")); + fileInput.Set(SdfAssetPath("./doesNotExist.jpg")); + + // Verify the invalid input file error occurs. + errors = validator->Validate(usdPreviewSurfaceShaderPrim); + expectedErrorIdentifier = + TfToken("usdShade:NormalMapTextureValidator.InvalidFile"); + expectedErrorMsg = + TfStringPrintf("UsdUVTexture prim <%s> has invalid or unresolvable " + "inputs:file of @%s@", + usdUvTextureShader.GetPath().GetText(), + "./doesNotExist.jpg"); + ValidateError(errors, + expectedErrorIdentifier, + usdUvTextureShader.GetPath(), + expectedErrorMsg); + + // Reset the file to a valid path. + fileInput.Set(SdfAssetPath("./normalMap.jpg")); + + // Verify no errors exist. + errors = validator->Validate(usdPreviewSurfaceShaderPrim); + TF_AXIOM(errors.empty()); +} + int main() { @@ -568,6 +777,7 @@ main() TestUsdShadeSubsetMaterialBindFamilyName(); TestUsdShadeSubsetsMaterialBindFamily(); TestUsdShadeEncapsulationRulesValidator(); + TestUsdShadeNormalMapTextureValidator(); return EXIT_SUCCESS; }; diff --git a/pxr/usd/usdShade/testenv/testUsdShadeValidators/normalMap.jpg b/pxr/usd/usdShade/testenv/testUsdShadeValidators/normalMap.jpg new file mode 100644 index 0000000000..562ac9304c Binary files /dev/null and b/pxr/usd/usdShade/testenv/testUsdShadeValidators/normalMap.jpg differ diff --git a/pxr/usd/usdShade/validatorTokens.h b/pxr/usd/usdShade/validatorTokens.h index afc2a909b6..2ec0507a33 100644 --- a/pxr/usd/usdShade/validatorTokens.h +++ b/pxr/usd/usdShade/validatorTokens.h @@ -17,12 +17,13 @@ PXR_NAMESPACE_OPEN_SCOPE #define USD_SHADE_VALIDATOR_NAME_TOKENS \ - ((encapsulationValidator, "usdShade:EncapsulationRulesValidator")) \ - ((materialBindingApiAppliedValidator, "usdShade:MaterialBindingApiAppliedValidator")) \ - ((materialBindingRelationships, "usdShade:MaterialBindingRelationships")) \ - ((materialBindingCollectionValidator, "usdShade:MaterialBindingCollectionValidator")) \ - ((shaderSdrCompliance, "usdShade:ShaderSdrCompliance")) \ - ((subsetMaterialBindFamilyName, "usdShade:SubsetMaterialBindFamilyName")) \ + ((encapsulationValidator, "usdShade:EncapsulationRulesValidator")) \ + ((materialBindingApiAppliedValidator, "usdShade:MaterialBindingApiAppliedValidator")) \ + ((materialBindingRelationships, "usdShade:MaterialBindingRelationships")) \ + ((materialBindingCollectionValidator, "usdShade:MaterialBindingCollectionValidator")) \ + ((normalMapTextureValidator, "usdShade:NormalMapTextureValidator")) \ + ((shaderSdrCompliance, "usdShade:ShaderSdrCompliance")) \ + ((subsetMaterialBindFamilyName, "usdShade:SubsetMaterialBindFamilyName")) \ ((subsetsMaterialBindFamily, "usdShade:SubsetsMaterialBindFamily")) #define USD_SHADE_VALIDATOR_KEYWORD_TOKENS \ @@ -43,6 +44,13 @@ PXR_NAMESPACE_OPEN_SCOPE ((mismatchPropertyType, "MismatchedPropertyType")) \ ((missingFamilyNameOnGeomSubset, "MissingFamilyNameOnGeomSubset")) \ ((invalidFamilyType, "InvalidFamilyType")) \ + ((nonShaderConnection, "NonShaderConnection")) \ + ((invalidFile, "InvalidFile")) \ + ((invalidShaderPrim, "InvalidShaderPrim")) \ + ((invalidSourceColorSpace, "InvalidSourceColorSpace")) \ + ((nonCompliantBiasAndScale, "NonCompliantBiasAndScale")) \ + ((nonCompliantScale, "NonCompliantScaleValues")) \ + ((nonCompliantBias, "NonCompliantBiasValues")) \ /// \def USD_SHADE_VALIDATOR_NAME_TOKENS /// Tokens representing validator names. Note that for plugin provided diff --git a/pxr/usd/usdShade/validators.cpp b/pxr/usd/usdShade/validators.cpp index 49656e6d4e..ae0a0392bb 100644 --- a/pxr/usd/usdShade/validators.cpp +++ b/pxr/usd/usdShade/validators.cpp @@ -21,15 +21,17 @@ #include "pxr/usd/usdShade/shader.h" #include "pxr/usd/usdShade/tokens.h" #include "pxr/usd/usdShade/validatorTokens.h" +#include "pxr/usd/usdShade/connectableAPI.h" #include "pxr/usd/usdShade/materialBindingAPI.h" +#include "pxr/usd/ar/resolver.h" #include #include #include -PXR_NAMESPACE_OPEN_SCOPE -static +PXR_NAMESPACE_OPEN_SCOPE + static UsdValidationErrorVector _EncapsulationValidator(const UsdPrim& usdPrim) { @@ -590,6 +592,234 @@ _SubsetsMaterialBindFamily(const UsdPrim& usdPrim) return errors; } +static +UsdValidationErrorVector +_NormalMapTextureValidator(const UsdPrim& usdPrim) { + + if (!usdPrim.IsA()) { + return {}; + } + + const UsdShadeShader shader(usdPrim); + + TfToken shaderId; + TfToken UsdPreviewSurface("UsdPreviewSurface"); + + // We may have failed to fetch an identifier for asset/source-based + // nodes. OR, we could potentially be driven by a UsdPrimvarReader, + // in which case we'd have nothing to validate + if (!shader.GetShaderId(&shaderId) || shaderId != UsdPreviewSurface) { + return {}; + } + + const UsdShadeInput normalInput = shader.GetInput(TfToken("normal")); + if (!normalInput) { + return {}; + } + + const UsdShadeAttributeVector valueProducingAttributes = UsdShadeUtils::GetValueProducingAttributes(normalInput); + if (valueProducingAttributes.empty() || valueProducingAttributes[0].GetPrim() == usdPrim) { + return {}; + } + + const UsdPrim sourcePrim = valueProducingAttributes[0].GetPrim(); + UsdShadeShader sourceShader(sourcePrim); + if (!sourceShader) { + // In theory, could be connected to an interface attribute of a + // parent connectable... not useful, but not an error + const UsdShadeConnectableAPI& connectable = + UsdShadeConnectableAPI(sourcePrim); + + if (connectable){ + return {}; + } + + return { + UsdValidationError{ + UsdShadeValidationErrorNameTokens->nonShaderConnection, + UsdValidationErrorType::Error, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), + usdPrim.GetPath()) + }, + TfStringPrintf("UsdPreviewSurface.normal on prim <%s> is connected to a" + " non-Shader prim.", + usdPrim.GetPath().GetText()) + } + }; + } + + TfToken sourceShaderId; + TfToken UsdUVTexture("UsdUVTexture"); + + bool gotShaderSourceId = sourceShader.GetShaderId(&sourceShaderId); + + // We may have failed to fetch an identifier for asset/source-based + // nodes. OR, we could potentially be driven by a UsdPrimvarReader, + // in which case we'd have nothing to validate + if (!gotShaderSourceId || sourceShaderId != UsdUVTexture) { + return {}; + } + + const auto getInputValue = [](const UsdShadeShader &inputShader, const TfToken &token, auto& outputValue) -> bool { + const UsdShadeInput input = inputShader.GetInput(token); + if (!input) { + return false; + } + + const UsdShadeAttributeVector valueProducingAttributes = + UsdShadeUtils::GetValueProducingAttributes(input); + + // Query value producing attributes for input values. + // This has to be a length of 1, otherwise no attribute is producing a value. + // We require an input parameter producing the value. + if (valueProducingAttributes.empty() || + valueProducingAttributes.size() != 1 || + !UsdShadeInput::IsInput(valueProducingAttributes[0])) { + return false; + } + + return valueProducingAttributes[0].Get(&outputValue, + UsdTimeCode::EarliestTime()); + }; + + SdfAssetPath textureAssetPath; + bool valueForFileExists = getInputValue(sourceShader, TfToken("file"), + textureAssetPath); + + UsdValidationErrorVector errors; + + if (!valueForFileExists || textureAssetPath.GetResolvedPath().empty()) { + std::string assetPath = !textureAssetPath.GetAssetPath().empty() + ? textureAssetPath.GetAssetPath() + : ""; + errors.emplace_back( + UsdShadeValidationErrorNameTokens->invalidFile, + UsdValidationErrorType::Error, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), + sourcePrim.GetPath()) + }, + TfStringPrintf("UsdUVTexture prim <%s> has invalid or unresolvable " + "inputs:file of @%s@", + sourcePrim.GetPath().GetText(), assetPath.c_str())); + } + + auto textureIs8Bit = [](std::string resolvedPath) { + + std::string extension = ArGetResolver().GetExtension(resolvedPath); + extension = TfStringToLower(extension); + static const std::unordered_set eightBitExtensions = + {"bmp", "tga", "png", "jpg", "jpeg", "tif"}; + + return eightBitExtensions.find(extension) != eightBitExtensions.end(); + }; + + if (!textureIs8Bit(textureAssetPath.GetResolvedPath())) { + // Nothing more is required for image depths > 8 bits, which + // we assume FOR NOW, are floating point + return errors; + } + + TfToken colorSpace; + TfToken rawColorSpace("raw"); + bool valueForColorSpaceExists = getInputValue(sourceShader, TfToken("sourceColorSpace"), colorSpace); + if (!valueForColorSpaceExists || colorSpace != rawColorSpace) { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->invalidSourceColorSpace, + UsdValidationErrorType::Error, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), + sourcePrim.GetPath()) + }, + TfStringPrintf("UsdUVTexture prim <%s> that reads" + " Normal Map @%s@ should set " + "inputs:sourceColorSpace to 'raw'.", + sourcePrim.GetPath().GetText(), + textureAssetPath.GetAssetPath().c_str())); + } + + GfVec4f bias; + bool valueForBiasExists = getInputValue(sourceShader, TfToken("bias"), bias); + + GfVec4f scale; + bool valueForScaleExists = getInputValue(sourceShader, TfToken("scale"), scale); + + if (!(valueForBiasExists && valueForScaleExists)) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->nonCompliantBiasAndScale, + UsdValidationErrorType::Error, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), sourcePrim.GetPath()) + }, + TfStringPrintf("UsdUVTexture prim <%s> reads 8 bit Normal Map " + "@%s@, which requires that inputs:scale be set to " + "(2, 2, 2, 1) and inputs:bias be set to " + "(-1, -1, -1, 0) for proper interpretation as per " + "the UsdPreviewSurface and UsdUVTexture docs.", + sourcePrim.GetPath().GetText(), textureAssetPath.GetAssetPath().c_str()) + ); + return errors; + } + + // We still warn for inputs:scale not conforming to UsdPreviewSurface + // guidelines, as some authoring tools may rely on this to scale an + // effect of normal perturbations. + // don't really care about fourth components... + bool nonCompliantScaleValues = scale[0] != 2 || + scale[1] != 2 || scale[2] != 2; + + if (nonCompliantScaleValues) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->nonCompliantScale, + UsdValidationErrorType::Warn, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), sourcePrim.GetPath()) + }, + TfStringPrintf("UsdUVTexture prim <%s> reads an 8 bit Normal " + "Map, but has non-standard inputs:scale value " + "of (%.6g, %.6g, %.6g, %.6g). inputs:scale must be set to " + "(2, 2, 2, 1) so as fulfill the requirements " + "of the normals to be in tangent space of " + "[(-1,-1,-1), (1,1,1)] as documented in the " + "UsdPreviewSurface and UsdUVTexture docs.", + sourcePrim.GetPath().GetText(), + scale[0], scale[1], scale[2], scale[3]) + ); + } + + // Note that for a 8bit normal map, inputs:bias must be appropriately + // set to [-1, -1, -1, 0] to fulfill the requirements of the + // normals to be in tangent space of [(-1,-1,-1), (1,1,1)] as documented + // in the UsdPreviewSurface docs. Note this is true only when scale + // values are respecting the requirements laid in the + // UsdPreviewSurface / UsdUVTexture docs. We continue to warn! + if (!nonCompliantScaleValues && (bias[0] != -1 || bias[1] != -1 || + bias[2] != -1)) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->nonCompliantBias, + UsdValidationErrorType::Error, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), sourcePrim.GetPath()) + }, + TfStringPrintf("UsdUVTexture prim <%s> reads an 8 bit Normal " + "Map, but has non-standard inputs:bias value of " + "(%.6g, %.6g, %.6g, %.6g). inputs:bias must be set to " + "[-1,-1,-1,0] so as to fulfill the requirements " + "of the normals to be in tangent space of " + "[(-1,-1,-1), (1,1,1)] as documented in the " + "UsdPreviewSurface and UsdUVTexture docs.", + sourcePrim.GetPath().GetText(), + bias[0], bias[1], bias[2], bias[3]) + ); + } + + return errors; +} + TF_REGISTRY_FUNCTION(UsdValidationRegistry) { UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance(); @@ -602,6 +832,10 @@ TF_REGISTRY_FUNCTION(UsdValidationRegistry) UsdShadeValidatorNameTokens->materialBindingRelationships, _MaterialBindingRelationships); + registry.RegisterPluginValidator( + UsdShadeValidatorNameTokens->normalMapTextureValidator, + _NormalMapTextureValidator); + registry.RegisterPluginValidator( UsdShadeValidatorNameTokens->materialBindingCollectionValidator, _MaterialBindingCollectionValidator);