diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_look.py b/openpype/hosts/maya/plugins/create/create_multiverse_look.py new file mode 100644 index 00000000000..f47c88a93b4 --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_multiverse_look.py @@ -0,0 +1,15 @@ +from openpype.hosts.maya.api import plugin + + +class CreateMultiverseLook(plugin.Creator): + """Create Multiverse Look""" + + name = "mvLook" + label = "Multiverse Look" + family = "mvLook" + icon = "cubes" + + def __init__(self, *args, **kwargs): + super(CreateMultiverseLook, self).__init__(*args, **kwargs) + self.data["fileFormat"] = ["usda", "usd"] + self.data["publishMipMap"] = True diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd.py index b2266e5a577..034714d51bc 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd.py @@ -2,11 +2,11 @@ class CreateMultiverseUsd(plugin.Creator): - """Multiverse USD data""" + """Create Multiverse USD Asset""" - name = "usdMain" - label = "Multiverse USD" - family = "usd" + name = "mvUsdMain" + label = "Multiverse USD Asset" + family = "mvUsd" icon = "cubes" def __init__(self, *args, **kwargs): @@ -15,6 +15,7 @@ def __init__(self, *args, **kwargs): # Add animation data first, since it maintains order. self.data.update(lib.collect_animation_data(True)) + self.data["fileFormat"] = ["usd", "usda", "usdz"] self.data["stripNamespaces"] = False self.data["mergeTransformAndShape"] = False self.data["writeAncestors"] = True @@ -45,6 +46,7 @@ def __init__(self, *args, **kwargs): self.data["writeShadingNetworks"] = False self.data["writeTransformMatrix"] = True self.data["writeUsdAttributes"] = False + self.data["writeInstancesAsReferences"] = False self.data["timeVaryingTopology"] = False self.data["customMaterialNamespace"] = '' self.data["numTimeSamples"] = 1 diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py index 77b808c459c..ed466a80688 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py @@ -4,9 +4,9 @@ class CreateMultiverseUsdComp(plugin.Creator): """Create Multiverse USD Composition""" - name = "usdCompositionMain" + name = "mvUsdCompositionMain" label = "Multiverse USD Composition" - family = "usdComposition" + family = "mvUsdComposition" icon = "cubes" def __init__(self, *args, **kwargs): @@ -15,9 +15,12 @@ def __init__(self, *args, **kwargs): # Add animation data first, since it maintains order. self.data.update(lib.collect_animation_data(True)) + # Order of `fileFormat` must match extract_multiverse_usd_comp.py + self.data["fileFormat"] = ["usda", "usd"] self.data["stripNamespaces"] = False self.data["mergeTransformAndShape"] = False self.data["flattenContent"] = False + self.data["writeAsCompoundLayers"] = False self.data["writePendingOverrides"] = False self.data["numTimeSamples"] = 1 self.data["timeSamplesSpan"] = 0.0 diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py index bb82ab2039a..06e22df2952 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py @@ -2,11 +2,11 @@ class CreateMultiverseUsdOver(plugin.Creator): - """Multiverse USD data""" + """Create Multiverse USD Override""" - name = "usdOverrideMain" + name = "mvUsdOverrideMain" label = "Multiverse USD Override" - family = "usdOverride" + family = "mvUsdOverride" icon = "cubes" def __init__(self, *args, **kwargs): @@ -15,6 +15,8 @@ def __init__(self, *args, **kwargs): # Add animation data first, since it maintains order. self.data.update(lib.collect_animation_data(True)) + # Order of `fileFormat` must match extract_multiverse_usd_over.py + self.data["fileFormat"] = ["usda", "usd"] self.data["writeAll"] = False self.data["writeTransforms"] = True self.data["writeVisibility"] = True diff --git a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py index c03f2c5d926..3350dc6ac9d 100644 --- a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py @@ -14,13 +14,13 @@ class MultiverseUsdLoader(load.LoaderPlugin): - """Load the USD by Multiverse""" + """Read USD data in a Multiverse Compound""" - families = ["model", "usd", "usdComposition", "usdOverride", + families = ["model", "mvUsd", "mvUsdComposition", "mvUsdOverride", "pointcache", "animation"] representations = ["usd", "usda", "usdc", "usdz", "abc"] - label = "Read USD by Multiverse" + label = "Load USD to Multiverse" order = -10 icon = "code-fork" color = "orange" diff --git a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py new file mode 100644 index 00000000000..edf40a27a6f --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -0,0 +1,372 @@ +import glob +import os +import re + +from maya import cmds +import pyblish.api +from openpype.hosts.maya.api import lib + +SHAPE_ATTRS = ["castsShadows", + "receiveShadows", + "motionBlur", + "primaryVisibility", + "smoothShading", + "visibleInReflections", + "visibleInRefractions", + "doubleSided", + "opposite"] + +SHAPE_ATTRS = set(SHAPE_ATTRS) +COLOUR_SPACES = ['sRGB', 'linear', 'auto'] +MIPMAP_EXTENSIONS = ['tdl'] + + +def get_look_attrs(node): + """Returns attributes of a node that are important for the look. + + These are the "changed" attributes (those that have edits applied + in the current scene). + + Returns: + list: Attribute names to extract + + """ + # When referenced get only attributes that are "changed since file open" + # which includes any reference edits, otherwise take *all* user defined + # attributes + is_referenced = cmds.referenceQuery(node, isNodeReferenced=True) + result = cmds.listAttr(node, userDefined=True, + changedSinceFileOpen=is_referenced) or [] + + # `cbId` is added when a scene is saved, ignore by default + if "cbId" in result: + result.remove("cbId") + + # For shapes allow render stat changes + if cmds.objectType(node, isAType="shape"): + attrs = cmds.listAttr(node, changedSinceFileOpen=True) or [] + for attr in attrs: + if attr in SHAPE_ATTRS: + result.append(attr) + elif attr.startswith('ai'): + result.append(attr) + + return result + + +def node_uses_image_sequence(node): + """Return whether file node uses an image sequence or single image. + + Determine if a node uses an image sequence or just a single image, + not always obvious from its file path alone. + + Args: + node (str): Name of the Maya node + + Returns: + bool: True if node uses an image sequence + + """ + + # useFrameExtension indicates an explicit image sequence + node_path = get_file_node_path(node).lower() + + # The following tokens imply a sequence + patterns = ["", "", "", "u_v", ".tif will return as /path/to/texture.*.tif. + + Args: + path (str): the image sequence path + + Returns: + str: Return glob string that matches the filename pattern. + + """ + + if path is None: + return path + + # If any of the patterns, convert the pattern + patterns = { + "": "", + "": "", + "": "", + "#": "#", + "u_v": "|", + "", # noqa - copied from collect_look.py + "": "" + } + + lower = path.lower() + has_pattern = False + for pattern, regex_pattern in patterns.items(): + if pattern in lower: + path = re.sub(regex_pattern, "*", path, flags=re.IGNORECASE) + has_pattern = True + + if has_pattern: + return path + + base = os.path.basename(path) + matches = list(re.finditer(r'\d+', base)) + if matches: + match = matches[-1] + new_base = '{0}*{1}'.format(base[:match.start()], + base[match.end():]) + head = os.path.dirname(path) + return os.path.join(head, new_base) + else: + return path + + +def get_file_node_path(node): + """Get the file path used by a Maya file node. + + Args: + node (str): Name of the Maya file node + + Returns: + str: the file path in use + + """ + # if the path appears to be sequence, use computedFileTextureNamePattern, + # this preserves the <> tag + if cmds.attributeQuery('computedFileTextureNamePattern', + node=node, + exists=True): + plug = '{0}.computedFileTextureNamePattern'.format(node) + texture_pattern = cmds.getAttr(plug) + + patterns = ["", + "", + "u_v", + "", + ""] + lower = texture_pattern.lower() + if any(pattern in lower for pattern in patterns): + return texture_pattern + + if cmds.nodeType(node) == 'aiImage': + return cmds.getAttr('{0}.filename'.format(node)) + if cmds.nodeType(node) == 'RedshiftNormalMap': + return cmds.getAttr('{}.tex0'.format(node)) + + # otherwise use fileTextureName + return cmds.getAttr('{0}.fileTextureName'.format(node)) + + +def get_file_node_files(node): + """Return the file paths related to the file node + + Note: + Will only return existing files. Returns an empty list + if not valid existing files are linked. + + Returns: + list: List of full file paths. + + """ + + path = get_file_node_path(node) + path = cmds.workspace(expandName=path) + if node_uses_image_sequence(node): + glob_pattern = seq_to_glob(path) + return glob.glob(glob_pattern) + elif os.path.exists(path): + return [path] + else: + return [] + + +def get_mipmap(fname): + for colour_space in COLOUR_SPACES: + for mipmap_ext in MIPMAP_EXTENSIONS: + mipmap_fname = '.'.join([fname, colour_space, mipmap_ext]) + if os.path.exists(mipmap_fname): + return mipmap_fname + return None + + +def is_mipmap(fname): + ext = os.path.splitext(fname)[1][1:] + if ext in MIPMAP_EXTENSIONS: + return True + return False + + +class CollectMultiverseLookData(pyblish.api.InstancePlugin): + """Collect Multiverse Look + + """ + + order = pyblish.api.CollectorOrder + 0.2 + label = 'Collect Multiverse Look' + families = ["mvLook"] + + def process(self, instance): + # Load plugin first + cmds.loadPlugin("MultiverseForMaya", quiet=True) + import multiverse + + self.log.info("Processing mvLook for '{}'".format(instance)) + + nodes = set() + for node in instance: + # We want only mvUsdCompoundShape nodes. + nodes_of_interest = cmds.ls(node, + dag=True, + shapes=False, + type="mvUsdCompoundShape", + noIntermediate=True, + long=True) + nodes.update(nodes_of_interest) + + files = [] + sets = {} + instance.data["resources"] = [] + publishMipMap = instance.data["publishMipMap"] + + for node in nodes: + self.log.info("Getting resources for '{}'".format(node)) + + # We know what nodes need to be collected, now we need to + # extract the materials overrides. + overrides = multiverse.ListMaterialOverridePrims(node) + for override in overrides: + matOver = multiverse.GetMaterialOverride(node, override) + + if isinstance(matOver, multiverse.MaterialSourceShadingGroup): + # We now need to grab the shadingGroup so add it to the + # sets we pass down the pipe. + shadingGroup = matOver.shadingGroupName + self.log.debug("ShadingGroup = '{}'".format(shadingGroup)) + sets[shadingGroup] = {"uuid": lib.get_id( + shadingGroup), "members": list()} + + # The SG may reference files, add those too! + history = cmds.listHistory(shadingGroup) + files = cmds.ls(history, type="file", long=True) + + for f in files: + resources = self.collect_resource(f, publishMipMap) + instance.data["resources"].append(resources) + + elif isinstance(matOver, multiverse.MaterialSourceUsdPath): + # TODO: Handle this later. + pass + + # Store data on the instance for validators, extractos, etc. + instance.data["lookData"] = { + "attributes": [], + "relationships": sets + } + + def collect_resource(self, node, publishMipMap): + """Collect the link to the file(s) used (resource) + Args: + node (str): name of the node + + Returns: + dict + """ + + self.log.debug("processing: {}".format(node)) + if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]: + self.log.error( + "Unsupported file node: {}".format(cmds.nodeType(node))) + raise AssertionError("Unsupported file node") + + if cmds.nodeType(node) == 'file': + self.log.debug(" - file node") + attribute = "{}.fileTextureName".format(node) + computed_attribute = "{}.computedFileTextureNamePattern".format( + node) + elif cmds.nodeType(node) == 'aiImage': + self.log.debug("aiImage node") + attribute = "{}.filename".format(node) + computed_attribute = attribute + elif cmds.nodeType(node) == 'RedshiftNormalMap': + self.log.debug("RedshiftNormalMap node") + attribute = "{}.tex0".format(node) + computed_attribute = attribute + + source = cmds.getAttr(attribute) + self.log.info(" - file source: {}".format(source)) + color_space_attr = "{}.colorSpace".format(node) + try: + color_space = cmds.getAttr(color_space_attr) + except ValueError: + # node doesn't have colorspace attribute + color_space = "Raw" + # Compare with the computed file path, e.g. the one with the + # pattern in it, to generate some logging information about this + # difference + # computed_attribute = "{}.computedFileTextureNamePattern".format(node) + computed_source = cmds.getAttr(computed_attribute) + if source != computed_source: + self.log.debug("Detected computed file pattern difference " + "from original pattern: {0} " + "({1} -> {2})".format(node, + source, + computed_source)) + + # We replace backslashes with forward slashes because V-Ray + # can't handle the UDIM files with the backslashes in the + # paths as the computed patterns + source = source.replace("\\", "/") + + files = get_file_node_files(node) + files = self.handle_files(files, publishMipMap) + if len(files) == 0: + self.log.error("No valid files found from node `%s`" % node) + + self.log.info("collection of resource done:") + self.log.info(" - node: {}".format(node)) + self.log.info(" - attribute: {}".format(attribute)) + self.log.info(" - source: {}".format(source)) + self.log.info(" - file: {}".format(files)) + self.log.info(" - color space: {}".format(color_space)) + + # Define the resource + return {"node": node, + "attribute": attribute, + "source": source, # required for resources + "files": files, + "color_space": color_space} # required for resources + + def handle_files(self, files, publishMipMap): + """This will go through all the files and make sure that they are + either already mipmapped or have a corresponding mipmap sidecar and + add that to the list.""" + if not publishMipMap: + return files + + extra_files = [] + self.log.debug("Expecting MipMaps, going to look for them.") + for fname in files: + self.log.info("Checking '{}' for mipmaps".format(fname)) + if is_mipmap(fname): + self.log.debug(" - file is already MipMap, skipping.") + continue + + mipmap = get_mipmap(fname) + if mipmap: + self.log.info(" mipmap found for '{}'".format(fname)) + extra_files.append(mipmap) + else: + self.log.warning(" no mipmap found for '{}'".format(fname)) + return files + extra_files diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 81d7c31ae79..d35b529c768 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -146,7 +146,7 @@ class ExtractLook(openpype.api.Extractor): label = "Extract Look (Maya Scene + JSON)" hosts = ["maya"] - families = ["look"] + families = ["look", "mvLook"] order = pyblish.api.ExtractorOrder + 0.2 scene_type = "ma" look_data_type = "json" diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_look.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_look.py new file mode 100644 index 00000000000..82e2b41929c --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_look.py @@ -0,0 +1,157 @@ +import os + +from maya import cmds + +import openpype.api +from openpype.hosts.maya.api.lib import maintained_selection + + +class ExtractMultiverseLook(openpype.api.Extractor): + """Extractor for Multiverse USD look data. + + This will extract: + + - the shading networks that are assigned in MEOW as Maya material overrides + to a Multiverse Compound + - settings for a Multiverse Write Override operation. + + Relevant settings are visible in the Maya set node created by a Multiverse + USD Look instance creator. + + The input data contained in the set is: + + - a single Multiverse Compound node with any number of Maya material + overrides (typically set in MEOW) + + Upon publish two files will be written: + + - a .usda override file containing material assignment information + - a .ma file containing shading networks + + Note: when layering the material assignment override on a loaded Compound, + remember to set a matching attribute override with the namespace of + the loaded compound in order for the material assignment to resolve. + """ + + label = "Extract Multiverse USD Look" + hosts = ["maya"] + families = ["mvLook"] + scene_type = "usda" + file_formats = ["usda", "usd"] + + @property + def options(self): + """Overridable options for Multiverse 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. + + """ + + return { + "writeAll": bool, + "writeTransforms": bool, + "writeVisibility": bool, + "writeAttributes": bool, + "writeMaterials": bool, + "writeVariants": bool, + "writeVariantsDefinition": bool, + "writeActiveState": bool, + "writeNamespaces": bool, + "numTimeSamples": int, + "timeSamplesSpan": float + } + + @property + def default_options(self): + """The default options for Multiverse USD extraction.""" + + return { + "writeAll": False, + "writeTransforms": False, + "writeVisibility": False, + "writeAttributes": False, + "writeMaterials": True, + "writeVariants": False, + "writeVariantsDefinition": False, + "writeActiveState": False, + "writeNamespaces": False, + "numTimeSamples": 1, + "timeSamplesSpan": 0.0 + } + + def get_file_format(self, instance): + fileFormat = instance.data["fileFormat"] + if fileFormat in range(len(self.file_formats)): + self.scene_type = self.file_formats[fileFormat] + + def process(self, instance): + # Load plugin first + cmds.loadPlugin("MultiverseForMaya", quiet=True) + + # Define output file path + staging_dir = self.staging_dir(instance) + self.get_file_format(instance) + file_name = "{0}.{1}".format(instance.name, self.scene_type) + file_path = os.path.join(staging_dir, file_name) + file_path = file_path.replace('\\', '/') + + # Parse export options + options = self.default_options + self.log.info("Export options: {0}".format(options)) + + # Perform extraction + self.log.info("Performing extraction ...") + + with maintained_selection(): + members = instance.data("setMembers") + members = cmds.ls(members, + dag=True, + shapes=False, + type="mvUsdCompoundShape", + noIntermediate=True, + long=True) + self.log.info('Collected object {}'.format(members)) + if len(members) > 1: + self.log.error('More than one member: {}'.format(members)) + + import multiverse + + over_write_opts = multiverse.OverridesWriteOptions() + options_discard_keys = { + "numTimeSamples", + "timeSamplesSpan", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "step", + "fps" + } + for key, value in options.items(): + if key in options_discard_keys: + continue + setattr(over_write_opts, key, value) + + for member in members: + # @TODO: Make sure there is only one here. + + self.log.debug("Writing Override for '{}'".format(member)) + multiverse.WriteOverrides(file_path, member, over_write_opts) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': self.scene_type, + 'ext': self.scene_type, + 'files': file_name, + 'stagingDir': staging_dir + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance {} to {}".format( + instance.name, file_path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py index 4e4efdc32cb..3654be7b34c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py @@ -8,11 +8,27 @@ class ExtractMultiverseUsd(openpype.api.Extractor): - """Extractor for USD by Multiverse.""" + """Extractor for Multiverse USD Asset data. - label = "Extract Multiverse USD" + This will extract settings for a Multiverse Write Asset operation: + they are visible in the Maya set node created by a Multiverse USD + Asset instance creator. + + The input data contained in the set is: + + - a single hierarchy of Maya nodes. Multiverse supports a variety of Maya + nodes such as transforms, mesh, curves, particles, instances, particle + instancers, pfx, MASH, lights, cameras, joints, connected materials, + shading networks etc. including many of their attributes. + + Upon publish a .usd (or .usdz) asset file will be typically written. + """ + + label = "Extract Multiverse USD Asset" hosts = ["maya"] - families = ["usd"] + families = ["mvUsd"] + scene_type = "usd" + file_formats = ["usd", "usda", "usdz"] @property def options(self): @@ -57,6 +73,7 @@ def options(self): "writeShadingNetworks": bool, "writeTransformMatrix": bool, "writeUsdAttributes": bool, + "writeInstancesAsReferences": bool, "timeVaryingTopology": bool, "customMaterialNamespace": str, "numTimeSamples": int, @@ -98,6 +115,7 @@ def default_options(self): "writeShadingNetworks": False, "writeTransformMatrix": True, "writeUsdAttributes": False, + "writeInstancesAsReferences": False, "timeVaryingTopology": False, "customMaterialNamespace": str(), "numTimeSamples": 1, @@ -130,12 +148,15 @@ def parse_overrides(self, instance, options): return options def process(self, instance): - # Load plugin firstly + # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) - file_name = "{}.usd".format(instance.name) + file_format = instance.data.get("fileFormat", 0) + if file_format in range(len(self.file_formats)): + self.scene_type = self.file_formats[file_format] + file_name = "{0}.{1}".format(instance.name, self.scene_type) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace('\\', '/') @@ -149,12 +170,6 @@ def process(self, instance): with maintained_selection(): members = instance.data("setMembers") - members = cmds.ls(members, - dag=True, - shapes=True, - type=("mesh"), - noIntermediate=True, - long=True) self.log.info('Collected object {}'.format(members)) import multiverse @@ -199,10 +214,10 @@ def process(self, instance): instance.data["representations"] = [] representation = { - 'name': 'usd', - 'ext': 'usd', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': file_name, - "stagingDir": staging_dir + 'stagingDir': staging_dir } instance.data["representations"].append(representation) diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py index 8fccc412e6e..ad9303657fa 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py @@ -7,11 +7,28 @@ class ExtractMultiverseUsdComposition(openpype.api.Extractor): - """Extractor of Multiverse USD Composition.""" + """Extractor of Multiverse USD Composition data. + + This will extract settings for a Multiverse Write Composition operation: + they are visible in the Maya set node created by a Multiverse USD + Composition instance creator. + + The input data contained in the set is either: + + - a single hierarchy consisting of several Multiverse Compound nodes, with + any number of layers, and Maya transform nodes + - a single Compound node with more than one layer (in this case the "Write + as Compound Layers" option should be set). + + Upon publish a .usda composition file will be written. + """ label = "Extract Multiverse USD Composition" hosts = ["maya"] - families = ["usdComposition"] + families = ["mvUsdComposition"] + scene_type = "usd" + # Order of `fileFormat` must match create_multiverse_usd_comp.py + file_formats = ["usda", "usd"] @property def options(self): @@ -29,6 +46,7 @@ def options(self): "stripNamespaces": bool, "mergeTransformAndShape": bool, "flattenContent": bool, + "writeAsCompoundLayers": bool, "writePendingOverrides": bool, "numTimeSamples": int, "timeSamplesSpan": float @@ -42,6 +60,7 @@ def default_options(self): "stripNamespaces": True, "mergeTransformAndShape": False, "flattenContent": False, + "writeAsCompoundLayers": False, "writePendingOverrides": False, "numTimeSamples": 1, "timeSamplesSpan": 0.0 @@ -71,12 +90,15 @@ def parse_overrides(self, instance, options): return options def process(self, instance): - # Load plugin firstly + # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) - file_name = "{}.usd".format(instance.name) + file_format = instance.data.get("fileFormat", 0) + if file_format in range(len(self.file_formats)): + self.scene_type = self.file_formats[file_format] + file_name = "{0}.{1}".format(instance.name, self.scene_type) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace('\\', '/') @@ -90,12 +112,6 @@ def process(self, instance): with maintained_selection(): members = instance.data("setMembers") - members = cmds.ls(members, - dag=True, - shapes=True, - type="mvUsdCompoundShape", - noIntermediate=True, - long=True) self.log.info('Collected object {}'.format(members)) import multiverse @@ -119,6 +135,18 @@ def process(self, instance): time_opts.framePerSecond = fps comp_write_opts = multiverse.CompositionWriteOptions() + + """ + OP tells MV to write to a staging directory, and then moves the + file to it's final publish directory. By default, MV write relative + paths, but these paths will break when the referencing file moves. + This option forces writes to absolute paths, which is ok within OP + because all published assets have static paths, and MV can only + reference published assets. When a proper UsdAssetResolver is used, + this won't be needed. + """ + comp_write_opts.forceAbsolutePaths = True + options_discard_keys = { 'numTimeSamples', 'timeSamplesSpan', @@ -140,10 +168,10 @@ def process(self, instance): instance.data["representations"] = [] representation = { - 'name': 'usd', - 'ext': 'usd', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': file_name, - "stagingDir": staging_dir + 'stagingDir': staging_dir } instance.data["representations"].append(representation) diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py index ce0e8a392a7..d44e3878b8a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py @@ -7,11 +7,26 @@ class ExtractMultiverseUsdOverride(openpype.api.Extractor): - """Extractor for USD Override by Multiverse.""" + """Extractor for Multiverse USD Override data. + + This will extract settings for a Multiverse Write Override operation: + they are visible in the Maya set node created by a Multiverse USD + Override instance creator. + + The input data contained in the set is: + + - a single Multiverse Compound node with any number of overrides (typically + set in MEOW) + + Upon publish a .usda override file will be written. + """ label = "Extract Multiverse USD Override" hosts = ["maya"] - families = ["usdOverride"] + families = ["mvUsdOverride"] + scene_type = "usd" + # Order of `fileFormat` must match create_multiverse_usd_over.py + file_formats = ["usda", "usd"] @property def options(self): @@ -58,12 +73,15 @@ def default_options(self): } def process(self, instance): - # Load plugin firstly + # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) - file_name = "{}.usda".format(instance.name) + file_format = instance.data.get("fileFormat", 0) + if file_format in range(len(self.file_formats)): + self.scene_type = self.file_formats[file_format] + file_name = "{0}.{1}".format(instance.name, self.scene_type) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace("\\", "/") @@ -78,7 +96,7 @@ def process(self, instance): members = instance.data("setMembers") members = cmds.ls(members, dag=True, - shapes=True, + shapes=False, type="mvUsdCompoundShape", noIntermediate=True, long=True) @@ -128,10 +146,10 @@ def process(self, instance): instance.data["representations"] = [] representation = { - "name": "usd", - "ext": "usd", - "files": file_name, - "stagingDir": staging_dir + 'name': self.scene_type, + 'ext': self.scene_type, + 'files': file_name, + 'stagingDir': staging_dir } instance.data["representations"].append(representation) diff --git a/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py new file mode 100644 index 00000000000..bac2c030c80 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py @@ -0,0 +1,92 @@ +import pyblish.api +import openpype.api +import openpype.hosts.maya.api.action + +import os + +COLOUR_SPACES = ['sRGB', 'linear', 'auto'] +MIPMAP_EXTENSIONS = ['tdl'] + + +class ValidateMvLookContents(pyblish.api.InstancePlugin): + order = openpype.api.ValidateContentsOrder + families = ['mvLook'] + hosts = ['maya'] + label = 'Validate mvLook Data' + actions = [openpype.hosts.maya.api.action.SelectInvalidAction] + + # Allow this validation step to be skipped when you just need to + # get things pushed through. + optional = True + + # These intents get enforced checks, other ones get warnings. + enforced_intents = ['-', 'Final'] + + def process(self, instance): + intent = instance.context.data['intent']['value'] + publishMipMap = instance.data["publishMipMap"] + enforced = True + if intent in self.enforced_intents: + self.log.info("This validation will be enforced: '{}'" + .format(intent)) + else: + enforced = False + self.log.info("This validation will NOT be enforced: '{}'" + .format(intent)) + + if not instance[:]: + raise RuntimeError("Instance is empty") + + invalid = set() + + resources = instance.data.get("resources", []) + for resource in resources: + files = resource["files"] + self.log.debug("Resouce '{}', files: [{}]".format(resource, files)) + node = resource["node"] + if len(files) == 0: + self.log.error("File node '{}' uses no or non-existing " + "files".format(node)) + invalid.add(node) + continue + for fname in files: + if not self.valid_file(fname): + self.log.error("File node '{}'/'{}' is not valid" + .format(node, fname)) + invalid.add(node) + + if publishMipMap and not self.is_or_has_mipmap(fname, files): + msg = "File node '{}'/'{}' does not have a mipmap".format( + node, fname) + if enforced: + invalid.add(node) + self.log.error(msg) + raise RuntimeError(msg) + else: + self.log.warning(msg) + + if invalid: + raise RuntimeError("'{}' has invalid look " + "content".format(instance.name)) + + def valid_file(self, fname): + self.log.debug("Checking validity of '{}'".format(fname)) + if not os.path.exists(fname): + return False + if os.path.getsize(fname) == 0: + return False + return True + + def is_or_has_mipmap(self, fname, files): + ext = os.path.splitext(fname)[1][1:] + if ext in MIPMAP_EXTENSIONS: + self.log.debug("Is a mipmap '{}'".format(fname)) + return True + + for colour_space in COLOUR_SPACES: + for mipmap_ext in MIPMAP_EXTENSIONS: + mipmap_fname = '.'.join([fname, colour_space, mipmap_ext]) + if mipmap_fname in files: + self.log.debug("Has a mipmap '{}'".format(fname)) + return True + return False diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 89df031fb0a..8bdf70b529d 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -41,6 +41,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "rig", "plate", "look", + "mvLook", "yetiRig", "yeticache", "nukenodes", diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f4a8840b3f7..91f61025011 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -109,8 +109,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usd", "staticMesh", "skeletalMesh", - "usdComposition", - "usdOverride", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", "simpleUnrealTexture" ] exclude_families = ["render.farm"] diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 48e10937534..fb1af765d2a 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -601,193 +601,3 @@ about customizing review process refer to [admin section](project_settings/setti If you don't move `modelMain` into `reviewMain`, review will be generated but it will be published as separate entity. - -## Working with Yeti in OpenPype - -OpenPype can work with [Yeti](https://peregrinelabs.com/yeti/) in two data modes. -It can handle Yeti caches and Yeti rigs. - -### Creating and publishing Yeti caches - -Let start by creating simple Yeti setup, just one object and Yeti node. Open new -empty scene in Maya and create sphere. Then select sphere and go **Yeti → Create Yeti Node on Mesh** -Open Yeti node graph **Yeti → Open Graph Editor** and create setup like this: - -![Maya - Yeti Basic Graph](assets/maya-yeti_basic_setup.jpg) - -It doesn't matter what setting you use now, just select proper shape in first -*Import* node. Select your Yeti node and create *Yeti Cache instance* - **OpenPype → Create...** -and select **Yeti Cache**. Leave `Use selection` checked. You should end up with this setup: - -![Maya - Yeti Basic Setup](assets/maya-yeti_basic_setup_outline.jpg) - -You can see there is `yeticacheDefault` set. Instead of *Default* it could be named with -whatever name you've entered in `subset` field during instance creation. - -We are almost ready for publishing cache. You can check basic settings by selecting -Yeti cache set and opening *Extra attributes* in Maya **Attribute Editor**. - -![Maya - Yeti Basic Setup](assets/maya-yeti_cache_attributes.jpg) - -Those attributes there are self-explanatory, but: - -- `Preroll` is number of frames simulation will run before cache frames are stored. -This is useful to "steady" simulation for example. -- `Frame Start` from what frame we start to store cache files -- `Frame End` to what frame we are storing cache files -- `Fps` of cache -- `Samples` how many time samples we take during caching - -You can now publish Yeti cache as any other types. **OpenPype → Publish**. It will -create sequence of `.fur` files and `.fursettings` metadata file with Yeti node -setting. - -### Loading Yeti caches - -You can load Yeti cache by **OpenPype → Load ...**. Select your cache, right+click on -it and select **Load Yeti cache**. This will create Yeti node in scene and set its -cache path to point to your published cache files. Note that this Yeti node will -be named with same name as the one you've used to publish cache. Also notice that -when you open graph on this Yeti node, all nodes are as they were in publishing node. - -### Creating and publishing Yeti Rig - -Yeti Rigs are working in similar way as caches, but are more complex and they deal with -other data used by Yeti, like geometry and textures. - -Let's start by [loading](artist_hosts_maya.md#loading-model) into new scene some model. -I've loaded my Buddha model. - -Create select model mesh, create Yeti node - **Yeti → Create Yeti Node on Mesh** and -setup similar Yeti graph as in cache example above. - -Then select this Yeti node (mine is called with default name `pgYetiMaya1`) and -create *Yeti Rig instance* - **OpenPype → Create...** and select **Yeti Cache**. -Leave `Use selection` checked. - -Last step is to add our model geometry to rig instance, so middle+drag its -geometry to `input_SET` under `yetiRigDefault` set representing rig instance. -Note that its name can differ and is based on your subset name. - -![Maya - Yeti Rig Setup](assets/maya-yeti_rig.jpg) - -Save your scene and ready for publishing our new simple Yeti Rig! - -Go to publish **OpenPype → Publish** and run. This will publish rig with its geometry -as `.ma` scene, save Yeti node settings and export one frame of Yeti cache from -the beginning of your timeline. It will also collect all textures used in Yeti -node, copy them to publish folder `resource` directory and set *Image search path* -of published node to this location. - -:::note Collect Yeti Cache failure -If you encounter **Collect Yeti Cache** failure during collecting phase, and the error is like -```fix -No object matches name: pgYetiMaya1Shape.cbId -``` -then it is probably caused by scene not being saved before publishing. -::: - -### Loading Yeti Rig - -You can load published Yeti Rigs as any other thing in OpenPype - **OpenPype → Load ...**, -select you Yeti rig and right+click on it. In context menu you should see -**Load Yeti Cache** and **Load Yeti Rig** items (among others). First one will -load that one frame cache. The other one will load whole rig. - -Notice that although we put only geometry into `input_SET`, whole hierarchy was -pulled inside also. This allows you to store complex scene element along Yeti -node. - -:::tip auto-connecting rig mesh to existing one -If you select some objects before loading rig it will try to find shapes -under selected hierarchies and match them with shapes loaded with rig (published -under `input_SET`). This mechanism uses *cbId* attribute on those shapes. -If match is found shapes are connected using their `outMesh` and `outMesh`. Thus you can easily connect existing animation to loaded rig. -::: - -## Working with Xgen in OpenPype - -OpenPype support publishing and loading of Xgen interactive grooms. You can publish -them as mayaAscii files with scalps that can be loaded into another maya scene, or as -alembic caches. - -### Publishing Xgen Grooms - -To prepare xgen for publishing just select all the descriptions that should be published together and the create Xgen Subset in the scene using - **OpenPype menu** → **Create**... and select **Xgen Interactive**. Leave Use selection checked. - -For actual publishing of your groom to go **OpenPype → Publish** and then press ▶ to publish. This will export `.ma` file containing your grooms with any geometries they are attached to and also a baked cache in `.abc` format - - -:::tip adding more descriptions -You can add multiple xgen description into the subset you are about to publish, simply by -adding them to the maya set that was created for you. Please make sure that only xgen description nodes are present inside of the set and not the scalp geometry. -::: - -### Loading Xgen - -You can use published xgens by loading them using OpenPype Publisher. You can choose to reference or import xgen. We don't have any automatic mesh linking at the moment and it is expected, that groom is published with a scalp, that can then be manually attached to your animated mesh for example. - -The alembic representation can be loaded too and it contains the groom converted to curves. Keep in mind that the density of the alembic directly depends on your viewport xgen density at the point of export. - - - -## Using Redshift Proxies - -OpenPype supports working with Redshift Proxy files. You can create Redshift Proxy from almost -any hierarchy in Maya and it will be included there. Redshift can export animation -proxy file per frame. - -### Creating Redshift Proxy - -To mark data to publish as Redshift Proxy, select them in Maya and - **OpenPype → Create ...** and -then select **Redshift Proxy**. You can name your subset and hit **Create** button. - -You can enable animation in Attribute Editor: - -![Maya - Yeti Rig Setup](assets/maya-create_rs_proxy.jpg) - -### Publishing Redshift Proxies - -Once data are marked as Redshift Proxy instance, they can be published - **OpenPype → Publish ...** - -### Using Redshift Proxies - -Published proxy files can be loaded with OpenPype Loader. It will create mesh and attach Redshift Proxy -parameters to it - Redshift will then represent proxy with bounding box. - -## Using VRay Proxies - -OpenPype support publishing, loading and using of VRay Proxy in look management. Their underlying format -can be either vrmesh or alembic. - -:::warning vrmesh or alembic and look management -Be aware that **vrmesh** cannot be used with looks as it doesn't retain IDs necessary to map shaders to geometry. -::: - -### Creating VRay Proxy - -To create VRay Proxy, select geometry you want and - **OpenPype → Create ...** select **VRay Proxy**. Name your -subset as you want and press **Create** button. - -This will create `vrayproxy` set for your subset. You can set some options in Attribute editor, mainly if you want -export animation instead of single frame. - -![Maya - VRay Proxy Creation](assets/maya-vray_proxy.jpg) - -### Publishing VRay Proxies - -VRay Proxy can be published - **OpenPype → Publish ...**. It will publish data as VRays `vrmesh` format and as -Alembic file. - -## Using VRay Proxies - -You can load VRay Proxy using loader - **OpenPype → Loader ...** - -![Maya - VRay Proxy Creation](assets/maya-vray_proxy-loader.jpg) - -Select your subset and right-click. Select **Import VRay Proxy (vrmesh)** to import it. - -:::note -Note that even if it states `vrmesh` in descriptions, if loader finds Alembic published along (default behavior) it will -use abc file instead of vrmesh as it is more flexible and without it looks doesn't work. -::: diff --git a/website/docs/artist_hosts_maya_multiverse.md b/website/docs/artist_hosts_maya_multiverse.md new file mode 100644 index 00000000000..e6520bafa06 --- /dev/null +++ b/website/docs/artist_hosts_maya_multiverse.md @@ -0,0 +1,237 @@ +--- +id: artist_hosts_maya_multiverse +title: Multiverse for Maya +sidebar_label: Multiverse USD +--- + +## Working with Multiverse in OpenPype + +OpenPype supports creating, publishing and loading of [Multiverse | USD]( +https://multi-verse.io) data. The minimum Multiverse version supported is v6.7, +and version 7.0 is recommended. + +In a nutshell it is possible to: + +- Create USD Assets, USD compositions, USD Overrides. + + This _creates_ OpenPype instances as Maya set nodes that contain information + for published USD data. + +- Create Multiverse Looks. + + This _creates_ OpenPype instances as Maya set nodes that contain information + for published Maya shading networks data and USD material assignment data. + +- Publish USD Assets, USD compositions and USD Overrides. + + This _writes_ USD files to disk and _publishes_ information to the OpenPype + database. + +- Publish Multiverse Looks. + + This _writes_ a Maya file containing shading networks (to import in Maya), a + USD override file containing material assignment information (to layer in a + Multiverse Compound), it copies original & mip-mapped textures to disk and + _publishes_ information to the OpenPype database. + +- Load any USD data into Multiverse "Compound" shape nodes. + + This _reads_ USD files (and also Alembic files) into Maya by _streaming_ them + to the viewport. + +- Rendering USD data procedurally with 3DelightNSI, Arnold, Redshift, + RenderMan and VRay. + + This reads USD files by _streaming_ them procedurally to the renderer, at + render time. + +USD files written by Multiverse are 100% native USD data, they can be exchanged +with any other DCC applications able to interchange USD. Likewise, Multiverse +can read native USD data created by other applications. The USD extensions are +supported: `.usd` (binary), `.usda` (ASCII), `.usdz`. (zipped, optionally with +textures). Sequences of USD files can also be read via "USD clips". + +It is also possible to load Alembic data (`.abc`) in Multiverse Compounds, +further compose it & override it in other USD files, and render it procedurally. +Alembic data is always converted on the fly (in memory) to USD data. USD clip +from Alembic data are also supported. + + +### Configuration + +To configure Multiverse in OpenPype, an admin privileges needs to setup a new +OpenPype tool in the OpenPype Project Settings, using a similar configuration as +the one depicted here: + +![Maya - Multiverse Setup](assets/maya-multiverse_setup.png) + +For more information about setup of Multiverse please refer to the relative page +on the [Multiverse official documentation](https://multi-verse.io/docs). + + +### Understanding Assets, Compounds, Compositions, Overrides and Layering + +In Multiverse we use some terminology that relates to USD I/O: terms like +"Assets", "Compounds", "Compositions", "Overrides" and "Layering". + +Please hop to the new [Multiverse Introduction]( +https://j-cube.jp/solutions/multiverse/docs/usage/introduction) page on the +official documentation to understand them before reading the next sections. + + +### Creators + +It is possible to create OpenPype "instances" (resulting in Maya set containers) +for publishing Multiverse USD Assets, Compositions, Overrides and Looks. + +When creating OpenPype instances for Multiverse USD Asset, Composition, +Override and Look, the creator plug-in will put the relative selected data in a +Maya set node which holds the properties used by the Multiverse data writer for +publishing. + +You can choose the USD file format in the Creators' set nodes: + +- Assets: `.usd` (default) or `.usda` or `.usdz` +- Compositions: `.usda` (default) or `.usd` +- Overrides: `.usda` (default) or `.usd` +- Looks: `.ma` + +![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_asset_creator.png) + +![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_composition_creator.png) + +![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_override_creator.png) + +![Maya - Multiverse Asset Creator](assets/maya-multiverse_openpype_look_creator.png) + +### Publishers + +The relative publishers for Multiverse USD Asset, Composition, Override and Look +are available. The first three write USD files to disk, while look writes a Maya +file along with the mip-mapped textures. All communicate publish info to the +OpenPype database. + +![Maya - Multiverse Publisher](assets/maya-multiverse_openpype_publishers.png) + + +### Loader + +The loader creates a Multiverse "Compound" shape node reading the USD file of +choice. All data is _streamed_ to the viewport and not contained in Maya. Thanks +to the various viewport draw options the user can strategically decide how to +minimize the cost of viewport draw effectively being able to load any data, this +allows to bring into Maya scenes of virtually unlimited complexity. + +![Maya - Multiverse Loader](assets/maya-multiverse_openpype_loader.png) + +:::tip Note +When using the Loader, Multiverse, by design, never "imports" USD data into the +Maya scene as Maya data. Instead, when desired, Multiverse permits to import +specific USD primitives, or entire hierarchies, into the Maya scene as Maya data +selectively from MEOW, it also tracks what is being imported with a "live +connection" , so upon modification, it is possible to write (create & publish) +the modifies data as a USD file for being layered on top of its relative +Compound. See the [Multiverse Importer]( +https://j-cube.jp/solutions/multiverse/docs/usage/importer)) documentation. +::: + +### Look + +In OpenPype a Multiverse Look is the combination of: + +- a Maya file that contains the shading networks that were assigned to the items + of a Multiverse Compound. +- a Multiverse USD Override file that contains the material assignment + information (which Maya material was assigned to which USD item) +- mip-mapped textures + +Multiverse Look shading networks are typically Maya-referenced in the lighting +and shot scenes. + +Materials are assigned to the USD items in the Compound via the "material +assignment" information that is output in the lookdev stage by a Multiverse +Override. Once published the override can be Layered on the Compound so that +materials will be assigned to items. Finally, an attribute Override on the root +item of the Compound is used to define the `namespace` with which the shading +networks were referenced in Maya. At this point the renderer knows which +material to assign to which item and it is possible to render and edit the +materials as usual. Because the material exists in Maya you can perform IPR and +tune the materials as you please. + +The Multiverse Look will also publish textures in optimized mip-map format, +currently supporting the `.tdl` (Texture Delight) mip map format of the 3Delight +NSI renderer. MipMaps are required when the relative option is checked and you +are publishing Multiverse Looks with the `final` or `-` subset, while they are +not required with the `WIP` or `test` subsets. MipMaps are found automatically +as long as they exist alongside the original textures. Their generation can be +automatic when using 3Delight for Maya or can be manual by using the `tdlmake` +binary utility. + + +### About embedding shading networks in USD + +Alternatively, but also complementary to the Multiverse Look, as of Multiverse +7 it is also possible to write shading networks _inside_ USD files: that is +achieved by using either the Asset writer (if material are defined in the +modeling stage) and the Override writer (if materials are defined in the lookdev +or later stage). + +Some interesting consequences of USD shading networks in Multiverse: + +1. they can be overridden by a shading network in Maya by assigning in MEOW a + Maya material as an override +2. they are available for assignment in MEOW, so you can assign a USD material + to an item as an override +3. From Hypershade you can use the Multiverse USD shading network write File> + Export option to write USD shading network libraries to then layer on an asset + and perform 2. again. + +Note that: + +- Shading networks in USD can then be currently rendered with + 3DelightNSI +- Shading networks in USD can be used for interchange with DCC apps. Multiverse + shading networks are written natively with the USD Shade schema. +- usdPreviewSurface shading networks are too considered embedded shading + networks, though they are classified separately from non-preview / final + quality shading networks +- USDZ files use usdPreviewSurface shading networks, and therefore can be, too, + rendered (with 3DelightNSI) +- in case both usdPreviewSurface and final quality shading networks, the latter + will be used for rendering (while the former can be previewed in the viewport) +- it is possible to disable rendering of any embedded shading network via the + relative option in the Compound Attribute Editor. + + +### Rendering + +Multiverse offers procedural rendering with all the major production renderers: + +- 3DelightNSI +- Arnold +- Redshift +- RenderMan +- VRay + +Procedural rendering effectively means that data is _streamed_ to the renderer +at render-time, without the need to store the data in the Maya scene (this +effectively means small .ma/.mb files that load fast) nor in the renderer native +file format scene description file (this effectively means tiny `.nsi` / `.ass` +/ `.vrscene` / `.rib` files that load fast). + +This is completely transparent to the user: Multiverse Compound nodes present in +the scene, once a render is launched, will stream data to the renderer in a +procedural fashion. + + +### Example Multiverse Pipeline and API + +An example diagram of the data flow in a Maya pipeline using Multiverse is +available, see the [Multiverse Pipeline]( +https://j-cube.jp/solutions/multiverse/docs/pipeline) documentation. + + +A very easy to use Python API to automate any task is available, the API is +user friendly and does not require any knowledge of the vast and complex USD +APIs. See the [Multiverse Python API]( +https://j-cube.jp/solutions/multiverse/docs/dev/python-api.html) documentation. diff --git a/website/docs/artist_hosts_maya_redshift.md b/website/docs/artist_hosts_maya_redshift.md new file mode 100644 index 00000000000..268c49d75ca --- /dev/null +++ b/website/docs/artist_hosts_maya_redshift.md @@ -0,0 +1,31 @@ +--- +id: artist_hosts_maya_redshift +title: Redshift for Maya +sidebar_label: Redshift +--- + +## Working with Redshift in OpenPype + +### Using Redshift Proxies + +OpenPype supports working with Redshift Proxy files. You can create Redshift Proxy from almost +any hierarchy in Maya and it will be included there. Redshift can export animation +proxy file per frame. + +### Creating Redshift Proxy + +To mark data to publish as Redshift Proxy, select them in Maya and - **OpenPype → Create ...** and +then select **Redshift Proxy**. You can name your subset and hit **Create** button. + +You can enable animation in Attribute Editor: + +![Maya - Yeti Rig Setup](assets/maya-create_rs_proxy.jpg) + +### Publishing Redshift Proxies + +Once data are marked as Redshift Proxy instance, they can be published - **OpenPype → Publish ...** + +### Using Redshift Proxies + +Published proxy files can be loaded with OpenPype Loader. It will create mesh and attach Redshift Proxy +parameters to it - Redshift will then represent proxy with bounding box. diff --git a/website/docs/artist_hosts_maya_vray.md b/website/docs/artist_hosts_maya_vray.md new file mode 100644 index 00000000000..a19fce9735e --- /dev/null +++ b/website/docs/artist_hosts_maya_vray.md @@ -0,0 +1,44 @@ +--- +id: artist_hosts_maya_vray +title: VRay for Maya +sidebar_label: VRay +--- + +## Working with VRay in OpenPype + +### #Using VRay Proxies + +OpenPype support publishing, loading and using of VRay Proxy in look management. Their underlying format +can be either vrmesh or alembic. + +:::warning vrmesh or alembic and look management +Be aware that **vrmesh** cannot be used with looks as it doesn't retain IDs necessary to map shaders to geometry. +::: + +### Creating VRay Proxy + +To create VRay Proxy, select geometry you want and - **OpenPype → Create ...** select **VRay Proxy**. Name your +subset as you want and press **Create** button. + +This will create `vrayproxy` set for your subset. You can set some options in Attribute editor, mainly if you want +export animation instead of single frame. + +![Maya - VRay Proxy Creation](assets/maya-vray_proxy.jpg) + +### Publishing VRay Proxies + +VRay Proxy can be published - **OpenPype → Publish ...**. It will publish data as VRays `vrmesh` format and as +Alembic file. + +## Using VRay Proxies + +You can load VRay Proxy using loader - **OpenPype → Loader ...** + +![Maya - VRay Proxy Creation](assets/maya-vray_proxy-loader.jpg) + +Select your subset and right-click. Select **Import VRay Proxy (vrmesh)** to import it. + +:::note +Note that even if it states `vrmesh` in descriptions, if loader finds Alembic published along (default behavior) it will +use abc file instead of vrmesh as it is more flexible and without it looks doesn't work. +::: diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md new file mode 100644 index 00000000000..8b0174a29f3 --- /dev/null +++ b/website/docs/artist_hosts_maya_xgen.md @@ -0,0 +1,29 @@ +--- +id: artist_hosts_maya_xgen +title: Xgen for Maya +sidebar_label: Xgen +--- + +## Working with Xgen in OpenPype + +OpenPype support publishing and loading of Xgen interactive grooms. You can publish +them as mayaAscii files with scalps that can be loaded into another maya scene, or as +alembic caches. + +### Publishing Xgen Grooms + +To prepare xgen for publishing just select all the descriptions that should be published together and the create Xgen Subset in the scene using - **OpenPype menu** → **Create**... and select **Xgen Interactive**. Leave Use selection checked. + +For actual publishing of your groom to go **OpenPype → Publish** and then press ▶ to publish. This will export `.ma` file containing your grooms with any geometries they are attached to and also a baked cache in `.abc` format + + +:::tip adding more descriptions +You can add multiple xgen description into the subset you are about to publish, simply by +adding them to the maya set that was created for you. Please make sure that only xgen description nodes are present inside of the set and not the scalp geometry. +::: + +### Loading Xgen + +You can use published xgens by loading them using OpenPype Publisher. You can choose to reference or import xgen. We don't have any automatic mesh linking at the moment and it is expected, that groom is published with a scalp, that can then be manually attached to your animated mesh for example. + +The alembic representation can be loaded too and it contains the groom converted to curves. Keep in mind that the density of the alembic directly depends on your viewport xgen density at the point of export. diff --git a/website/docs/artist_hosts_maya_yeti.md b/website/docs/artist_hosts_maya_yeti.md new file mode 100644 index 00000000000..f5a6a4d2c95 --- /dev/null +++ b/website/docs/artist_hosts_maya_yeti.md @@ -0,0 +1,108 @@ +--- +id: artist_hosts_maya_yeti +title: Yeti for Maya +sidebar_label: Yeti +--- + +## Working with Yeti in OpenPype + +OpenPype can work with [Yeti](https://peregrinelabs.com/yeti/) in two data modes. +It can handle Yeti caches and Yeti rigs. + +### Creating and publishing Yeti caches + +Let start by creating simple Yeti setup, just one object and Yeti node. Open new +empty scene in Maya and create sphere. Then select sphere and go **Yeti → Create Yeti Node on Mesh** +Open Yeti node graph **Yeti → Open Graph Editor** and create setup like this: + +![Maya - Yeti Basic Graph](assets/maya-yeti_basic_setup.jpg) + +It doesn't matter what setting you use now, just select proper shape in first +*Import* node. Select your Yeti node and create *Yeti Cache instance* - **OpenPype → Create...** +and select **Yeti Cache**. Leave `Use selection` checked. You should end up with this setup: + +![Maya - Yeti Basic Setup](assets/maya-yeti_basic_setup_outline.jpg) + +You can see there is `yeticacheDefault` set. Instead of *Default* it could be named with +whatever name you've entered in `subset` field during instance creation. + +We are almost ready for publishing cache. You can check basic settings by selecting +Yeti cache set and opening *Extra attributes* in Maya **Attribute Editor**. + +![Maya - Yeti Basic Setup](assets/maya-yeti_cache_attributes.jpg) + +Those attributes there are self-explanatory, but: + +- `Preroll` is number of frames simulation will run before cache frames are stored. +This is useful to "steady" simulation for example. +- `Frame Start` from what frame we start to store cache files +- `Frame End` to what frame we are storing cache files +- `Fps` of cache +- `Samples` how many time samples we take during caching + +You can now publish Yeti cache as any other types. **OpenPype → Publish**. It will +create sequence of `.fur` files and `.fursettings` metadata file with Yeti node +setting. + +### Loading Yeti caches + +You can load Yeti cache by **OpenPype → Load ...**. Select your cache, right+click on +it and select **Load Yeti cache**. This will create Yeti node in scene and set its +cache path to point to your published cache files. Note that this Yeti node will +be named with same name as the one you've used to publish cache. Also notice that +when you open graph on this Yeti node, all nodes are as they were in publishing node. + +### Creating and publishing Yeti Rig + +Yeti Rigs are working in similar way as caches, but are more complex and they deal with +other data used by Yeti, like geometry and textures. + +Let's start by [loading](artist_hosts_maya.md#loading-model) into new scene some model. +I've loaded my Buddha model. + +Create select model mesh, create Yeti node - **Yeti → Create Yeti Node on Mesh** and +setup similar Yeti graph as in cache example above. + +Then select this Yeti node (mine is called with default name `pgYetiMaya1`) and +create *Yeti Rig instance* - **OpenPype → Create...** and select **Yeti Cache**. +Leave `Use selection` checked. + +Last step is to add our model geometry to rig instance, so middle+drag its +geometry to `input_SET` under `yetiRigDefault` set representing rig instance. +Note that its name can differ and is based on your subset name. + +![Maya - Yeti Rig Setup](assets/maya-yeti_rig.jpg) + +Save your scene and ready for publishing our new simple Yeti Rig! + +Go to publish **OpenPype → Publish** and run. This will publish rig with its geometry +as `.ma` scene, save Yeti node settings and export one frame of Yeti cache from +the beginning of your timeline. It will also collect all textures used in Yeti +node, copy them to publish folder `resource` directory and set *Image search path* +of published node to this location. + +:::note Collect Yeti Cache failure +If you encounter **Collect Yeti Cache** failure during collecting phase, and the error is like +```fix +No object matches name: pgYetiMaya1Shape.cbId +``` +then it is probably caused by scene not being saved before publishing. +::: + +### Loading Yeti Rig + +You can load published Yeti Rigs as any other thing in OpenPype - **OpenPype → Load ...**, +select you Yeti rig and right+click on it. In context menu you should see +**Load Yeti Cache** and **Load Yeti Rig** items (among others). First one will +load that one frame cache. The other one will load whole rig. + +Notice that although we put only geometry into `input_SET`, whole hierarchy was +pulled inside also. This allows you to store complex scene element along Yeti +node. + +:::tip auto-connecting rig mesh to existing one +If you select some objects before loading rig it will try to find shapes +under selected hierarchies and match them with shapes loaded with rig (published +under `input_SET`). This mechanism uses *cbId* attribute on those shapes. +If match is found shapes are connected using their `outMesh` and `outMesh`. Thus you can easily connect existing animation to loaded rig. +::: diff --git a/website/docs/assets/maya-multiverse_openpype_asset_creator.png b/website/docs/assets/maya-multiverse_openpype_asset_creator.png new file mode 100644 index 00000000000..0426e9f8230 Binary files /dev/null and b/website/docs/assets/maya-multiverse_openpype_asset_creator.png differ diff --git a/website/docs/assets/maya-multiverse_openpype_composition_creator.png b/website/docs/assets/maya-multiverse_openpype_composition_creator.png new file mode 100644 index 00000000000..eb0e5e33487 Binary files /dev/null and b/website/docs/assets/maya-multiverse_openpype_composition_creator.png differ diff --git a/website/docs/assets/maya-multiverse_openpype_loader.png b/website/docs/assets/maya-multiverse_openpype_loader.png new file mode 100644 index 00000000000..0579e0dcde1 Binary files /dev/null and b/website/docs/assets/maya-multiverse_openpype_loader.png differ diff --git a/website/docs/assets/maya-multiverse_openpype_look_creator.png b/website/docs/assets/maya-multiverse_openpype_look_creator.png new file mode 100644 index 00000000000..fd27d5fd1af Binary files /dev/null and b/website/docs/assets/maya-multiverse_openpype_look_creator.png differ diff --git a/website/docs/assets/maya-multiverse_openpype_override_creator.png b/website/docs/assets/maya-multiverse_openpype_override_creator.png new file mode 100644 index 00000000000..d7b1299ba65 Binary files /dev/null and b/website/docs/assets/maya-multiverse_openpype_override_creator.png differ diff --git a/website/docs/assets/maya-multiverse_openpype_publishers.png b/website/docs/assets/maya-multiverse_openpype_publishers.png new file mode 100644 index 00000000000..bee6c79fe98 Binary files /dev/null and b/website/docs/assets/maya-multiverse_openpype_publishers.png differ diff --git a/website/docs/assets/maya-multiverse_setup.png b/website/docs/assets/maya-multiverse_setup.png new file mode 100644 index 00000000000..8aa89ef7e50 Binary files /dev/null and b/website/docs/assets/maya-multiverse_setup.png differ diff --git a/website/sidebars.js b/website/sidebars.js index ee816dd6785..d4fec9fba2c 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -19,7 +19,18 @@ module.exports = { items: [ "artist_hosts_hiero", "artist_hosts_nuke_tut", - "artist_hosts_maya", + { + type: "category", + label: "Maya", + items: [ + "artist_hosts_maya", + "artist_hosts_maya_multiverse", + "artist_hosts_maya_yeti", + "artist_hosts_maya_xgen", + "artist_hosts_maya_vray", + "artist_hosts_maya_redshift", + ], + }, "artist_hosts_blender", "artist_hosts_harmony", "artist_hosts_houdini", diff --git a/website/src/pages/index.js b/website/src/pages/index.js index f57fd1002a8..115102ed040 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -68,6 +68,10 @@ const collab = [ title: 'Ellipse Studio', image: '/img/ellipse-studio.png', infoLink: 'http://www.dargaudmedia.com' + }, { + title: 'J Cube Inc', + image: '/img/jcube_logo_bw.png', + infoLink: 'https://j-cube.jp' } ]; @@ -229,7 +233,7 @@ function Home() { Get Support - +

OpenPYPE is developed, maintained and supported by PYPE.club

@@ -290,7 +294,7 @@ function Home() { - +