diff --git a/pxr/usd/usdSkel/CMakeLists.txt b/pxr/usd/usdSkel/CMakeLists.txt index e9b56aaac4..d1a0a1ed9c 100644 --- a/pxr/usd/usdSkel/CMakeLists.txt +++ b/pxr/usd/usdSkel/CMakeLists.txt @@ -37,6 +37,10 @@ pxr_library(usdSkel tokens topology utils + validatorTokens + + CPPFILES + validators.cpp PUBLIC_HEADERS api.h @@ -93,6 +97,15 @@ pxr_library(usdSkel images/influencesPrimvarLayout.svg images/unboundedInterpolationExample.svg ) + +pxr_build_test(testUsdSkelValidators + LIBRARIES + tf + usd + usdSkel + CPPFILES + testenv/testUsdSkelValidators.cpp +) pxr_test_scripts( testenv/testUsdSkelAnimMapper.py @@ -189,3 +202,9 @@ pxr_register_test(testUsdSkelUtils COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdSkelUtils" EXPECTED_RETURN_CODE 0 ) + +pxr_register_test(testUsdSkelValidators + COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdSkelValidators" + EXPECTED_RETURN_CODE 0 +) + diff --git a/pxr/usd/usdSkel/plugInfo.json b/pxr/usd/usdSkel/plugInfo.json index dc4e7c0d95..1066d856b3 100644 --- a/pxr/usd/usdSkel/plugInfo.json +++ b/pxr/usd/usdSkel/plugInfo.json @@ -69,6 +69,14 @@ "implementsComputeExtent": true, "schemaKind": "concreteTyped" } + }, + "Validators": { + "SkelBindingApiAppliedValidator": { + "doc": "Verify a prim has the SkelBindingAPI applied if it has a UsdSkelBinding property. If a prim has the SkelBindingAPI applied, make sure it's of type SkelRoot or parented by a SkelRoot." + }, + "keywords": [ + "UsdSkelValidators" + ] } }, "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", diff --git a/pxr/usd/usdSkel/testenv/testUsdSkelValidators.cpp b/pxr/usd/usdSkel/testenv/testUsdSkelValidators.cpp new file mode 100644 index 0000000000..a9d02ad818 --- /dev/null +++ b/pxr/usd/usdSkel/testenv/testUsdSkelValidators.cpp @@ -0,0 +1,107 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#include "pxr/usd/usd/validationRegistry.h" +#include "pxr/usd/usd/validationError.h" +#include +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +void +TestUsdSkelValidators() +{ + // This should be updated with every new validator added with + // UsdSkelValidators keyword. + UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance(); + UsdValidatorMetadataVector metadata = + registry.GetValidatorMetadataForKeyword( + UsdSkelValidatorKeywordTokens->UsdSkelValidators); + // Since other validators can be registered with a UsdSkelValidators + // keyword, our validators registered in usdSkel are a subset of the entire + // set. + std::set validatorMetadataNameSet; + for (const UsdValidatorMetadata &metadata : metadata) { + validatorMetadataNameSet.insert(metadata.name); + } + + const std::set expectedValidatorNames = + {UsdSkelValidatorNameTokens->skelBindingApiAppliedValidator}; + + TF_AXIOM(std::includes(validatorMetadataNameSet.begin(), + validatorMetadataNameSet.end(), + expectedValidatorNames.begin(), + expectedValidatorNames.end())); +} + +void +TestUsdSkelBindingApiAppliedValidator() +{ + // Verify that the validator exists. + UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance(); + const UsdValidator *validator = registry.GetOrLoadValidatorByName( + UsdSkelValidatorNameTokens->skelBindingApiAppliedValidator); + TF_AXIOM(validator); + + // Create Stage and mesh with a skel binding property + UsdStageRefPtr usdStage = UsdStage::CreateInMemory(); + UsdGeomMesh mesh = UsdGeomMesh::Define(usdStage, SdfPath("/SkelRoot/Mesh")); + UsdGeomPrimvarsAPI primvarsApi(mesh); + UsdGeomPrimvar jointIndicesPrimvar = primvarsApi.CreatePrimvar(TfToken("skel:jointIndices"), SdfValueTypeNames->IntArray, UsdGeomTokens->vertex); + jointIndicesPrimvar.Set(VtIntArray{0, 1, 2}); + + UsdValidationErrorVector errors = validator->Validate(mesh.GetPrim()); + + // Verify the error for not having the SkelBindingAPI schema applied is present. + TF_AXIOM(errors.size() == 1); + TF_AXIOM(errors[0].GetType() == UsdValidationErrorType::Error); + 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() == + SdfPath("/SkelRoot/Mesh")); + std::string expectedErrorMsg = ("Found a UsdSkelBinding property (primvars:skel:jointIndices)" + ", but no SkelBindingAPI applied on the prim ."); + TF_AXIOM(errors[0].GetMessage() == expectedErrorMsg); + + // Apply the SkelBindingAPI. + UsdSkelBindingAPI skelBindingApi = UsdSkelBindingAPI::Apply(mesh.GetPrim()); + + errors = validator->Validate(mesh.GetPrim()); + + // Verify the error for not having a SkelRoot parenting a prim with the SkelBindingAPI applied. + TF_AXIOM(errors.size() == 1); + TF_AXIOM(errors[0].GetType() == UsdValidationErrorType::Error); + 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() == + SdfPath("/SkelRoot/Mesh")); + expectedErrorMsg = ("UsdSkelBindingAPI applied on prim: , " + "which is not of type SkelRoot or is not rooted at a prim " + "of type SkelRoot, as required by the UsdSkel schema."); + const std::string message = errors[0].GetMessage(); + TF_AXIOM(errors[0].GetMessage() == expectedErrorMsg); + + // Add SkelRoot as a parent to the mesh. + UsdSkelRoot skelRoot = UsdSkelRoot::Define(usdStage, SdfPath("/SkelRoot")); + errors = validator->Validate(mesh.GetPrim()); + + // Verify all errors are gone + TF_AXIOM(errors.size() == 0); +} + +int +main() +{ + TestUsdSkelValidators(); + TestUsdSkelBindingApiAppliedValidator(); + printf("OK\n"); + return EXIT_SUCCESS; +}; \ No newline at end of file diff --git a/pxr/usd/usdSkel/validatorTokens.cpp b/pxr/usd/usdSkel/validatorTokens.cpp new file mode 100644 index 0000000000..ecfa5a722d --- /dev/null +++ b/pxr/usd/usdSkel/validatorTokens.cpp @@ -0,0 +1,17 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// + +#include "pxr/usd/usdSkel/validatorTokens.h" + +PXR_NAMESPACE_OPEN_SCOPE + +TF_DEFINE_PUBLIC_TOKENS(UsdSkelValidatorNameTokens, + USD_SKEL_VALIDATOR_NAME_TOKENS); +TF_DEFINE_PUBLIC_TOKENS(UsdSkelValidatorKeywordTokens, + USD_SKEL_VALIDATOR_KEYWORD_TOKENS); + +PXR_NAMESPACE_CLOSE_SCOPE \ No newline at end of file diff --git a/pxr/usd/usdSkel/validatorTokens.h b/pxr/usd/usdSkel/validatorTokens.h new file mode 100644 index 0000000000..61b29d46de --- /dev/null +++ b/pxr/usd/usdSkel/validatorTokens.h @@ -0,0 +1,34 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#ifndef USDSKEL_VALIDATOR_TOKENS_H +#define USDSKEL_VALIDATOR_TOKENS_H +/// \file +#include "pxr/pxr.h" +#include "pxr/usd/usdSkel/api.h" +#include "pxr/base/tf/staticTokens.h" +PXR_NAMESPACE_OPEN_SCOPE + +#define USD_SKEL_VALIDATOR_NAME_TOKENS \ + ((skelBindingApiAppliedValidator, "usdSkel:SkelBindingApiAppliedValidator")) +#define USD_SKEL_VALIDATOR_KEYWORD_TOKENS \ + (UsdSkelValidators) + +///\def +/// Tokens representing validator names. Note that for plugin provided +/// validators, the names must be prefixed by usdSkel:, which is the name of +/// the usdSkel plugin. +TF_DECLARE_PUBLIC_TOKENS(UsdSkelValidatorNameTokens, USDSKEL_API, + USD_SKEL_VALIDATOR_NAME_TOKENS); +///\def +/// Tokens representing keywords associated with any validator in the usdShade +/// plugin. Clients can use this to inspect validators contained within a +/// specific keywords, or use these to be added as keywords to any new +/// validator. +TF_DECLARE_PUBLIC_TOKENS(UsdSkelValidatorKeywordTokens, USDSKEL_API, + USD_SKEL_VALIDATOR_KEYWORD_TOKENS); +PXR_NAMESPACE_CLOSE_SCOPE +#endif \ No newline at end of file diff --git a/pxr/usd/usdSkel/validators.cpp b/pxr/usd/usdSkel/validators.cpp new file mode 100644 index 0000000000..1ef8755af9 --- /dev/null +++ b/pxr/usd/usdSkel/validators.cpp @@ -0,0 +1,89 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#include "pxr/usd/usd/prim.h" +#include "pxr/usd/usd/schemaRegistry.h" +#include "pxr/usd/usd/validationError.h" +#include "pxr/usd/usd/validationRegistry.h" +#include "pxr/usd/usd/validator.h" +#include "pxr/base/tf/token.h" +#include "pxr/base/tf/stringUtils.h" +#include "pxr/usd/usdSkel/validatorTokens.h" +#include "pxr/usd/usdSkel/bindingAPI.h" + +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +static +UsdValidationErrorVector +_SkelBindingApiAppliedValidator(const UsdPrim &usdPrim) +{ + UsdValidationErrorVector errors; + + if (!usdPrim.HasAPI()){ + static const std::unordered_set skelPropertyNames = []() { + UsdSchemaRegistry& usdSchemaRegistry = UsdSchemaRegistry::GetInstance(); + std::unique_ptr primDef = usdSchemaRegistry.BuildComposedPrimDefinition(TfToken(), {UsdSkelTokens->SkelBindingAPI}); + const std::vector skelPropertyNamesVector = primDef->GetPropertyNames(); + return std::unordered_set(skelPropertyNamesVector.begin(), skelPropertyNamesVector.end()); + }(); + + const std::vector primPropertyNames = usdPrim.GetPropertyNames(); + for (const TfToken &primToken : primPropertyNames){ + if (skelPropertyNames.find(primToken) == skelPropertyNames.end()){ + continue; + } + errors.emplace_back( + UsdValidationErrorType::Error, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), + usdPrim.GetPath()) + }, + TfStringPrintf(("Found a UsdSkelBinding property (%s), " + "but no SkelBindingAPI applied on the prim <%s>."), + primToken.GetText(), usdPrim.GetPath().GetText())); + break; + } + } + else { + if (usdPrim.GetTypeName() != UsdSkelTokens->SkelRoot) { + UsdPrim parentPrim = usdPrim.GetParent(); + while (parentPrim && !parentPrim.IsPseudoRoot()) { + if (parentPrim.GetTypeName() != UsdSkelTokens->SkelRoot) { + parentPrim = parentPrim.GetParent(); + } + else { + return errors; + } + } + errors.emplace_back( + UsdValidationErrorType::Error, + UsdValidationErrorSites{ + UsdValidationErrorSite(usdPrim.GetStage(), + usdPrim.GetPath()) + }, + TfStringPrintf(("UsdSkelBindingAPI applied on prim: <%s>, " + "which is not of type SkelRoot or is not rooted at a prim " + "of type SkelRoot, as required by the UsdSkel schema."), + usdPrim.GetPath().GetText())); + } + } + + return errors; +} + +TF_REGISTRY_FUNCTION(UsdValidationRegistry) +{ + UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance(); + + registry.RegisterPluginValidator( + UsdSkelValidatorNameTokens->skelBindingApiAppliedValidator, + _SkelBindingApiAppliedValidator); +} + +PXR_NAMESPACE_CLOSE_SCOPE \ No newline at end of file