Draft for implementing a native Maya USD creator next to the Multiverse USD creator

This commit is contained in:
Roy Nieterau 2023-09-05 17:56:09 +02:00
parent 8aee7d0ca4
commit 8ceedd7b60
6 changed files with 451 additions and 1 deletions

View file

@ -129,12 +129,34 @@ class MayaCreatorBase(object):
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
@ -186,6 +208,11 @@ class MayaCreatorBase(object):
# 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):
@ -230,6 +257,14 @@ class MayaCreator(NewCreator, MayaCreatorBase):
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

View file

@ -0,0 +1,143 @@
from openpype.hosts.maya.api import plugin, lib
from openpype.lib import (
BoolDef,
NumberDef,
TextDef,
EnumDef
)
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"
def get_publish_families(self):
return ["usd", "mayaUsd"]
def get_instance_attr_defs(self):
defs = lib.collect_animation_defs()
defs.extend([
EnumDef("defaultUSDFormat",
label="File format",
items={
"usdc": "Binary",
"usda": "ASCII"
},
default="usdc"),
BoolDef("stripNamespaces",
label="Strip Namespaces",
default=True),
BoolDef("mergeTransformAndShape",
label="Merge Transform and Shape",
default=True),
# BoolDef("writeAncestors",
# label="Write Ancestors",
# default=True),
# BoolDef("flattenParentXforms",
# label="Flatten Parent Xforms",
# default=False),
# BoolDef("writeSparseOverrides",
# label="Write Sparse Overrides",
# default=False),
# BoolDef("useMetaPrimPath",
# label="Use Meta Prim Path",
# default=False),
# TextDef("customRootPath",
# label="Custom Root Path",
# default=''),
# TextDef("customAttributes",
# label="Custom Attributes",
# tooltip="Comma-separated list of attribute names",
# default=''),
# TextDef("nodeTypesToIgnore",
# label="Node Types to Ignore",
# tooltip="Comma-separated list of node types to be ignored",
# default=''),
# BoolDef("writeMeshes",
# label="Write Meshes",
# default=True),
# BoolDef("writeCurves",
# label="Write Curves",
# default=True),
# BoolDef("writeParticles",
# label="Write Particles",
# default=True),
# BoolDef("writeCameras",
# label="Write Cameras",
# default=False),
# BoolDef("writeLights",
# label="Write Lights",
# default=False),
# BoolDef("writeJoints",
# label="Write Joints",
# default=False),
# BoolDef("writeCollections",
# label="Write Collections",
# default=False),
# BoolDef("writePositions",
# label="Write Positions",
# default=True),
# BoolDef("writeNormals",
# label="Write Normals",
# default=True),
# BoolDef("writeUVs",
# label="Write UVs",
# default=True),
# BoolDef("writeColorSets",
# label="Write Color Sets",
# default=False),
# BoolDef("writeTangents",
# label="Write Tangents",
# default=False),
# BoolDef("writeRefPositions",
# label="Write Ref Positions",
# default=True),
# BoolDef("writeBlendShapes",
# label="Write BlendShapes",
# default=False),
# BoolDef("writeDisplayColor",
# label="Write Display Color",
# default=True),
# BoolDef("writeSkinWeights",
# label="Write Skin Weights",
# default=False),
# BoolDef("writeMaterialAssignment",
# label="Write Material Assignment",
# default=False),
# BoolDef("writeHardwareShader",
# label="Write Hardware Shader",
# default=False),
# BoolDef("writeShadingNetworks",
# label="Write Shading Networks",
# default=False),
# BoolDef("writeTransformMatrix",
# label="Write Transform Matrix",
# default=True),
# BoolDef("writeUsdAttributes",
# label="Write USD Attributes",
# default=True),
# BoolDef("writeInstancesAsReferences",
# label="Write Instances as References",
# default=False),
# BoolDef("timeVaryingTopology",
# label="Time Varying Topology",
# default=False),
# TextDef("customMaterialNamespace",
# label="Custom Material Namespace",
# default=''),
# NumberDef("numTimeSamples",
# label="Num Time Samples",
# default=1),
# NumberDef("timeSamplesSpan",
# label="Time Samples Span",
# default=0.0),
#
])
return defs

View file

@ -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):

View file

@ -0,0 +1,100 @@
# -*- 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")
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)

View file

@ -0,0 +1,168 @@
import os
import six
from maya import cmds
import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.maya.api.lib import maintained_selection
class ExtractMayaUsd(publish.Extractor):
"""Extractor for Maya USD Asset data.
Upon publish a .usd (or .usdz) asset file will typically be written.
"""
label = "Extract Maya USD Asset"
hosts = ["maya"]
families = ["mayaUsd"]
@property
def options(self):
"""Overridable options for Maya USD Export
Given in the following format
- {NAME: EXPECTED TYPE}
If the overridden option's type does not match,
the option is not included and a warning is logged.
"""
# TODO: Support more `mayaUSDExport` parameters
return {
"stripNamespaces": bool,
"mergeTransformAndShape": bool,
"exportDisplayColor": bool,
"exportColorSets": bool,
"exportInstances": bool,
"exportUVs": bool,
"exportVisibility": bool,
"exportComponentTags": bool,
"exportRefsAsInstanceable": bool,
"eulerFilter": bool,
"renderableOnly": bool,
#"worldspace": bool,
}
@property
def default_options(self):
"""The default options for Maya USD Export."""
# TODO: Support more `mayaUSDExport` parameters
return {
"stripNamespaces": False,
"mergeTransformAndShape": False,
"exportDisplayColor": False,
"exportColorSets": True,
"exportInstances": True,
"exportUVs": True,
"exportVisibility": True,
"exportComponentTags": True,
"exportRefsAsInstanceable": False,
"eulerFilter": True,
"renderableOnly": False,
#"worldspace": False
}
def parse_overrides(self, instance, options):
"""Inspect data of instance to determine overridden options"""
for key in instance.data:
if key not in self.options:
continue
# Ensure the data is of correct type
value = instance.data[key]
if isinstance(value, six.text_type):
value = str(value)
if not isinstance(value, self.options[key]):
self.log.warning(
"Overridden attribute {key} was of "
"the wrong type: {invalid_type} "
"- should have been {valid_type}".format(
key=key,
invalid_type=type(value).__name__,
valid_type=self.options[key].__name__))
continue
options[key] = value
return options
def filter_members(self, members):
# Can be overridden by inherited classes
return members
def process(self, instance):
# Load plugin first
cmds.loadPlugin("mayaUsdPlugin", quiet=True)
# Define output file path
staging_dir = self.staging_dir(instance)
file_name = "{0}.usd".format(instance.name)
file_path = os.path.join(staging_dir, file_name)
file_path = file_path.replace('\\', '/')
# Parse export options
options = self.default_options
options = self.parse_overrides(instance, options)
self.log.info("Export options: {0}".format(options))
# Perform extraction
self.log.debug("Performing extraction ...")
members = instance.data("setMembers")
self.log.debug('Collected objects: {}'.format(members))
members = self.filter_members(members)
if not members:
self.log.error('No members!')
return
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
with maintained_selection():
self.log.debug('Exporting USD: {} / {}'.format(file_path, members))
cmds.mayaUSDExport(file=file_path,
frameRange=(start, end),
frameStride=instance.data.get("step", 1.0),
exportRoots=members,
**options)
representation = {
'name': "usd",
'ext': "usd",
'files': file_name,
'stagingDir': staging_dir
}
instance.data.setdefault("representations", []).append(representation)
self.log.debug(
"Extracted instance {} to {}".format(instance.name, file_path)
)
class ExtractMayaUsdAnim(ExtractMayaUsd):
"""Extractor for Maya USD Animation Sparse Cache data.
This will extract the sparse cache data from the scene and generate a
USD file with all the animation data.
Upon publish a .usd sparse cache will be written.
"""
label = "Extract Maya USD Animation Sparse Cache"
families = ["animation", "mayaUsd"]
match = pyblish.api.Subset
def filter_members(self, members):
out_set = next((i for i in members if i.endswith("out_SET")), None)
if out_set is None:
self.log.warning("Expecting out_SET")
return None
members = cmds.ls(cmds.sets(out_set, query=True), long=True)
return members

View file

@ -28,7 +28,7 @@ class ExtractMultiverseUsd(publish.Extractor):
label = "Extract Multiverse USD Asset"
hosts = ["maya"]
families = ["usd"]
families = ["mvUsd"]
scene_type = "usd"
file_formats = ["usd", "usda", "usdz"]