diff --git a/pxr/usd/usdShade/plugInfo.json b/pxr/usd/usdShade/plugInfo.json index c7dcb46998..0975c6bb1b 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." - }, + }, + "ShaderValidator": { + "doc": "Shader nodes must have 'id' as the implementationSource, with id values that begin with 'Usd*|ND_*'. Also, shader inputs with connections must each have a single, valid connection source." + }, "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..941e82725d 100644 --- a/pxr/usd/usdShade/testenv/testUsdShadeValidators.cpp +++ b/pxr/usd/usdShade/testenv/testUsdShadeValidators.cpp @@ -15,6 +15,7 @@ #include "pxr/usd/usd/validationError.h" #include "pxr/usd/usd/validationRegistry.h" #include "pxr/usd/usd/validator.h" +#include "pxr/usd/usdGeom/cube.h" #include "pxr/usd/usdGeom/validatorTokens.h" #include "pxr/usd/usdGeom/scope.h" #include "pxr/usd/usdShade/shader.h" @@ -47,7 +48,8 @@ TestUsdShadeValidators() UsdShadeValidatorNameTokens->materialBindingCollectionValidator, UsdShadeValidatorNameTokens->shaderSdrCompliance, UsdShadeValidatorNameTokens->subsetMaterialBindFamilyName, - UsdShadeValidatorNameTokens->subsetsMaterialBindFamily + UsdShadeValidatorNameTokens->subsetsMaterialBindFamily, + UsdShadeValidatorNameTokens->shaderValidator, }; const UsdValidationRegistry& registry = @@ -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,173 @@ TestUsdShadeEncapsulationRulesValidator() } } +void +TestUsdShadeShaderValidator() +{ + UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance(); + const UsdValidator *validator = registry.GetOrLoadValidatorByName( + UsdShadeValidatorNameTokens->shaderValidator); + TF_AXIOM(validator); + + // Create Stage with a shader that will be modified to show all errors + UsdStageRefPtr usdStage = UsdStage::CreateInMemory(); + + UsdShadeShader testShader = + UsdShadeShader::Define(usdStage, SdfPath("/TestShader")); + + // Set Implementation Source to something other than id + // Do not include info:id + testShader.GetImplementationSourceAttr().Set(UsdShadeTokens->sourceAsset); + + // Verify appropriate errors occured + UsdValidationErrorVector errors = + validator->Validate(testShader.GetPrim()); + TF_AXIOM(errors.size() == 2u); + + std::vector expectedErrorIdentifiers = { + TfToken("usdShade:ShaderValidator.NonIdImplementationSource"), + TfToken("usdShade:ShaderValidator.InvalidShaderId") + }; + std::vector expectedErrorMessages = { + "Shader has non-id implementation source 'sourceAsset'.", + "Shader has unsupported info:id 'None'." + }; + + for(size_t i = 0; i < errors.size(); ++i) + { + TF_AXIOM(errors[i].GetIdentifier() == expectedErrorIdentifiers[i]); + TF_AXIOM(errors[i].GetType() == UsdValidationErrorType::Error); + TF_AXIOM(errors[i].GetSites().size() == 1u); + TF_AXIOM(errors[i].GetSites()[0].IsValid()); + TF_AXIOM(errors[i].GetSites()[0].IsPrim()); + TF_AXIOM(errors[i].GetSites()[0].GetPrim().GetPath() == + SdfPath("/TestShader")); + TF_AXIOM(errors[i].GetMessage() == expectedErrorMessages[i]); + } + + // Update implementation source to be id + testShader.GetImplementationSourceAttr().Set(UsdShadeTokens->id); + // Add an invalid info:id value + testShader.GetIdAttr().Set(TfToken("myInvalidId")); + + // Verify error occurs + errors = validator->Validate(testShader.GetPrim()); + TfToken expectedErrorIdentifier( + "usdShade:ShaderValidator.InvalidShaderId"); + TF_AXIOM(errors.size() == 1u); + TF_AXIOM(errors[0].GetIdentifier() == expectedErrorIdentifier); + TF_AXIOM(errors[0].GetType() == UsdValidationErrorType::Error); + TF_AXIOM(errors[0].GetSites().size() == 1u); + TF_AXIOM(errors[0].GetSites()[0].IsValid()); + TF_AXIOM(errors[0].GetSites()[0].IsPrim()); + TF_AXIOM(errors[0].GetSites()[0].GetPrim().GetPath() == + SdfPath("/TestShader")); + std::string expectedErrorMsg = + "Shader has unsupported info:id 'myInvalidId'."; + TF_AXIOM(errors[0].GetMessage() == expectedErrorMsg); + + // Set a valid info:id value + testShader.GetIdAttr().Set(TfToken("UsdPreviewSurface")); + + // Add two more shaders to set up multiple connections + UsdShadeShader sourceShaderOne = UsdShadeShader::Define( + usdStage, SdfPath("/SourceShaderOne")); + UsdShadeShader sourceShaderTwo = UsdShadeShader::Define( + usdStage, SdfPath("/SourceShaderTwo")); + UsdShadeOutput sourceOutputOne = sourceShaderOne.CreateOutput( + TfToken("outValue"), SdfValueTypeNames->Float); + sourceOutputOne.Set(1.0f); + UsdShadeOutput sourceOutputTwo = sourceShaderTwo.CreateOutput( + TfToken("outValue"), SdfValueTypeNames->Float); + sourceOutputTwo.Set(2.0f); + + // Create an input for connections + UsdShadeInput shaderInput = testShader.CreateInput(TfToken("myInput"), + SdfValueTypeNames->Float); + + // Set multiple connections on the same input + shaderInput.GetAttr().SetConnections({ + SdfPath("/SourceShaderOne.outputs:outValue"), + SdfPath("/SourceShaderTwo.outputs:outValue")}); + + // Verify the error occurs + errors = validator->Validate(testShader.GetPrim()); + expectedErrorIdentifier = TfToken( + "usdShade:ShaderValidator.MultipleConnectionSources"); + TF_AXIOM(errors.size() == 1u); + TF_AXIOM(errors[0].GetIdentifier() == expectedErrorIdentifier); + TF_AXIOM(errors[0].GetType() == UsdValidationErrorType::Error); + TF_AXIOM(errors[0].GetSites().size() == 1u); + TF_AXIOM(errors[0].GetSites()[0].IsValid()); + TF_AXIOM(errors[0].GetSites()[0].IsPrim()); + TF_AXIOM(errors[0].GetSites()[0].GetPrim().GetPath() == + SdfPath("/TestShader")); + expectedErrorMsg = + "Shader input has 2 connection " + "sources, but only one is allowed."; + + TF_AXIOM(errors[0].GetMessage() == expectedErrorMsg); + + // Update the connections to a prim that does not exist + shaderInput.GetAttr().SetConnections({SdfPath("/DoesNotExist")}); + + // Verify the error occurs + errors = validator->Validate(testShader.GetPrim()); + expectedErrorIdentifier = TfToken( + "usdShade:ShaderValidator.MissingConnectionSource"); + TF_AXIOM(errors.size() == 1u); + TF_AXIOM(errors[0].GetIdentifier() == expectedErrorIdentifier); + TF_AXIOM(errors[0].GetType() == UsdValidationErrorType::Error); + TF_AXIOM(errors[0].GetSites().size() == 1u); + TF_AXIOM(errors[0].GetSites()[0].IsValid()); + TF_AXIOM(errors[0].GetSites()[0].IsPrim()); + TF_AXIOM(errors[0].GetSites()[0].GetPrim().GetPath() == + SdfPath("/TestShader")); + expectedErrorMsg = + "Connection source for shader " + "input is missing."; + TF_AXIOM(errors[0].GetMessage() == expectedErrorMsg); + + // Create a cube with an output + UsdGeomCube cubePrim = UsdGeomCube::Define(usdStage, SdfPath("/Cube")); + UsdAttribute outValueAttr = cubePrim.GetPrim().CreateAttribute( + TfToken("outputs:outValue"), SdfValueTypeNames->Float, true); + + // Set a value for the custom output attribute + outValueAttr.Set(2.0f); + + // Update the connection to be something other than a Shader or Material + shaderInput.GetAttr().SetConnections({SdfPath("/Cube.outputs:outValue")}); + + // Verify the error occurs + errors = validator->Validate(testShader.GetPrim()); + + expectedErrorIdentifier = TfToken( + "usdShade:ShaderValidator.InvalidConnectionSourcePrimType"); + TF_AXIOM(errors.size() == 1u); + TF_AXIOM(errors[0].GetIdentifier() == expectedErrorIdentifier); + TF_AXIOM(errors[0].GetType() == UsdValidationErrorType::Error); + TF_AXIOM(errors[0].GetSites().size() == 1u); + TF_AXIOM(errors[0].GetSites()[0].IsValid()); + TF_AXIOM(errors[0].GetSites()[0].IsPrim()); + TF_AXIOM(errors[0].GetSites()[0].GetPrim().GetPath() == + SdfPath("/TestShader")); + expectedErrorMsg = + "Shader input has an invalid " + "connection source prim of type 'Cube'."; + TF_AXIOM(errors[0].GetMessage() == expectedErrorMsg); + + // Use a valid connection + shaderInput.GetAttr().SetConnections({ + SdfPath("/SourceShaderOne.outputs:outValue") + }); + + errors = validator->Validate(testShader.GetPrim()); + + // Verify all errors are gone + TF_AXIOM(errors.empty()); +} + int main() { @@ -568,6 +737,7 @@ main() TestUsdShadeSubsetMaterialBindFamilyName(); TestUsdShadeSubsetsMaterialBindFamily(); TestUsdShadeEncapsulationRulesValidator(); + TestUsdShadeShaderValidator(); return EXIT_SUCCESS; }; diff --git a/pxr/usd/usdShade/validatorTokens.h b/pxr/usd/usdShade/validatorTokens.h index afc2a909b6..4d66131d94 100644 --- a/pxr/usd/usdShade/validatorTokens.h +++ b/pxr/usd/usdShade/validatorTokens.h @@ -22,27 +22,34 @@ PXR_NAMESPACE_OPEN_SCOPE ((materialBindingRelationships, "usdShade:MaterialBindingRelationships")) \ ((materialBindingCollectionValidator, "usdShade:MaterialBindingCollectionValidator")) \ ((shaderSdrCompliance, "usdShade:ShaderSdrCompliance")) \ + ((shaderValidator, "usdShade:ShaderValidator")) \ ((subsetMaterialBindFamilyName, "usdShade:SubsetMaterialBindFamilyName")) \ ((subsetsMaterialBindFamily, "usdShade:SubsetsMaterialBindFamily")) #define USD_SHADE_VALIDATOR_KEYWORD_TOKENS \ (UsdShadeValidators) -#define USD_SHADE_VALIDATION_ERROR_NAME_TOKENS \ - ((connectableInNonContainer, "ConnectableInNonContainer")) \ - ((invalidConnectableHierarchy, "InvalidConnectableHierarchy")) \ - ((missingMaterialBindingAPI, "MissingMaterialBindingAPI")) \ - ((materialBindingPropNotARel, "MaterialBindingPropNotARel")) \ - ((invalidMaterialCollection, "InvalidMaterialCollection")) \ - ((invalidResourcePath, "InvalidResourcePath")) \ - ((invalidImplSource, "InvalidImplementationSrc")) \ - ((missingSourceType, "MissingSourceType")) \ - ((missingShaderIdInRegistry, "MissingShaderIdInRegistry")) \ - ((missingSourceTypeInRegistry, "MissingSourceTypeInRegistry")) \ - ((incompatShaderPropertyWarning, "IncompatShaderPropertyWarning")) \ - ((mismatchPropertyType, "MismatchedPropertyType")) \ - ((missingFamilyNameOnGeomSubset, "MissingFamilyNameOnGeomSubset")) \ - ((invalidFamilyType, "InvalidFamilyType")) \ +#define USD_SHADE_VALIDATION_ERROR_NAME_TOKENS \ + ((connectableInNonContainer, "ConnectableInNonContainer")) \ + ((invalidConnectableHierarchy, "InvalidConnectableHierarchy")) \ + ((missingMaterialBindingAPI, "MissingMaterialBindingAPI")) \ + ((materialBindingPropNotARel, "MaterialBindingPropNotARel")) \ + ((invalidMaterialCollection, "InvalidMaterialCollection")) \ + ((invalidResourcePath, "InvalidResourcePath")) \ + ((invalidImplSource, "InvalidImplementationSrc")) \ + ((missingSourceType, "MissingSourceType")) \ + ((missingShaderIdInRegistry, "MissingShaderIdInRegistry")) \ + ((missingSourceTypeInRegistry, "MissingSourceTypeInRegistry")) \ + ((incompatShaderPropertyWarning, "IncompatShaderPropertyWarning")) \ + ((mismatchPropertyType, "MismatchedPropertyType")) \ + ((missingFamilyNameOnGeomSubset, "MissingFamilyNameOnGeomSubset")) \ + ((invalidFamilyType, "InvalidFamilyType")) \ + ((nonIdImplementationSource, "NonIdImplementationSource")) \ + ((invalidShaderId, "InvalidShaderId")) \ + ((multipleConnectionSources, "MultipleConnectionSources")) \ + ((missingConnectionSource, "MissingConnectionSource")) \ + ((invalidConnectionSourcePrimType, "InvalidConnectionSourcePrimType")) \ + /// \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..25b3150793 100644 --- a/pxr/usd/usdShade/validators.cpp +++ b/pxr/usd/usdShade/validators.cpp @@ -590,6 +590,157 @@ _SubsetsMaterialBindFamily(const UsdPrim& usdPrim) return errors; } +static +UsdValidationErrorVector +_ShaderValidator(const UsdPrim& usdPrim) +{ + if (!usdPrim.IsA()) + { + return {}; + } + + const UsdShadeShader shaderPrim(usdPrim); + + const TfToken &implementationSource = + shaderPrim.GetImplementationSource(); + + UsdValidationErrorVector errors; + const UsdValidationErrorSites primErrorSite = { + UsdValidationErrorSite(usdPrim.GetStage(), + usdPrim.GetPath()) }; + if (implementationSource != UsdShadeTokens->id) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->nonIdImplementationSource, + UsdValidationErrorType::Error, + primErrorSite, + TfStringPrintf( + "Shader <%s> has non-id implementation " + "source '%s'.", + usdPrim.GetPath().GetText(), + implementationSource.GetText()) + ); + } + TfToken shaderId; + const bool gotShaderId = shaderPrim.GetShaderId(&shaderId); + + const std::vector validShaderIds = { + TfToken("UsdPreviewSurface"), + TfToken("UsdUVTexture"), + TfToken("UsdTransform2d"), + TfToken("UsdPrimvarReader")}; + + auto isValidShaderId = [&]() + { + bool isKnownShaderName = std::find(validShaderIds.begin(), + validShaderIds.end(), shaderId) != validShaderIds.end(); + bool isKnownShaderPrefix = TfStringStartsWith(shaderId.GetText(), + "UsdPrimvarReader") || + TfStringStartsWith(shaderId.GetText(), "ND_"); + + return isKnownShaderName || isKnownShaderPrefix; + }; + + if (!gotShaderId) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->invalidShaderId, + UsdValidationErrorType::Error, + primErrorSite, + TfStringPrintf( + "Shader <%s> has unsupported info:id 'None'.", + usdPrim.GetPath().GetText()) + ); + } + else if(!isValidShaderId()) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->invalidShaderId, + UsdValidationErrorType::Error, + primErrorSite, + TfStringPrintf( + "Shader <%s> has unsupported info:id '%s'.", + usdPrim.GetPath().GetText(), + shaderId.GetText()) + ); + } + + const std::vector shaderInputs = shaderPrim.GetInputs(); + + // Check shader input connections + for(const UsdShadeInput &shaderInput: shaderInputs) + { + SdfPathVector sources; + shaderInput.GetAttr().GetConnections(&sources); + + // If an input has one or more connections, ensure that the + // connections are valid. + if (sources.empty()) + { + continue; + } + + if (sources.size() > 1) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->multipleConnectionSources, + UsdValidationErrorType::Error, + primErrorSite, + TfStringPrintf( + "Shader input <%s> has %lu connection " + "sources, but only one is allowed.", + shaderInput.GetAttr().GetPath().GetText(), + sources.size()) + ); + continue; + } + + UsdShadeConnectableAPI connectableAPI; + TfToken sourceName; + UsdShadeAttributeType sourceType; + const bool gotSource = shaderInput.GetConnectedSource(&connectableAPI, + &sourceName, &sourceType); + + if (!gotSource) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens->missingConnectionSource, + UsdValidationErrorType::Error, + primErrorSite, + TfStringPrintf( + "Connection source <%s> for shader " + "input <%s> is missing.", + sources[0].GetText(), + shaderInput.GetAttr().GetPath().GetText() + ) + ); + } + else + { + // The source must be a valid shader or material prim. + const UsdPrim &sourcePrim = connectableAPI.GetPrim(); + if (!sourcePrim.IsA() && + !sourcePrim.IsA()) + { + errors.emplace_back( + UsdShadeValidationErrorNameTokens-> + invalidConnectionSourcePrimType, + UsdValidationErrorType::Error, + primErrorSite, + TfStringPrintf( + "Shader input <%s> has an invalid " + "connection source prim of type '%s'.", + shaderInput.GetAttr().GetPath().GetText(), + sourcePrim.GetTypeName().GetText() + ) + ); + } + } + } + + return errors; +} + TF_REGISTRY_FUNCTION(UsdValidationRegistry) { UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance(); @@ -621,6 +772,10 @@ TF_REGISTRY_FUNCTION(UsdValidationRegistry) registry.RegisterPluginValidator( UsdShadeValidatorNameTokens->encapsulationValidator, _EncapsulationValidator); + + registry.RegisterPluginValidator( + UsdShadeValidatorNameTokens->shaderValidator, + _ShaderValidator); } PXR_NAMESPACE_CLOSE_SCOPE