Last active
February 16, 2025 10:22
-
-
Save BigRoy/7d73ec1bfcc5944a6aa887ee31b1bf44 to your computer and use it in GitHub Desktop.
Maya USD Export Chaser to keep only 'lookdev' related specs in the output file, to make a "look" overlay USD file from native maya geometry export - using Arnold job context in this case, to export materials and e.g. arnold subdiv opinions only
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import logging | |
from maya.api import OpenMaya | |
import mayaUsd.lib as mayaUsdLib | |
from pxr import Sdf | |
def log_errors(fn): | |
"""Decorator to log errors on error""" | |
def wrap(*args, **kwargs): | |
try: | |
return fn(*args, **kwargs) | |
except Exception as exc: | |
logging.error(exc, exc_info=True) | |
raise | |
return wrap | |
def remove_spec(spec): | |
"""Remove Sdf.Spec authored opinion.""" | |
if spec.expired: | |
return | |
if isinstance(spec, Sdf.PrimSpec): | |
# PrimSpec | |
parent = spec.nameParent | |
if parent: | |
view = parent.nameChildren | |
else: | |
# Assume PrimSpec is root prim | |
view = spec.layer.rootPrims | |
del view[spec.name] | |
elif isinstance(spec, Sdf.PropertySpec): | |
# Relationship and Attribute specs | |
del spec.owner.properties[spec.name] | |
elif isinstance(spec, Sdf.VariantSetSpec): | |
# Owner is Sdf.PrimSpec (or can also be Sdf.VariantSpec) | |
del spec.owner.variantSets[spec.name] | |
elif isinstance(spec, Sdf.VariantSpec): | |
# Owner is Sdf.VariantSetSpec | |
spec.owner.RemoveVariant(spec) | |
else: | |
raise TypeError(f"Unsupported spec type: {spec}") | |
class LookdevOnlyExportChaser(mayaUsdLib.ExportChaser): | |
"""Keep only lookdev related data in the USD file. | |
Keep only: | |
- Materials + Shaders (and their children) | |
- Boundable Prim attributes 'material:binding' and 'primvars:arnold:' | |
- For GeomSubset that has material binding, include the the full GeomSubset | |
""" | |
def __init__(self, factoryContext, *args, **kwargs): | |
super(LookdevOnlyExportChaser, self).__init__(factoryContext, *args, | |
**kwargs) | |
self.log = logging.getLogger("lookdev_chaser") | |
self.dag_to_usd = factoryContext.GetDagToUsdMap() | |
self.stage = factoryContext.GetStage() | |
self.job_args = factoryContext.GetJobArgs() | |
@log_errors | |
def PostExport(self): | |
# TODO: We might want to clear "kind" or "ApiSchemas" on a PrimSpec | |
# Strip all geometry attributes like points, uvs, etc. | |
# but keep all renderer related attributes | |
# Exclude `texCoord2f[]` attributes, e.g. `primvars:st` | |
from pxr import UsdGeom, UsdShade, Usd | |
include_paths = set() | |
boundables = set() | |
# Find paths to include in the output | |
iterator = iter(self.stage.Traverse()) | |
for prim in iterator: | |
prim_path = prim.GetPath() | |
# Include shaders/materials completely | |
if prim.IsA(UsdShade.NodeGraph) or prim.IsA(UsdShade.Shader): | |
# Include self and all children always | |
include_paths.add(prim_path) | |
iterator.PruneChildren() | |
continue | |
# For boundable prims get any arnold vars or material bindings | |
is_boundable = prim.IsA(UsdGeom.Boundable) | |
for property in prim.GetProperties(): | |
if not property.IsAuthored(): | |
continue | |
name = property.GetName() | |
property_path = prim_path.AppendProperty(name) | |
if is_boundable and "primvars:arnold" in name: | |
include_paths.add(property_path) | |
continue | |
elif "material:binding" in name: | |
# For GeomSubset include the full prim | |
if prim.IsA(UsdGeom.Subset): | |
# Include self and all children always | |
include_paths.add(prim_path) | |
iterator.PruneChildren() | |
break | |
else: | |
include_paths.add(property_path) | |
continue | |
for layer in self.stage.GetLayerStack(): | |
specs_to_remove = [] | |
def find_specs_to_remove(path): | |
if path.IsAbsoluteRootPath(): | |
return | |
# include paths that are explicitly included | |
if path in include_paths: | |
return | |
# include property if the prim is included | |
if path.GetPrimPath() in include_paths: | |
return | |
# always include children paths | |
for parent_path in path.GetAncestorsRange(): | |
if parent_path in include_paths: | |
return | |
specs_to_remove.append(path) | |
layer.Traverse("/", find_specs_to_remove) | |
# Iterate in reverse so we iterate the highest paths | |
# first, so when removing a spec the children specs | |
# are already removed | |
for spec_path in reversed(specs_to_remove): | |
spec = layer.GetObjectAtPath(spec_path) | |
if not spec or spec.expired: | |
continue | |
if isinstance(spec, Sdf.PrimSpec) and any(path.HasPrefix(spec_path) for path in include_paths): | |
# If any children are still to be included | |
# then we should not remove the spec completely | |
# but strip it, keeping only the children | |
self.log.debug("Override: %s", spec_path) | |
spec.specifier = Sdf.SpecifierOver | |
else: | |
self.log.debug("Removing: %s | %s", spec.typeName, spec_path) | |
remove_spec(spec) | |
return True | |
# Testing | |
from maya import cmds | |
cmds.loadPlugin("mayaUsdPlugin", quiet=True) | |
# Unregister en register to update the registry so we can run | |
# this code multiple times and always use the latest state | |
mayaUsdLib.ExportChaser.Unregister(LookdevOnlyExportChaser, "lookdev") | |
mayaUsdLib.ExportChaser.Register(LookdevOnlyExportChaser, "lookdev") | |
# We want to export a Maya USD file with `jobContext` set to `Arnold` | |
# but additionally include our own `chaser`. Unfortunately you can't | |
# seem to mix `jobContext` with `chaser` because the `chaser` will | |
# be completely ignored. So we convert `jobContext` to its individual | |
# arguments for the export so we can append our own chaser | |
def parse_arguments(text): | |
data = {} | |
for key_value in text.split(";"): | |
key_value = key_value.strip() | |
if not key_value: | |
continue | |
key, value = key_value.split("=", 1) | |
if value.startswith("[") and value.endswith("]"): | |
value = value[1:-1] | |
value = value.split(",") | |
data[key] = value | |
return data | |
arguments = cmds.mayaUSDListJobContexts(exportArguments="Arnold") | |
kwargs = parse_arguments(arguments) | |
kwargs.setdefault("chaser", []).append("lookdev") | |
# Perform the export (preserve selection, just for ease of testing) | |
sel = cmds.ls(selection=True) | |
try: | |
cmds.mayaUSDExport(file="C:/Users/User/Desktop/render.usda", | |
# frameRange=(1, 5), | |
# frameStride=1.0, | |
# mergeTransformAndShape=True, | |
selection=True, | |
**kwargs) | |
finally: | |
cmds.select(sel, replace=True, noExpand=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment