Omni Asset Validator (Tutorial)

Introduction

Omniverse Asset Validator is an extensible framework to validate USD. Initially inspired from Pixar Compliance Checker, extends upon these ideas and adds more validation rules applicable throughout Omniverse, as well as adding the ability to automatically fix issues.

Currently, there are two main components:

  • Omni Asset Validator (Core): Core components for Validation engine. Feel free to use this component to implement programmatic functionality or extend Core functionality through its framework.

  • Omni Asset Validator (UI): Convenient UI for Omniverse. Used for daily tasks with Omniverse tools as Create.

The following tutorial will help you to:

  • Run basic operations for Asset Validator, ValidationEngine and IssueFixer.

  • Get familiar with existing Rules to diagnose and fix problems.

  • Create your custom Rules.

Tutorials

Testing assets

In order to run Asset Validator, we need to enable the extension Omni asset validator (Core). Optionally we can also enable Omni asset validator (UI) to perform similar operations using the user interface. Through this tutorial we will use Script Editor, enable it under the Window menu.

The following tutorial uses the file BASIC_TUTORIAL_PATH which is bundled together with omni.asset_validator.core. We can see its contents with the following snippet:

import omni.asset_validator.core
from pxr import Usd

stage = Usd.Stage.Open(omni.asset_validator.core.BASIC_TUTORIAL_PATH)
print(stage.ExportToString())

The contents should be equivalent to:

#usda 1.0
(
    doc="""Generated from Composed Stage of root layer C:\\some\\location\exts\\omni.asset_validator.core\\omni\\asset_validator\\core\\resources\\tutorial.usda"""
)

def Xform "Hello"
{
    def Sphere "World"
    {
    }
}

To run a simple asset validation, with Script Editor execute the following code.

import omni.asset_validator.core

engine = omni.asset_validator.core.ValidationEngine()
print(engine.validate(omni.asset_validator.core.BASIC_TUTORIAL_PATH))

Note

For more information about ValidationEngine together with more examples can be found at the ValidationEngine API.

The above code would produce similar results to.

Results(
    asset="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda",
    issues=[
        Issue(
            message="Stage does not specify an upAxis.",
            severity=IssueSeverity.FAILURE,
            rule=StageMetadataChecker,
            at=None,
            suggestion=None
        ),
        Issue(
            message="Stage does not specify its linear scale in metersPerUnit.",
            severity=IssueSeverity.FAILURE,
            rule=StageMetadataChecker,
            at=None,
            suggestion=None
        ),
        Issue(
            message="Stage has missing or invalid defaultPrim.",
            severity=IssueSeverity.FAILURE,
            rule=StageMetadataChecker,
            at=None,
            suggestion=None
        ),
        Issue(
            message="Stage has missing or invalid defaultPrim.",
            severity=IssueSeverity.FAILURE,
            rule=OmniDefaultPrimChecker,
            at=StageId(
                identifier="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda"
            ),
            suggestion=Suggestion(
                callable=UpdateDefaultPrim,
                message="Updates the default prim"
            )
        )
    ]
)

The main result of validation engine is called an Issue. The main task of Asset validation is to detect and fix issues. An Issue has important information on how to achieve both tasks.

  • Detect. Once an issue has been found it offers a description of the problem (through a human-readable message), its severity, the Rule that found the issue and its location (i.e. the Usd.Prim).

  • Fix. If a suggestion is available, it will help to address the Issue found. Information on the Rule and the location of the issue will be used to address it.

In the following section we will walk you through on how to identify and fix issues.

Note

For more information see the Issue API.

Understanding Rules

Omni asset validator (Core) ships with multiple rules, in the previous example we already covered two:

  • StageMetadataChecker: All stages should declare their upAxis and metersPerUnit. Stages that can be consumed as referencable assets should furthermore have a valid defaultPrim declared, and stages meant for consumer-level packaging should always have upAxis set to Y.

  • OmniDefaultPrimChecker: Omniverse requires a single, active, Xformable root prim, also set to the layer’s defaultPrim.

Refer to Rules for the rest of rules. In the previous example when calling ValidationEngine we invoked all rules available. ValidationRulesRegistry has a registry of all rules to be used by ValidationEngine.

import omni.asset_validator.core

for category in omni.asset_validator.core.ValidationRulesRegistry.categories():
        for rule in omni.asset_validator.core.ValidationRulesRegistry.rules(category=category):
                print(rule.__name__)

If we want to have finer control of what we can execute, we can also specify which rules to run, for example:

import omni.asset_validator.core

engine = omni.asset_validator.core.ValidationEngine(initRules=False)
engine.enableRule(omni.asset_validator.core.OmniDefaultPrimChecker)
print(engine.validate(omni.asset_validator.core.BASIC_TUTORIAL_PATH))

There are two new elements present here:

  • initRules: By default set to true. if set to false, no rules will be automatically loaded.

  • enableRule: A method of ValidationEngine to add rules.

The above would produce the following result:

Results(
    asset="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda",
    issues=[
        Issue(
            message="Stage has missing or invalid defaultPrim.",
            severity=IssueSeverity.FAILURE,
            rule=OmniDefaultPrimChecker,
            at=StageId(
                identifier="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda"
            ),
            suggestion=Suggestion(
                callable=UpdateDefaultPrim,
                message="Updates the default prim"
            )
        )
    ]
)

In this particular case, OmniDefaultPrimChecker has implemented a suggestion for this specific issue. The second important class in Core we want to cover is IssueFixer, the way to invoke it is quite straightforward.

import omni.asset_validator.core

fixer = omni.asset_validator.core.IssueFixer(asset=omni.asset_validator.core.BASIC_TUTORIAL_PATH)
fixer.fix([])

fixer.fix will receive the list of issues that should be addressed. The list of issues to address can be accessed through issues method in Results class.

Note

For more information about IssueFixer see IssueFixer API.

By combining the previous two examples we can now, detect and fix issues for a specific rule.

import omni.asset_validator.core

# Detect issues
engine = omni.asset_validator.core.ValidationEngine(initRules=False)
engine.enableRule(omni.asset_validator.core.OmniDefaultPrimChecker)
result = engine.validate(omni.asset_validator.core.BASIC_TUTORIAL_PATH)

# Fix issues
fixer = omni.asset_validator.core.IssueFixer(asset=omni.asset_validator.core.BASIC_TUTORIAL_PATH)
fixer.fix(result.issues())

Warning

In this tutorial we do temporary changes. To persist your changes uses fixer.save().

If we inspect the file BASIC_TUTORIAL_PATH:

import omni.asset_validator.core
from pxr import Usd

stage = Usd.Stage.Open(omni.asset_validator.core.BASIC_TUTORIAL_PATH)
print(stage.ExportToString())

We can find the issue reported by OmniDefaultPrimChecker is fixed:

#usda 1.0
(
    defaultPrim="Hello"
    doc="""Generated from Composed Stage of root layer C:\\some\\location\exts\\omni.asset_validator.core\\omni\\asset_validator\\core\\resources\\tutorial.usda"""
)

def Xform "Hello"
{
    def Sphere "World"
    {
    }
}

Note

Try to repeat the same steps using the UI.

Custom Rule: Detection

If the shipped rules are not enough for your needs you can also implement your own rule. ValidationEngine allows to be extensible, by levering users to add its own rules. To add a new rule extend from BaseRuleChecker. The most simple code to achieve this is as follows:

import omni.asset_validator.core
from pxr import Usd

class MyRule(omni.asset_validator.core.BaseRuleChecker):

    def CheckPrim(self, prim: Usd.Prim) -> None:
        pass

engine = omni.asset_validator.core.ValidationEngine(initRules=False)
engine.enableRule(MyRule)
print(engine.validate(omni.asset_validator.core.BASIC_TUTORIAL_PATH))

We can see our method CheckPrim being invoked for every Prim. However, our output is empty because CheckPrim has not notified of any issues.

Results(
    asset="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda",
    issues=[

    ]
)

Note

CheckPrim is not the only method available, see more at BaseRuleChecker API.

To add a bit of logic we can change our class to report a single failure when we encounter the prim whose path is /Hello/World:

import omni.asset_validator.core
from pxr import Usd

class MyRule(omni.asset_validator.core.BaseRuleChecker):

    def CheckPrim(self, prim: Usd.Prim) -> None:
        if prim.GetPath() == "/Hello/World":
            self._AddFailedCheck(
                message="Goodbye!",
                at=prim,
            )

engine = omni.asset_validator.core.ValidationEngine(initRules=False)
engine.enableRule(MyRule)
print(engine.validate(omni.asset_validator.core.BASIC_TUTORIAL_PATH))

There are three levels of issues:

  • Errors: Errors are used to notify user that something unexpected happened that would not let the Rule run (i.e. File not found error). Added through the method _AddError.

  • Warnings: Warnings are used to notify users that though correct data is found it could cause a potential problem. Added through the method _AddWarning.

  • Failures: The most common way to report an Issue in Asset Validation. This can be done through _AddFailedCheck as seen in the example above.

Above code will generate:

Results(
    asset="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda",
    issues=[
        Issue(
            message="Goodbye!",
            severity=IssueSeverity.FAILURE,
            rule=MyRule,
            at=PrimId(
                stage_ref=StageId(
                    identifier="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda"
                ),
                path="/Hello/World"
            ),
            suggestion=None
        )
    ]
)

With our Rule implemented the next step is to propose a suggestion to fix it.

Custom Rule: Fix

The fixing interface requires to implement a Suggestion. A Suggestion will take as parameters:

  • Stage: The original stage where the issue was found.

  • Location: The location defined in the Issue (i.e. the at attribute). This will help us to scope down our fix.

import omni.asset_validator.core
from pxr import Usd

class MyRule(omni.asset_validator.core.BaseRuleChecker):

    def Callable(self, stage: Usd.Stage, location: Usd.Prim) -> None:
        raise NotImplementedError()

    def CheckPrim(self, prim: Usd.Prim) -> None:
        if prim.GetPath() == "/Hello/World":
            self._AddFailedCheck(
                message="Goodbye!",
                at=prim,
                suggestion=omni.asset_validator.core.Suggestion(
                    message="Avoids saying goodbye!",
                    callable=self.Callable,
                )
            )

engine = omni.asset_validator.core.ValidationEngine(initRules=False)
engine.enableRule(MyRule)
print(engine.validate(omni.asset_validator.core.BASIC_TUTORIAL_PATH))

It will now produce the following output.

Results(
    asset="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda",
    issues=[
        Issue(
            message="Goodbye!",
            severity=IssueSeverity.FAILURE,
            rule=MyRule,
            at=PrimId(
                stage_ref=StageId(
                    identifier="C:\some\location\exts\omni.asset_validator.core\omni\asset_validator\core\resources\tutorial.usda"
                ),
                path="/Hello/World"
            ),
            suggestion=Suggestion(
                callable=Callable,
                message="Avoids saying goodbye!"
            )
        )
    ]
)

As we can see we have now a suggestion with a description and a method to invoke. The full example will be:

import omni.asset_validator.core
from pxr import Usd

class MyRule(omni.asset_validator.core.BaseRuleChecker):

    def Callable(self, stage: Usd.Stage, location: Usd.Prim) -> None:
        raise NotImplementedError()

    def CheckPrim(self, prim: Usd.Prim) -> None:
        if prim.GetPath() == "/Hello/World":
            self._AddFailedCheck(
                message="Goodbye!",
                at=prim,
                suggestion=omni.asset_validator.core.Suggestion(
                    message="Avoids saying goodbye!",
                    callable=self.Callable,
                )
            )

engine = omni.asset_validator.core.ValidationEngine(initRules=False)
engine.enableRule(MyRule)
result = engine.validate(omni.asset_validator.core.BASIC_TUTORIAL_PATH)

fixer = omni.asset_validator.core.IssueFixer(asset=omni.asset_validator.core.BASIC_TUTORIAL_PATH)
result = fixer.fix(result.issues())
print(result)

Notice how the NotImplementedError error was not thrown during fixer.fix. However, we can access the result of execution by inspecting result:

[
    FixResult(
        issue=Issue(
            message='Goodbye!',
            severity=FAILURE,
            rule=<class '__main__.MyRule'>,
            at=PrimId(
                stage_ref=StageId(identifier='C:\\sources\\asset-validator\\_build\\windows-x86_64\\release\\exts\\omni.asset_validator.core\\omni\\asset_validator\\core\\resources\\tutorial.usda'),
                path='/Hello/World'
            ),
            suggestion=Suggestion(callable=Callable, message='Avoids saying goodbye!')
        ),
        status=FAILURE,
        exception=NotImplementedError()
    )
]

Finally, if you decide to run your custom Rule with the rest of the rules, it may be useful to register it in ValidationRulesRegistry, this can be done using registerRule.

import omni.asset_validator.core
from pxr import Usd

@omni.asset_validator.core.registerRule("MyCategory")
class MyRule(omni.asset_validator.core.BaseRuleChecker):

        def CheckPrim(self, prim: Usd.Prim) -> None:
                pass

for rule in omni.asset_validator.core.ValidationRulesRegistry.rules(category="MyCategory"):
        print(rule.__name__)

Custom Rule: Locations

For this section, let us use LAYERS_TUTORIAL_PATH. We proceed like in the previous section:

import omni.asset_validator.core
from pxr import Usd

stage = Usd.Stage.Open(omni.asset_validator.core.LAYERS_TUTORIAL_PATH)
print(stage.ExportToString())

The contents should be equivalent to:

#usda 1.0
(
    doc="""Generated from Composed Stage of root layer C:\\some\\location\exts\\omni.asset_validator.core\\omni\\asset_validator\\core\\resources\\tutorial2.usda"""
)

def Xform "Hello"
{
    def Sphere "World"
    {
        double3 xformOp:translate = (-250, 0, 0)
        uniform token[] xformOpOrder = ["xformOp:translate"]
    }
}

What we are doing is adding an opinion to the prim “/Hello/World”. In the previous section we learned how to create a Rule and issue a Failure. The data model is similar to the following code snippet:

from pxr import Usd
import omni.asset_validator.core

# We open the stage
stage = Usd.Stage.Open(omni.asset_validator.core.LAYERS_TUTORIAL_PATH)

# We inspect a specific prim
prim = stage.GetPrimAtPath("/Hello/World")

# We create the data model for the issue
def Callable(stage: Usd.Stage, location: Usd.Prim) -> None:
        raise NotImplementedError()

issue = omni.asset_validator.core.Issue(
        message="Goodbye!",
        at=prim,
        severity= omni.asset_validator.core.IssueSeverity.FAILURE,
        suggestion=omni.asset_validator.core.Suggestion(
                message="Avoids saying goodbye!",
                callable=Callable
        ),
)

# Inspect the fixing points for the suggestion
for fix_at in issue.all_fix_sites:
        layer_id = fix_at.layer_id
        path = fix_at.path
        print(layer_id, path)

The output of the above snippet should show first the path of layers tutorial (i.e. LAYERS_TUTORIAL_PATH) and second the basic tutorial (i.e. BASIC_TUTORIAL_PATH). While this may be correct for above issue, different issues may need to override this information.

Every issue, has associated fixing sites (i.e. property all_fix_sites). The fixing sites are all places that contribute opinions to the prim from strongest to weakest order. When no layer is provided to fix, by default will be the strongest. If no indicated (as above) the preferred site will be the Root layer. To change the preferred site to fix, we can add the at attribute to Suggestion.

from pxr import Usd, Sdf
import omni.asset_validator.core

# We open the stage
stage = Usd.Stage.Open(omni.asset_validator.core.LAYERS_TUTORIAL_PATH)

# We inspect a specific prim
prim = stage.GetPrimAtPath("/Hello/World")

# We create the data model for the issue
def Callable(stage: Usd.Stage, location: Usd.Prim) -> None:
        raise NotImplementedError()

issue = omni.asset_validator.core.Issue(
        message="Goodbye!",
        at=prim,
        severity= omni.asset_validator.core.IssueSeverity.FAILURE,
        suggestion=omni.asset_validator.core.Suggestion(
                message="Avoids saying goodbye!",
                callable=Callable,
        at=[Sdf.Layer.FindOrOpen(omni.asset_validator.core.BASIC_TUTORIAL_PATH)]
        ),
)

# Inspect the fixing points for the suggestion
for fix_at in issue.all_fix_sites:
        layer_id = fix_at.layer_id
        path = fix_at.path
        print(layer_id, path)

The output will change the order now, and you should see basic tutorial path first (i.e. BASIC_TUTORIAL_PATH).

The previous tutorial should have helped you to:

  • Create a custom Rule, generating an error and a suggestion to fix it.

  • Run ValidationEngine with a specific rule.

  • Run IssueFixer to fix specific issues and review the response.

Frequently Asked Questions

Are there any guards to make sure fixes are still relevant / don’t collide?

In our general practice we have noticed:

  • Fixing an issue may solve another issue. If a consecutive suggestion may fail to be applied, we just keep the exception in FixResult and continue execution. You will then decide the steps to take with FixResult.

  • Fixing an issue may generate another issue. For the second case it is recommended to run ValidationEngine again, to discover those cases. Think of it as an iterative process with help of an automated tool.

Are fixes addressed in the root layer? strongest layer?

Currently, some Issues would perform the suggestion on the strongest layer, while many on the root layer. We are working into offer flexibility to decide in which layer aim the changes, while also offering a default layer for automated workflows.