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

Maya: Implement USD publish and load using native mayaUsdPlugin #5573

Merged
merged 19 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 57 additions & 2 deletions openpype/hosts/maya/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,49 @@ def cache_subsets(shared_data):
shared_data["maya_cached_legacy_subsets"] = cache_legacy
return shared_data

def get_publish_families(self):
"""Return families for the instances of this creator.

Allow a Creator to define multiple families so that a creator can
e.g. specify `usd` and `usdMaya` and another USD creator can also
specify `usd` but apply different extractors like `usdMultiverse`.

There is no need to override this method if you only have the
primary family defined by the `family` property as that will always
be set.

Returns:
list: families for instances of this creator

"""
return []

def imprint_instance_node(self, node, data):

# We never store the instance_node as value on the node since
# it's the node name itself
data.pop("instance_node", None)

# Don't store `families` since it's up to the creator itself
# to define the initial publish families - not a stored attribute of
# `families`
data.pop("families", None)

# We store creator attributes at the root level and assume they
# will not clash in names with `subset`, `task`, etc. and other
# default names. This is just so these attributes in many cases
# are still editable in the maya UI by artists.
# pop to move to end of dict to sort attributes last on the node
# note: pop to move to end of dict to sort attributes last on the node
creator_attributes = data.pop("creator_attributes", {})

# We only flatten value types which `imprint` function supports
json_creator_attributes = {}
for key, value in dict(creator_attributes).items():
if isinstance(value, (list, tuple, dict)):
creator_attributes.pop(key)
json_creator_attributes[key] = value

# Flatten remaining creator attributes to the node itself
data.update(creator_attributes)

# We know the "publish_attributes" will be complex data of
Expand All @@ -150,6 +181,10 @@ def imprint_instance_node(self, node, data):
data.pop("publish_attributes", {})
)

# Persist the non-flattened creator attributes (special value types,
# like multiselection EnumDef)
data["creator_attributes"] = json.dumps(json_creator_attributes)

# Since we flattened the data structure for creator attributes we want
# to correctly detect which flattened attributes should end back in the
# creator attributes when reading the data from the node, so we store
Expand All @@ -170,22 +205,34 @@ def read_instance_node(self, node):
# being read as 'data'
node_data.pop("cbId", None)

# Make sure we convert any creator attributes from the json string
creator_attributes = node_data.get("creator_attributes")
if creator_attributes:
node_data["creator_attributes"] = json.loads(creator_attributes)
else:
node_data["creator_attributes"] = {}

# Move the relevant attributes into "creator_attributes" that
# we flattened originally
node_data["creator_attributes"] = {}
creator_attribute_keys = node_data.pop("__creator_attributes_keys",
"").split(",")
for key in creator_attribute_keys:
if key in node_data:
node_data["creator_attributes"][key] = node_data.pop(key)

# Make sure we convert any publish attributes from the json string
publish_attributes = node_data.get("publish_attributes")
if publish_attributes:
node_data["publish_attributes"] = json.loads(publish_attributes)

# Explicitly re-parse the node name
node_data["instance_node"] = node

# If the creator plug-in specifies
families = self.get_publish_families()
if families:
node_data["families"] = families

return node_data

def _default_collect_instances(self):
Expand Down Expand Up @@ -230,6 +277,14 @@ def create(self, subset_name, instance_data, pre_create_data):
if pre_create_data.get("use_selection"):
members = cmds.ls(selection=True)

# Allow a Creator to define multiple families
publish_families = self.get_publish_families()
if publish_families:
families = instance_data.setdefault("families", [])
for family in self.get_publish_families():
if family not in families:
families.append(family)

with lib.undo_chunk():
instance_node = cmds.sets(members, name=subset_name)
instance_data["instance_node"] = instance_node
Expand Down
102 changes: 102 additions & 0 deletions openpype/hosts/maya/plugins/create/create_maya_usd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from openpype.hosts.maya.api import plugin, lib
from openpype.lib import (
BoolDef,
EnumDef,
TextDef
)

from maya import cmds


class CreateMayaUsd(plugin.MayaCreator):
"""Create Maya USD Export"""

identifier = "io.openpype.creators.maya.mayausd"
label = "Maya USD"
family = "usd"
icon = "cubes"
description = "Create Maya USD Export"

cache = {}

def get_publish_families(self):
return ["usd", "mayaUsd"]

def get_instance_attr_defs(self):

if "jobContextItems" not in self.cache:
# Query once instead of per instance
job_context_items = {}
try:
cmds.loadPlugin("mayaUsdPlugin", quiet=True)
job_context_items = {
cmds.mayaUSDListJobContexts(jobContext=name): name
for name in cmds.mayaUSDListJobContexts(export=True) or []
}
except RuntimeError:
# Likely `mayaUsdPlugin` plug-in not available
self.log.warning("Unable to retrieve available job "
"contexts for `mayaUsdPlugin` exports")

if not job_context_items:
# enumdef multiselection may not be empty
job_context_items = ["<placeholder; do not use>"]

self.cache["jobContextItems"] = job_context_items

defs = lib.collect_animation_defs()
defs.extend([
EnumDef("defaultUSDFormat",
label="File format",
items={
"usdc": "Binary",
"usda": "ASCII"
},
default="usdc"),
BoolDef("stripNamespaces",
label="Strip Namespaces",
tooltip=(
"Remove namespaces during export. By default, "
"namespaces are exported to the USD file in the "
"following format: nameSpaceExample_pPlatonic1"
),
default=True),
BoolDef("mergeTransformAndShape",
label="Merge Transform and Shape",
tooltip=(
"Combine Maya transform and shape into a single USD"
"prim that has transform and geometry, for all"
" \"geometric primitives\" (gprims).\n"
"This results in smaller and faster scenes. Gprims "
"will be \"unpacked\" back into transform and shape "
"nodes when imported into Maya from USD."
),
default=True),
BoolDef("includeUserDefinedAttributes",
label="Include User Defined Attributes",
tooltip=(
"Whether to include all custom maya attributes found "
"on nodes as metadata (userProperties) in USD."
),
default=False),
TextDef("attr",
label="Custom Attributes",
default="",
placeholder="attr1, attr2"),
TextDef("attrPrefix",
label="Custom Attributes Prefix",
default="",
placeholder="prefix1, prefix2"),
EnumDef("jobContext",
label="Job Context",
items=self.cache["jobContextItems"],
tooltip=(
"Specifies an additional export context to handle.\n"
"These usually contain extra schemas, primitives,\n"
"and materials that are to be exported for a "
"specific\ntask, a target renderer for example."
),
multiselection=True),
])

return defs
4 changes: 4 additions & 0 deletions openpype/hosts/maya/plugins/create/create_multiverse_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class CreateMultiverseUsd(plugin.MayaCreator):
label = "Multiverse USD Asset"
family = "usd"
icon = "cubes"
description = "Create Multiverse USD Asset"

def get_publish_families(self):
return ["usd", "mvUsd"]

def get_instance_attr_defs(self):

Expand Down
6 changes: 4 additions & 2 deletions openpype/hosts/maya/plugins/load/load_arnold_standin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from openpype.hosts.maya.api.pipeline import containerise


def is_sequence(files):
sequence = False
collections, remainder = clique.assemble(files, minimum_items=1)
Expand All @@ -29,11 +30,12 @@ def get_current_session_fps():
session_fps = float(legacy_io.Session.get('AVALON_FPS', 25))
return convert_to_maya_fps(session_fps)


class ArnoldStandinLoader(load.LoaderPlugin):
"""Load as Arnold standin"""

families = ["ass", "animation", "model", "proxyAbc", "pointcache"]
representations = ["ass", "abc"]
families = ["ass", "animation", "model", "proxyAbc", "pointcache", "usd"]
representations = ["ass", "abc", "usda", "usdc", "usd"]
BigRoy marked this conversation as resolved.
Show resolved Hide resolved

label = "Load as Arnold standin"
order = -5
Expand Down
108 changes: 108 additions & 0 deletions openpype/hosts/maya/plugins/load/load_maya_usd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
import maya.cmds as cmds

from openpype.pipeline import (
load,
get_representation_path,
)
from openpype.pipeline.load import get_representation_path_from_context
from openpype.hosts.maya.api.lib import (
namespaced,
unique_namespace
)
from openpype.hosts.maya.api.pipeline import containerise


class MayaUsdLoader(load.LoaderPlugin):
"""Read USD data in a Maya USD Proxy"""

families = ["model", "usd", "pointcache", "animation"]
representations = ["usd", "usda", "usdc", "usdz", "abc"]

label = "Load USD to Maya Proxy"
order = -1
icon = "code-fork"
color = "orange"

def load(self, context, name=None, namespace=None, options=None):
asset = context['asset']['name']
namespace = namespace or unique_namespace(
asset + "_",
prefix="_" if asset[0].isdigit() else "",
suffix="_",
)

# Make sure we can load the plugin
cmds.loadPlugin("mayaUsdPlugin", quiet=True)

path = get_representation_path_from_context(context)

# Create the shape
cmds.namespace(addNamespace=namespace)
with namespaced(namespace, new=False):
transform = cmds.createNode("transform",
name=name,
skipSelect=True)
proxy = cmds.createNode('mayaUsdProxyShape',
name="{}Shape".format(name),
parent=transform,
skipSelect=True)

cmds.connectAttr("time1.outTime", "{}.time".format(proxy))
cmds.setAttr("{}.filePath".format(proxy), path, type="string")

# By default, we force the proxy to not use a shared stage because
# when doing so Maya will quite easily allow to save into the
# loaded usd file. Since we are loading published files we want to
# avoid altering them. Unshared stages also save their edits into
# the workfile as an artist might expect it to do.
cmds.setAttr("{}.shareStage".format(proxy), False)
# cmds.setAttr("{}.shareStage".format(proxy), lock=True)

nodes = [transform, proxy]
self[:] = nodes

return containerise(
name=name,
namespace=namespace,
nodes=nodes,
context=context,
loader=self.__class__.__name__)

def update(self, container, representation):
# type: (dict, dict) -> None
"""Update container with specified representation."""
node = container['objectName']
assert cmds.objExists(node), "Missing container"

members = cmds.sets(node, query=True) or []
shapes = cmds.ls(members, type="mayaUsdProxyShape")

path = get_representation_path(representation)
for shape in shapes:
cmds.setAttr("{}.filePath".format(shape), path, type="string")

cmds.setAttr("{}.representation".format(node),
str(representation["_id"]),
type="string")

def switch(self, container, representation):
self.update(container, representation)

def remove(self, container):
# type: (dict) -> None
"""Remove loaded container."""
# Delete container and its contents
if cmds.objExists(container['objectName']):
members = cmds.sets(container['objectName'], query=True) or []
cmds.delete([container['objectName']] + members)

# Remove the namespace, if empty
namespace = container['namespace']
if cmds.namespace(exists=namespace):
members = cmds.namespaceInfo(namespace, listNamespace=True)
if not members:
cmds.namespace(removeNamespace=namespace)
else:
self.log.warning("Namespace not deleted because it "
"still has members: %s", namespace)
14 changes: 0 additions & 14 deletions openpype/hosts/maya/plugins/publish/collect_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,3 @@ def process(self, instance):
if instance.data.get("farm"):
instance.data["families"].append("publish.farm")

# Collect user defined attributes.
if not instance.data.get("includeUserDefinedAttributes", False):
return

user_defined_attributes = set()
for node in hierarchy:
attrs = cmds.listAttr(node, userDefined=True) or list()
shapes = cmds.listRelatives(node, shapes=True) or list()
for shape in shapes:
attrs.extend(cmds.listAttr(shape, userDefined=True) or list())

user_defined_attributes.update(attrs)

instance.data["userDefinedAttributes"] = list(user_defined_attributes)
15 changes: 0 additions & 15 deletions openpype/hosts/maya/plugins/publish/collect_pointcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,3 @@ def process(self, instance):
if proxy_set:
instance.remove(proxy_set)
instance.data["setMembers"].remove(proxy_set)

# Collect user defined attributes.
if not instance.data.get("includeUserDefinedAttributes", False):
return

user_defined_attributes = set()
for node in instance:
attrs = cmds.listAttr(node, userDefined=True) or list()
shapes = cmds.listRelatives(node, shapes=True) or list()
for shape in shapes:
attrs.extend(cmds.listAttr(shape, userDefined=True) or list())

user_defined_attributes.update(attrs)

instance.data["userDefinedAttributes"] = list(user_defined_attributes)