Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Maya USD Export Chaser to Filter Properties #193

14 changes: 14 additions & 0 deletions client/ayon_maya/api/chasers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# AYON Maya USD Chasers

This folder contains AYON Maya USD python import and export chasers to be
registered on Maya startup. These chasers have the ability to influence how
USD data is imported and exported in Maya.

For example, the Filter Properties export chaser allows to filter properties
in the exported USD file to only those that match by the specified pattern
using a SideFX Houdini style pattern matching.

The chasers are registered in the `MayaHost.install` method on Maya launch.

See also the [Maya USD Import Chaser documentation](https://github.com/Autodesk/maya-usd/blob/dev/lib/mayaUsd/commands/Readme.md#import-chasers)
and [Maya USD Export Chaser documentation](https://github.com/Autodesk/maya-usd/blob/dev/lib/mayaUsd/commands/Readme.md#export-chasers-advanced).
138 changes: 138 additions & 0 deletions client/ayon_maya/api/chasers/export_filter_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import re
import fnmatch
import logging
from typing import List

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: Sdf.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}")


def remove_layer_specs(layer: Sdf.Layer, spec_paths: List[Sdf.Path]):
# 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(spec_paths):
spec = layer.GetObjectAtPath(spec_path)
if not spec or spec.expired:
continue
remove_spec(spec)


def match_pattern(name: str, text_pattern: str) -> bool:
"""SideFX Houdini like pattern matching"""
patterns = text_pattern.split(" ")
is_match = False
for pattern in patterns:
# * means any character
# ? means any single character
# [abc] means a, b, or c
pattern = pattern.strip(" ")
if not pattern:
continue

excludes = pattern[0] == "^"

# If name is already matched against earlier pattern in the text
# pattern, then we can skip the pattern if it is not an exclude pattern
if is_match and not excludes:
continue

if excludes:
pattern = pattern[1:]

regex = fnmatch.translate(pattern)
match = re.match(regex, name)
if match:
is_match = not excludes
return is_match



class FilterPropertiesExportChaser(mayaUsdLib.ExportChaser):
"""Remove property specs based on pattern"""

name = "AYON_filterProperties"

def __init__(self, factoryContext, *args, **kwargs):
super().__init__(factoryContext, *args, **kwargs)
self.log = logging.getLogger(self.__class__.__name__)
self.stage = factoryContext.GetStage()
self.job_args = factoryContext.GetJobArgs()

@log_errors
def PostExport(self):

chaser_args = self.job_args.allChaserArgs[self.name]
# strip all or use user-specified pattern
pattern = chaser_args.get("pattern", "*")
for layer in self.stage.GetLayerStack():

specs_to_remove = []

def find_attribute_specs_to_remove(path: Sdf.Path):
if not path.IsPropertyPath():
return

spec = layer.GetObjectAtPath(path)
if not spec:
return

if not isinstance(spec, Sdf.PropertySpec):
return

if not match_pattern(spec.name, pattern):
self.log.debug("Removing spec: %s", path)
specs_to_remove.append(path)
else:
self.log.debug("Keeping spec: %s", path)

layer.Traverse("/", find_attribute_specs_to_remove)

remove_layer_specs(layer, specs_to_remove)

return True
21 changes: 21 additions & 0 deletions client/ayon_maya/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def install(self):
)
register_event_callback("workfile.save.after", after_workfile_save)

self._register_maya_usd_chasers()

def open_workfile(self, filepath):
return open_file(filepath)

Expand Down Expand Up @@ -239,6 +241,25 @@ def _register_callbacks(self):
self.log.info("Installed event handler _check_lock_file..")
self.log.info("Installed event handler _before_close_maya..")

def _register_maya_usd_chasers(self):
"""Register Maya USD chasers if Maya USD libraries are available."""

try:
import mayaUsd.lib # noqa
except ImportError:
# Do not register if Maya USD is not available
return

self.log.info("Installing AYON Maya USD chasers..")

from .chasers import export_filter_properties # noqa

for export_chaser in [
export_filter_properties.FilterPropertiesExportChaser
]:
mayaUsd.lib.ExportChaser.Register(export_chaser,
export_chaser.name)


def _set_project():
"""Sets the maya project to the current Session's work directory.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pyblish.api

from ayon_core.lib import TextDef
from ayon_core.pipeline.publish import AYONPyblishPluginMixin
from ayon_maya.api import plugin


class CollectMayaUsdFilterProperties(plugin.MayaInstancePlugin,
AYONPyblishPluginMixin):

order = pyblish.api.CollectorOrder
label = "Maya USD Export Chaser: Filter Properties"
families = ["mayaUsd"]

default_filter = ""

@classmethod
def get_attribute_defs(cls):
return [
TextDef(
"filter_properties",
label="USD Filter Properties",
tooltip=(
"Filter USD properties using a pattern:\n"
"- Only include xforms: xformOp*\n"
"- All but xforms: * ^xformOp*\n"
"- All but mesh point data: * ^extent ^points "
"^faceVertex* ^primvars*\n\n"
"The pattern matching is very similar to SideFX Houdini's "
"Pattern Matching in Parameters."
),
placeholder="* ^xformOp* ^points",
default=cls.default_filter
)
]

def process(self, instance):
attr_values = self.get_attr_values_from_data(instance.data)
filter_pattern = attr_values.get("filter_properties")
if not filter_pattern:
return

self.log.debug(
"Enabling USD filter properties chaser "
f"with pattern {filter_pattern}"
)
instance.data.setdefault("chaser", []).append("AYON_filterProperties")
instance.data.setdefault("chaserArgs", []).append(
("AYON_filterProperties", "pattern", filter_pattern)
)
11 changes: 10 additions & 1 deletion client/ayon_maya/plugins/publish/extract_maya_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ def options(self):

# TODO: Support more `mayaUSDExport` parameters
return {
"chaser": (list, None), # optional list
"chaserArgs": (list, None), # optional list
"defaultUSDFormat": str,
"stripNamespaces": bool,
"mergeTransformAndShape": bool,
Expand All @@ -191,6 +193,8 @@ def default_options(self):

# TODO: Support more `mayaUSDExport` parameters
return {
"chaser": None,
"chaserArgs": None,
"defaultUSDFormat": "usdc",
"stripNamespaces": False,
"mergeTransformAndShape": True,
Expand Down Expand Up @@ -234,6 +238,11 @@ def parse_overrides(self, instance, options):

options[key] = value

# Do not pass None values
for key, value in options.copy().items():
if value is None:
del options[key]

return options

def filter_members(self, members):
Expand Down Expand Up @@ -300,7 +309,7 @@ def process(self, instance):
options["filterTypes"] = ["constraint"]

def parse_attr_str(attr_str):
"""Return list of strings from `a,b,c,,d` to `[a, b, c, d]`.
"""Return list of strings from `a,b,c,d` to `[a, b, c, d]`.

Args:
attr_str (str): Concatenated attributes by comma
Expand Down
28 changes: 28 additions & 0 deletions server/settings/publishers.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@ class CollectGLTFModel(BaseSettingsModel):
enabled: bool = SettingsField(title="CollectGLTF")


class CollectMayaUsdFilterPropertiesModel(BaseSettingsModel):
enabled: bool = SettingsField(title="Maya USD Export Chaser: Filter Properties")
default_filter: str = SettingsField(
title="Default Filter",
description=(
"Set the default filter for USD properties to export. It uses"
" [SideFX Houdini Pattern Matching in Parameters]"
"(https://www.sidefx.com/docs/houdini/network/patterns.html)."
"\nSome examples would include:\n"
"- Only include xforms: `xformOp*`\n"
"- Everything but xforms: `* ^xformOp*`\n"
"- Everything but mesh point data: `* ^extent ^points"
" ^faceVertexCounts ^faceVertexIndices ^primvars*`"
),
default=""
)


class ValidateFrameRangeModel(BaseSettingsModel):
enabled: bool = SettingsField(title="ValidateFrameRange")
optional: bool = SettingsField(title="Optional")
Expand Down Expand Up @@ -620,6 +638,12 @@ class PublishersModel(BaseSettingsModel):
default_factory=CollectGLTFModel,
title="Collect Assets for GLB/GLTF export"
)
CollectMayaUsdFilterProperties: CollectMayaUsdFilterPropertiesModel = (
SettingsField(
default_factory=CollectMayaUsdFilterPropertiesModel,
title="Maya USD Export Chaser: Filter Properties"
)
)
ValidateInstanceInContext: BasicValidateModel = SettingsField(
default_factory=BasicValidateModel,
title="Validate Instance In Context",
Expand Down Expand Up @@ -1083,6 +1107,10 @@ class PublishersModel(BaseSettingsModel):
"CollectGLTF": {
"enabled": False
},
"CollectMayaUsdFilterProperties": {
"enabled": False,
"default_filter": ""
},
"ValidateInstanceInContext": {
"enabled": True,
"optional": True,
Expand Down
Loading