Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/ayon_maya/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1769,6 +1769,10 @@ def get_container_members(container):
# Assume it's a container dictionary
container = container["objectName"]

if "," in container:
# Assume it's a UFE path - return it as the only member
return [container]

members = cmds.sets(container, query=True) or []
members = cmds.ls(members, long=True, objectsOnly=True) or []
all_members = set(members)
Expand Down
47 changes: 47 additions & 0 deletions client/ayon_maya/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,35 @@ def parse_container(container):
return data


def parse_usd_prim_container(prim, proxy):
"""Parse instance container from UsdPrim if it is marked as one

Args:
prim (pxr.Usd.Prim): USD Primitive
proxy (str): The maya usd stage proxy shape node the primitive
belongs to.

Returns:
dict: The container schema data for this container node.

"""
data = prim.GetCustomDataByKey("ayon")
if not data or not data.get("id") == AYON_CONTAINER_ID:
return

# Store transient data
data["prim"] = prim
data["proxy"] = proxy

# Store the maya UFE path as objectName
prim_path = str(prim.GetPath())
data["objectName"] = "{},{}".format(proxy, prim_path)
data["namespace"] = prim_path
data["name"] = proxy

return data


def _ls():
"""Yields AYON container node names.

Expand Down Expand Up @@ -402,6 +431,24 @@ def _maya_iterate(iterator):
if value in ids:
yield fn_dep.name()

for container in ls_maya_usd_proxy_prims():
yield container


def ls_maya_usd_proxy_prims():
# TODO: This might be nicer once the Loader API gets a refactor where
# the loaders themselves can return the containers from the scene
if cmds.pluginInfo("mayaUsdPlugin", query=True, loaded=True):
usd_proxies = cmds.ls(type="mayaUsdProxyShape", long=True)
if usd_proxies:
import mayaUsd.ufe
for proxy in usd_proxies:
stage = mayaUsd.ufe.getStage('|world' + proxy)
for prim in stage.TraverseAll():
container = parse_usd_prim_container(prim, proxy=proxy)
if container:
yield container


def ls():
"""Yields containers from active Maya scene
Expand Down
80 changes: 80 additions & 0 deletions client/ayon_maya/api/usdlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from ayon_core.pipeline.constants import AVALON_CONTAINER_ID
from maya import cmds
from pxr import Sdf


def remove_spec(spec):
"""Delete Sdf.PrimSpec or Sdf.PropertySpec

Also see:
https://forum.aousd.org/t/api-basics-for-designing-a-manage-edits-editor-for-usd/676/1 # noqa
https://gist.github.com/BigRoy/4d2bf2eef6c6a83f4fda3c58db1489a5

"""
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]
else:
raise TypeError(f"Unsupported spec type: {spec}")


def iter_ufe_usd_selection():
"""Yield Maya USD Proxy Shape related UFE paths in selection.

The returned path are the Maya node name joined by a command to the
USD prim path.

Yields:
str: Path to UFE path in USD stage in selection.

"""
for path in cmds.ls(selection=True, ufeObjects=True, long=True,
absoluteName=True):
if "," not in path:
continue

node, ufe_path = path.split(",", 1)
if cmds.nodeType(node) != "mayaUsdProxyShape":
continue

yield path


def containerise_prim(prim,
name,
namespace,
context,
loader):
"""Containerise a USD prim.

Arguments:
prim (pxr.Usd.Prim): The prim to containerise.
name (str): Name to containerize.
namespace (str): Namespace to containerize.
context (dict): Load context (incl. representation).
name (str): Name to containerize.
loader (str): Loader name.

"""
for key, value in {
"ayon:schema": "openpype:container-2.0",
"ayon:id": AVALON_CONTAINER_ID,
"ayon:name": name,
"ayon:namespace": namespace,
"ayon:loader": loader,
"ayon:representation": context["representation"]["id"],
}.items():
prim.SetCustomDataByKey(key, str(value))
152 changes: 148 additions & 4 deletions client/ayon_maya/plugins/create/create_maya_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@
from ayon_core.lib import (
BoolDef,
EnumDef,
TextDef
TextDef,
UILabelDef,
UISeparatorDef,
)

from maya import cmds


class CreateMayaUsd(plugin.MayaCreator):
"""Create Maya USD Export"""
"""Create Maya USD Export from maya scene objects"""

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

cache = {}

def get_publish_families(self):
Expand Down Expand Up @@ -44,7 +45,15 @@ def get_instance_attr_defs(self):

self.cache["jobContextItems"] = job_context_items

defs = lib.collect_animation_defs()
defs = [
BoolDef("exportAnimationData",
label="Export Animation Data",
tooltip="When disabled no frame range is exported and "
"only the start frame is used to define the "
"static export frame.",
default=True)
]
defs.extend(lib.collect_animation_defs())
defs.extend([
EnumDef("defaultUSDFormat",
label="File format",
Expand All @@ -53,6 +62,17 @@ def get_instance_attr_defs(self):
"usda": "ASCII"
},
default="usdc"),
# TODO: Remove note from tooltip when issue is resolved, see:
# https://github.com/Autodesk/maya-usd/issues/3389
BoolDef("exportRoots",
label="Export as roots",
tooltip=(
"Export the members of the object sets without "
"their parents.\n"
"Note: There's an export bug that when this is "
"enabled MayaUsd fails to export instance meshes"
),
default=True),
BoolDef("stripNamespaces",
label="Strip Namespaces",
tooltip=(
Expand Down Expand Up @@ -100,3 +120,127 @@ def get_instance_attr_defs(self):
])

return defs


class CreateMayaUsdContribution(CreateMayaUsd):
"""

When writing a USD as 'contribution' it will be added into what it's
contributing to. It will usually contribute to either the main *asset*
or *shot* but can be customized.

Usually the contribution is done into a Department Layer, like e.g.
model, rig, look for models and layout, animation, fx, lighting for shots.

Each department contribution will be 'sublayered' into the departments
contribution.

"""

identifier = "io.openpype.creators.maya.mayausd.assetcontribution"
label = "Maya USD Asset Contribution"
product_type = "usd"
icon = "cubes"
description = "Create Maya USD Contribution"

# default_variants = ["main"]
# TODO: Do not include material for model publish
# TODO: Do only include material + assignments for material publish
# + attribute overrides onto existing geo? (`over`?)
# Define all in `geo` as `over`?

bootstrap = "asset"

contribution_asset_layer = None

def create_template_hierarchy(self, folder_name, variant):
"""Create the asset root template to hold the geo for the usd asset.

Args:
folder_name: Asset name to use for the group
variant: Variant name to use as namespace.
This is needed so separate asset contributions can be
correctly created from a single scene.

Returns:
list: The root node and geometry group.

"""

def set_usd_type(node, value):
attr = "USD_typeName"
if not cmds.attributeQuery(attr, node=node, exists=True):
cmds.addAttr(node, ln=attr, dt="string")
cmds.setAttr(f"{node}.{attr}", value, type="string")

# Ensure simple unique namespace (add trailing number)
namespace = variant
name = f"{namespace}:{folder_name}"
i = 1
while cmds.objExists(name):
name = f"{namespace}{i}:{folder_name}"
i += 1

# Define template hierarchy {folder_name}/geo
root = cmds.createNode("transform",
name=name,
skipSelect=True)
geo = cmds.createNode("transform",
name="geo",
parent=root,
skipSelect=True)
set_usd_type(geo, "Scope")
# Lock + hide transformations since we're exporting as Scope
for attr in ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"]:
cmds.setAttr(f"{geo}.{attr}", lock=True, keyable=False)

return [root, geo]

def create(self, subset_name, instance_data, pre_create_data):

# Create template hierarchy
if pre_create_data.get("createTemplateHierarchy", True):
members = []
if pre_create_data.get("use_selection"):
members = cmds.ls(selection=True,
long=True,
type="dagNode")

folder_path = instance_data["folderPath"]
folder_name = folder_path.rsplit("/", 1)[-1]

root, geo = self.create_template_hierarchy(
folder_name=folder_name,
variant=instance_data["variant"]
)

if members:
cmds.parent(members, geo)

# Select root and enable selection just so parent class'
# create adds it to the created instance
cmds.select(root, replace=True, noExpand=True)
pre_create_data["use_selection"] = True

# Create as if we're the other plug-in so that the instance after
# creation thinks it was created by `CreateMayaUsd` and this Creator
# here is solely used to apply different default values
# TODO: Improve this hack
CreateMayaUsd(
project_settings=self.project_settings,
create_context=self.create_context
).create(
subset_name,
instance_data,
pre_create_data
)

def get_pre_create_attr_defs(self):
defs = super(CreateMayaUsdContribution,
self).get_pre_create_attr_defs()
defs.extend([
BoolDef("createTemplateHierarchy",
label="Create template hierarchy",
default=True)
])
return defs
Loading