diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py
new file mode 100644
index 0000000000..260241f5fc
--- /dev/null
+++ b/openpype/hosts/maya/api/fbx.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+"""Tools to work with FBX."""
+import logging
+
+from pyblish.api import Instance
+
+from maya import cmds # noqa
+import maya.mel as mel # noqa
+
+
+class FBXExtractor:
+ """Extract FBX from Maya.
+
+ This extracts reproducible FBX exports ignoring any of the settings set
+ on the local machine in the FBX export options window.
+
+ All export settings are applied with the `FBXExport*` commands prior
+ to the `FBXExport` call itself. The options can be overridden with
+ their
+ nice names as seen in the "options" property on this class.
+
+ For more information on FBX exports see:
+ - https://knowledge.autodesk.com/support/maya/learn-explore/caas
+ /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4
+ -9CB19C28F4E0-htm.html
+ - http://forums.cgsociety.org/archive/index.php?t-1032853.html
+ - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE
+ /LKs9hakE28kJ
+
+ """
+ @property
+ def options(self):
+ """Overridable options for FBX 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 {
+ "cameras": bool,
+ "smoothingGroups": bool,
+ "hardEdges": bool,
+ "tangents": bool,
+ "smoothMesh": bool,
+ "instances": bool,
+ # "referencedContainersContent": bool, # deprecated in Maya 2016+
+ "bakeComplexAnimation": int,
+ "bakeComplexStart": int,
+ "bakeComplexEnd": int,
+ "bakeComplexStep": int,
+ "bakeResampleAnimation": bool,
+ "animationOnly": bool,
+ "useSceneName": bool,
+ "quaternion": str, # "euler"
+ "shapes": bool,
+ "skins": bool,
+ "constraints": bool,
+ "lights": bool,
+ "embeddedTextures": bool,
+ "inputConnections": bool,
+ "upAxis": str, # x, y or z,
+ "triangulate": bool
+ }
+
+ @property
+ def default_options(self):
+ """The default options for FBX extraction.
+
+ This includes shapes, skins, constraints, lights and incoming
+ connections and exports with the Y-axis as up-axis.
+
+ By default this uses the time sliders start and end time.
+
+ """
+
+ start_frame = int(cmds.playbackOptions(query=True,
+ animationStartTime=True))
+ end_frame = int(cmds.playbackOptions(query=True,
+ animationEndTime=True))
+
+ return {
+ "cameras": False,
+ "smoothingGroups": True,
+ "hardEdges": False,
+ "tangents": False,
+ "smoothMesh": True,
+ "instances": False,
+ "bakeComplexAnimation": True,
+ "bakeComplexStart": start_frame,
+ "bakeComplexEnd": end_frame,
+ "bakeComplexStep": 1,
+ "bakeResampleAnimation": True,
+ "animationOnly": False,
+ "useSceneName": False,
+ "quaternion": "euler",
+ "shapes": True,
+ "skins": True,
+ "constraints": False,
+ "lights": True,
+ "embeddedTextures": False,
+ "inputConnections": True,
+ "upAxis": "y",
+ "triangulate": False
+ }
+
+ def __init__(self, log=None):
+ # Ensure FBX plug-in is loaded
+ self.log = log or logging.getLogger(self.__class__.__name__)
+ cmds.loadPlugin("fbxmaya", quiet=True)
+
+ def parse_overrides(self, instance, options):
+ """Inspect data of instance to determine overridden options
+
+ An instance may supply any of the overridable options
+ as data, the option is then added to the extraction.
+
+ """
+
+ for key in instance.data:
+ if key not in self.options:
+ continue
+
+ # Ensure the data is of correct type
+ value = instance.data[key]
+ 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 set_options_from_instance(self, instance):
+ # type: (Instance) -> None
+ """Sets FBX export options from data in the instance.
+
+ Args:
+ instance (Instance): Instance data.
+
+ """
+ # Parse export options
+ options = self.default_options
+ options = self.parse_overrides(instance, options)
+ self.log.info("Export options: {0}".format(options))
+
+ # Collect the start and end including handles
+ start = instance.data.get("frameStartHandle") or \
+ instance.context.data.get("frameStartHandle")
+ end = instance.data.get("frameEndHandle") or \
+ instance.context.data.get("frameEndHandle")
+
+ options['bakeComplexStart'] = start
+ options['bakeComplexEnd'] = end
+
+ # First apply the default export settings to be fully consistent
+ # each time for successive publishes
+ mel.eval("FBXResetExport")
+
+ # Apply the FBX overrides through MEL since the commands
+ # only work correctly in MEL according to online
+ # available discussions on the topic
+ _iteritems = getattr(options, "iteritems", options.items)
+ for option, value in _iteritems():
+ key = option[0].upper() + option[1:] # uppercase first letter
+
+ # Boolean must be passed as lower-case strings
+ # as to MEL standards
+ if isinstance(value, bool):
+ value = str(value).lower()
+
+ template = "FBXExport{0} {1}" if key == "UpAxis" else \
+ "FBXExport{0} -v {1}" # noqa
+ cmd = template.format(key, value)
+ self.log.info(cmd)
+ mel.eval(cmd)
+
+ # Never show the UI or generate a log
+ mel.eval("FBXExportShowUI -v false")
+ mel.eval("FBXExportGenerateLog -v false")
+
+ @staticmethod
+ def export(members, path):
+ # type: (list, str) -> None
+ """Export members as FBX with given path.
+
+ Args:
+ members (list): List of members to export.
+ path (str): Path to use for export.
+
+ """
+ cmds.select(members, r=True, noExpand=True)
+ mel.eval('FBXExport -f "{}" -s'.format(path))
diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py
index 92fc5133a9..90688423e0 100644
--- a/openpype/hosts/maya/api/lib.py
+++ b/openpype/hosts/maya/api/lib.py
@@ -3138,11 +3138,20 @@ def set_colorspace():
@contextlib.contextmanager
-def root_parent(nodes):
- # type: (list) -> list
+def parent_nodes(nodes, parent=None):
+ # type: (list, str) -> list
"""Context manager to un-parent provided nodes and return them back."""
import pymel.core as pm # noqa
+ parent_node = None
+ delete_parent = False
+
+ if parent:
+ if not cmds.objExists(parent):
+ parent_node = pm.createNode("transform", n=parent, ss=False)
+ delete_parent = True
+ else:
+ parent_node = pm.PyNode(parent)
node_parents = []
for node in nodes:
n = pm.PyNode(node)
@@ -3153,9 +3162,14 @@ def root_parent(nodes):
node_parents.append((n, root))
try:
for node in node_parents:
- node[0].setParent(world=True)
+ if not parent:
+ node[0].setParent(world=True)
+ else:
+ node[0].setParent(parent_node)
yield
finally:
for node in node_parents:
if node[1]:
node[0].setParent(node[1])
+ if delete_parent:
+ pm.delete(parent_node)
diff --git a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py
new file mode 100644
index 0000000000..a6deeeee2e
--- /dev/null
+++ b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+"""Creator for Unreal Skeletal Meshes."""
+from openpype.hosts.maya.api import plugin, lib
+from avalon.api import Session
+from maya import cmds # noqa
+
+
+class CreateUnrealSkeletalMesh(plugin.Creator):
+ """Unreal Static Meshes with collisions."""
+ name = "staticMeshMain"
+ label = "Unreal - Skeletal Mesh"
+ family = "skeletalMesh"
+ icon = "thumbs-up"
+ dynamic_subset_keys = ["asset"]
+
+ joint_hints = []
+
+ def __init__(self, *args, **kwargs):
+ """Constructor."""
+ super(CreateUnrealSkeletalMesh, self).__init__(*args, **kwargs)
+
+ @classmethod
+ def get_dynamic_data(
+ cls, variant, task_name, asset_id, project_name, host_name
+ ):
+ dynamic_data = super(CreateUnrealSkeletalMesh, cls).get_dynamic_data(
+ variant, task_name, asset_id, project_name, host_name
+ )
+ dynamic_data["asset"] = Session.get("AVALON_ASSET")
+ return dynamic_data
+
+ def process(self):
+ self.name = "{}_{}".format(self.family, self.name)
+ with lib.undo_chunk():
+ instance = super(CreateUnrealSkeletalMesh, self).process()
+ content = cmds.sets(instance, query=True)
+
+ # empty set and process its former content
+ cmds.sets(content, rm=instance)
+ geometry_set = cmds.sets(name="geometry_SET", empty=True)
+ joints_set = cmds.sets(name="joints_SET", empty=True)
+
+ cmds.sets([geometry_set, joints_set], forceElement=instance)
+ members = cmds.ls(content) or []
+
+ for node in members:
+ if node in self.joint_hints:
+ cmds.sets(node, forceElement=joints_set)
+ else:
+ cmds.sets(node, forceElement=geometry_set)
diff --git a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py
index 9ad560ab7c..f62d15fe62 100644
--- a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py
+++ b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py
@@ -10,7 +10,7 @@ class CreateUnrealStaticMesh(plugin.Creator):
"""Unreal Static Meshes with collisions."""
name = "staticMeshMain"
label = "Unreal - Static Mesh"
- family = "unrealStaticMesh"
+ family = "staticMesh"
icon = "cube"
dynamic_subset_keys = ["asset"]
@@ -28,10 +28,10 @@ class CreateUnrealStaticMesh(plugin.Creator):
variant, task_name, asset_id, project_name, host_name
)
dynamic_data["asset"] = Session.get("AVALON_ASSET")
-
return dynamic_data
def process(self):
+ self.name = "{}_{}".format(self.family, self.name)
with lib.undo_chunk():
instance = super(CreateUnrealStaticMesh, self).process()
content = cmds.sets(instance, query=True)
diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py
index 04a25f6493..a7222edfd4 100644
--- a/openpype/hosts/maya/plugins/load/load_reference.py
+++ b/openpype/hosts/maya/plugins/load/load_reference.py
@@ -22,7 +22,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"camera",
"rig",
"camerarig",
- "xgen"]
+ "xgen",
+ "staticMesh"]
representations = ["ma", "abc", "fbx", "mb"]
label = "Reference"
diff --git a/openpype/hosts/maya/plugins/publish/clean_nodes.py b/openpype/hosts/maya/plugins/publish/clean_nodes.py
deleted file mode 100644
index 03995cdabe..0000000000
--- a/openpype/hosts/maya/plugins/publish/clean_nodes.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Cleanup leftover nodes."""
-from maya import cmds # noqa
-import pyblish.api
-
-
-class CleanNodesUp(pyblish.api.InstancePlugin):
- """Cleans up the staging directory after a successful publish.
-
- This will also clean published renders and delete their parent directories.
-
- """
-
- order = pyblish.api.IntegratorOrder + 10
- label = "Clean Nodes"
- optional = True
- active = True
-
- def process(self, instance):
- if not instance.data.get("cleanNodes"):
- self.log.info("Nothing to clean.")
- return
-
- nodes_to_clean = instance.data.pop("cleanNodes", [])
- self.log.info("Removing {} nodes".format(len(nodes_to_clean)))
- for node in nodes_to_clean:
- try:
- cmds.delete(node)
- except ValueError:
- # object might be already deleted, don't complain about it
- pass
diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py
new file mode 100644
index 0000000000..79693bb35e
--- /dev/null
+++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+from maya import cmds # noqa
+import pyblish.api
+
+
+class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin):
+ """Collect Unreal Skeletal Mesh."""
+
+ order = pyblish.api.CollectorOrder + 0.2
+ label = "Collect Unreal Skeletal Meshes"
+ families = ["skeletalMesh"]
+
+ def process(self, instance):
+ frame = cmds.currentTime(query=True)
+ instance.data["frameStart"] = frame
+ instance.data["frameEnd"] = frame
+
+ geo_sets = [
+ i for i in instance[:]
+ if i.lower().startswith("geometry_set")
+ ]
+
+ joint_sets = [
+ i for i in instance[:]
+ if i.lower().startswith("joints_set")
+ ]
+
+ instance.data["geometry"] = []
+ instance.data["joints"] = []
+
+ for geo_set in geo_sets:
+ geo_content = cmds.ls(cmds.sets(geo_set, query=True), long=True)
+ if geo_content:
+ instance.data["geometry"] += geo_content
+
+ for join_set in joint_sets:
+ join_content = cmds.ls(cmds.sets(join_set, query=True), long=True)
+ if join_content:
+ instance.data["joints"] += join_content
diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py
index b1fb0542f2..79d0856fa0 100644
--- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py
+++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py
@@ -1,38 +1,36 @@
# -*- coding: utf-8 -*-
-from maya import cmds
+from maya import cmds # noqa
import pyblish.api
+from pprint import pformat
class CollectUnrealStaticMesh(pyblish.api.InstancePlugin):
- """Collect Unreal Static Mesh
-
- Ensures always only a single frame is extracted (current frame). This
- also sets correct FBX options for later extraction.
-
- """
+ """Collect Unreal Static Mesh."""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Unreal Static Meshes"
- families = ["unrealStaticMesh"]
+ families = ["staticMesh"]
def process(self, instance):
- # add fbx family to trigger fbx extractor
- instance.data["families"].append("fbx")
- # take the name from instance (without the `S_` prefix)
- instance.data["staticMeshCombinedName"] = instance.name[2:]
-
- geometry_set = [i for i in instance if i == "geometry_SET"]
- instance.data["membersToCombine"] = cmds.sets(
+ geometry_set = [
+ i for i in instance
+ if i.startswith("geometry_SET")
+ ]
+ instance.data["geometryMembers"] = cmds.sets(
geometry_set, query=True)
- collision_set = [i for i in instance if i == "collisions_SET"]
+ self.log.info("geometry: {}".format(
+ pformat(instance.data.get("geometryMembers"))))
+
+ collision_set = [
+ i for i in instance
+ if i.startswith("collisions_SET")
+ ]
instance.data["collisionMembers"] = cmds.sets(
collision_set, query=True)
- # set fbx overrides on instance
- instance.data["smoothingGroups"] = True
- instance.data["smoothMesh"] = True
- instance.data["triangulate"] = True
+ self.log.info("collisions: {}".format(
+ pformat(instance.data.get("collisionMembers"))))
frame = cmds.currentTime(query=True)
instance.data["frameStart"] = frame
diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx.py b/openpype/hosts/maya/plugins/publish/extract_fbx.py
index a2adcb3091..fbbe8e06b0 100644
--- a/openpype/hosts/maya/plugins/publish/extract_fbx.py
+++ b/openpype/hosts/maya/plugins/publish/extract_fbx.py
@@ -5,152 +5,29 @@ from maya import cmds # noqa
import maya.mel as mel # noqa
import pyblish.api
import openpype.api
-from openpype.hosts.maya.api.lib import (
- root_parent,
- maintained_selection
-)
+from openpype.hosts.maya.api.lib import maintained_selection
+
+from openpype.hosts.maya.api import fbx
class ExtractFBX(openpype.api.Extractor):
"""Extract FBX from Maya.
- This extracts reproducible FBX exports ignoring any of the settings set
- on the local machine in the FBX export options window.
-
- All export settings are applied with the `FBXExport*` commands prior
- to the `FBXExport` call itself. The options can be overridden with their
- nice names as seen in the "options" property on this class.
-
- For more information on FBX exports see:
- - https://knowledge.autodesk.com/support/maya/learn-explore/caas
- /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4
- -9CB19C28F4E0-htm.html
- - http://forums.cgsociety.org/archive/index.php?t-1032853.html
- - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE
- /LKs9hakE28kJ
+ This extracts reproducible FBX exports ignoring any of the
+ settings set on the local machine in the FBX export options window.
"""
-
order = pyblish.api.ExtractorOrder
label = "Extract FBX"
families = ["fbx"]
- @property
- def options(self):
- """Overridable options for FBX 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 {
- "cameras": bool,
- "smoothingGroups": bool,
- "hardEdges": bool,
- "tangents": bool,
- "smoothMesh": bool,
- "instances": bool,
- # "referencedContainersContent": bool, # deprecated in Maya 2016+
- "bakeComplexAnimation": int,
- "bakeComplexStart": int,
- "bakeComplexEnd": int,
- "bakeComplexStep": int,
- "bakeResampleAnimation": bool,
- "animationOnly": bool,
- "useSceneName": bool,
- "quaternion": str, # "euler"
- "shapes": bool,
- "skins": bool,
- "constraints": bool,
- "lights": bool,
- "embeddedTextures": bool,
- "inputConnections": bool,
- "upAxis": str, # x, y or z,
- "triangulate": bool
- }
-
- @property
- def default_options(self):
- """The default options for FBX extraction.
-
- This includes shapes, skins, constraints, lights and incoming
- connections and exports with the Y-axis as up-axis.
-
- By default this uses the time sliders start and end time.
-
- """
-
- start_frame = int(cmds.playbackOptions(query=True,
- animationStartTime=True))
- end_frame = int(cmds.playbackOptions(query=True,
- animationEndTime=True))
-
- return {
- "cameras": False,
- "smoothingGroups": False,
- "hardEdges": False,
- "tangents": False,
- "smoothMesh": False,
- "instances": False,
- "bakeComplexAnimation": True,
- "bakeComplexStart": start_frame,
- "bakeComplexEnd": end_frame,
- "bakeComplexStep": 1,
- "bakeResampleAnimation": True,
- "animationOnly": False,
- "useSceneName": False,
- "quaternion": "euler",
- "shapes": True,
- "skins": True,
- "constraints": False,
- "lights": True,
- "embeddedTextures": True,
- "inputConnections": True,
- "upAxis": "y",
- "triangulate": False
- }
-
- def parse_overrides(self, instance, options):
- """Inspect data of instance to determine overridden options
-
- An instance may supply any of the overridable options
- as data, the option is then added to the extraction.
-
- """
-
- for key in instance.data:
- if key not in self.options:
- continue
-
- # Ensure the data is of correct type
- value = instance.data[key]
- 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 process(self, instance):
-
- # Ensure FBX plug-in is loaded
- cmds.loadPlugin("fbxmaya", quiet=True)
+ fbx_exporter = fbx.FBXExtractor(log=self.log)
# Define output path
- stagingDir = self.staging_dir(instance)
+ staging_dir = self.staging_dir(instance)
filename = "{0}.fbx".format(instance.name)
- path = os.path.join(stagingDir, filename)
+ path = os.path.join(staging_dir, filename)
# The export requires forward slashes because we need
# to format it into a string in a mel expression
@@ -162,54 +39,13 @@ class ExtractFBX(openpype.api.Extractor):
self.log.info("Members: {0}".format(members))
self.log.info("Instance: {0}".format(instance[:]))
- # Parse export options
- options = self.default_options
- options = self.parse_overrides(instance, options)
- self.log.info("Export options: {0}".format(options))
-
- # Collect the start and end including handles
- start = instance.data["frameStartHandle"]
- end = instance.data["frameEndHandle"]
-
- options['bakeComplexStart'] = start
- options['bakeComplexEnd'] = end
-
- # First apply the default export settings to be fully consistent
- # each time for successive publishes
- mel.eval("FBXResetExport")
-
- # Apply the FBX overrides through MEL since the commands
- # only work correctly in MEL according to online
- # available discussions on the topic
- _iteritems = getattr(options, "iteritems", options.items)
- for option, value in _iteritems():
- key = option[0].upper() + option[1:] # uppercase first letter
-
- # Boolean must be passed as lower-case strings
- # as to MEL standards
- if isinstance(value, bool):
- value = str(value).lower()
-
- template = "FBXExport{0} {1}" if key == "UpAxis" else "FBXExport{0} -v {1}" # noqa
- cmd = template.format(key, value)
- self.log.info(cmd)
- mel.eval(cmd)
-
- # Never show the UI or generate a log
- mel.eval("FBXExportShowUI -v false")
- mel.eval("FBXExportGenerateLog -v false")
+ fbx_exporter.set_options_from_instance(instance)
# Export
- if "unrealStaticMesh" in instance.data["families"]:
- with maintained_selection():
- with root_parent(members):
- self.log.info("Un-parenting: {}".format(members))
- cmds.select(members, r=1, noExpand=True)
- mel.eval('FBXExport -f "{}" -s'.format(path))
- else:
- with maintained_selection():
- cmds.select(members, r=1, noExpand=True)
- mel.eval('FBXExport -f "{}" -s'.format(path))
+ with maintained_selection():
+ fbx_exporter.export(members, path)
+ cmds.select(members, r=1, noExpand=True)
+ mel.eval('FBXExport -f "{}" -s'.format(path))
if "representations" not in instance.data:
instance.data["representations"] = []
@@ -218,7 +54,7 @@ class ExtractFBX(openpype.api.Extractor):
'name': 'fbx',
'ext': 'fbx',
'files': filename,
- "stagingDir": stagingDir,
+ "stagingDir": staging_dir,
}
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py
new file mode 100644
index 0000000000..7ef7f2f181
--- /dev/null
+++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+"""Create Unreal Skeletal Mesh data to be extracted as FBX."""
+import os
+from contextlib import contextmanager
+
+from maya import cmds # noqa
+
+import pyblish.api
+import openpype.api
+from openpype.hosts.maya.api import fbx
+
+
+@contextmanager
+def renamed(original_name, renamed_name):
+ # type: (str, str) -> None
+ try:
+ cmds.rename(original_name, renamed_name)
+ yield
+ finally:
+ cmds.rename(renamed_name, original_name)
+
+
+class ExtractUnrealSkeletalMesh(openpype.api.Extractor):
+ """Extract Unreal Skeletal Mesh as FBX from Maya. """
+
+ order = pyblish.api.ExtractorOrder - 0.1
+ label = "Extract Unreal Skeletal Mesh"
+ families = ["skeletalMesh"]
+
+ def process(self, instance):
+ fbx_exporter = fbx.FBXExtractor(log=self.log)
+
+ # Define output path
+ staging_dir = self.staging_dir(instance)
+ filename = "{0}.fbx".format(instance.name)
+ path = os.path.join(staging_dir, filename)
+
+ geo = instance.data.get("geometry")
+ joints = instance.data.get("joints")
+
+ to_extract = geo + joints
+
+ # The export requires forward slashes because we need
+ # to format it into a string in a mel expression
+ path = path.replace('\\', '/')
+
+ self.log.info("Extracting FBX to: {0}".format(path))
+ self.log.info("Members: {0}".format(to_extract))
+ self.log.info("Instance: {0}".format(instance[:]))
+
+ fbx_exporter.set_options_from_instance(instance)
+
+ # This magic is done for variants. To let Unreal merge correctly
+ # existing data, top node must have the same name. So for every
+ # variant we extract we need to rename top node of the rig correctly.
+ # It is finally done in context manager so it won't affect current
+ # scene.
+
+ # we rely on hierarchy under one root.
+ original_parent = to_extract[0].split("|")[1]
+
+ parent_node = instance.data.get("asset")
+
+ renamed_to_extract = []
+ for node in to_extract:
+ node_path = node.split("|")
+ node_path[1] = parent_node
+ renamed_to_extract.append("|".join(node_path))
+
+ with renamed(original_parent, parent_node):
+ self.log.info("Extracting: {}".format(renamed_to_extract, path))
+ fbx_exporter.export(renamed_to_extract, path)
+
+ if "representations" not in instance.data:
+ instance.data["representations"] = []
+
+ representation = {
+ 'name': 'fbx',
+ 'ext': 'fbx',
+ 'files': filename,
+ "stagingDir": staging_dir,
+ }
+ instance.data["representations"].append(representation)
+
+ self.log.info("Extract FBX successful to: {0}".format(path))
diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py
index 32dc9d1d1c..69d51f9ff1 100644
--- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py
+++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py
@@ -1,33 +1,61 @@
# -*- coding: utf-8 -*-
"""Create Unreal Static Mesh data to be extracted as FBX."""
-import openpype.api
-import pyblish.api
+import os
+
from maya import cmds # noqa
+import pyblish.api
+import openpype.api
+from openpype.hosts.maya.api.lib import (
+ parent_nodes,
+ maintained_selection
+)
+from openpype.hosts.maya.api import fbx
+
class ExtractUnrealStaticMesh(openpype.api.Extractor):
- """Extract FBX from Maya. """
+ """Extract Unreal Static Mesh as FBX from Maya. """
order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Unreal Static Mesh"
- families = ["unrealStaticMesh"]
+ families = ["staticMesh"]
def process(self, instance):
- to_combine = instance.data.get("membersToCombine")
- static_mesh_name = instance.data.get("staticMeshCombinedName")
- self.log.info(
- "merging {} into {}".format(
- " + ".join(to_combine), static_mesh_name))
- duplicates = cmds.duplicate(to_combine, ic=True)
- cmds.polyUnite(
- *duplicates,
- n=static_mesh_name, ch=False)
+ members = instance.data.get("geometryMembers", [])
+ if instance.data.get("collisionMembers"):
+ members = members + instance.data.get("collisionMembers")
- if not instance.data.get("cleanNodes"):
- instance.data["cleanNodes"] = []
+ fbx_exporter = fbx.FBXExtractor(log=self.log)
- instance.data["cleanNodes"].append(static_mesh_name)
- instance.data["cleanNodes"] += duplicates
+ # Define output path
+ staging_dir = self.staging_dir(instance)
+ filename = "{0}.fbx".format(instance.name)
+ path = os.path.join(staging_dir, filename)
- instance.data["setMembers"] = [static_mesh_name]
- instance.data["setMembers"] += instance.data["collisionMembers"]
+ # The export requires forward slashes because we need
+ # to format it into a string in a mel expression
+ path = path.replace('\\', '/')
+
+ self.log.info("Extracting FBX to: {0}".format(path))
+ self.log.info("Members: {0}".format(members))
+ self.log.info("Instance: {0}".format(instance[:]))
+
+ fbx_exporter.set_options_from_instance(instance)
+
+ with maintained_selection():
+ with parent_nodes(members):
+ self.log.info("Un-parenting: {}".format(members))
+ fbx_exporter.export(members, path)
+
+ if "representations" not in instance.data:
+ instance.data["representations"] = []
+
+ representation = {
+ 'name': 'fbx',
+ 'ext': 'fbx',
+ 'files': filename,
+ "stagingDir": staging_dir,
+ }
+ instance.data["representations"].append(representation)
+
+ self.log.info("Extract FBX successful to: {0}".format(path))
diff --git a/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml b/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml
new file mode 100644
index 0000000000..d30c4cb69d
--- /dev/null
+++ b/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml
@@ -0,0 +1,14 @@
+
+
+
+Skeletal Mesh Top Node
+## Skeletal meshes needs common root
+
+Skeletal meshes and their joints must be under one common root.
+
+### How to repair?
+
+Make sure all geometry and joints resides under same root.
+
+
+
diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py
new file mode 100644
index 0000000000..54a86d27cf
--- /dev/null
+++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+import pyblish.api
+import openpype.api
+from openpype.pipeline import PublishXmlValidationError
+
+from maya import cmds
+
+
+class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin):
+ """Validates that nodes has common root."""
+
+ order = openpype.api.ValidateContentsOrder
+ hosts = ["maya"]
+ families = ["skeletalMesh"]
+ label = "Skeletal Mesh Top Node"
+
+ def process(self, instance):
+ geo = instance.data.get("geometry")
+ joints = instance.data.get("joints")
+
+ joints_parents = cmds.ls(joints, long=True)
+ geo_parents = cmds.ls(geo, long=True)
+
+ parents_set = {
+ parent.split("|")[1] for parent in (joints_parents + geo_parents)
+ }
+
+ if len(set(parents_set)) != 1:
+ raise PublishXmlValidationError(
+ self,
+ "Multiple roots on geometry or joints."
+ )
diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py
index b2ef174374..c05121a1b0 100644
--- a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py
+++ b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py
@@ -10,10 +10,11 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin):
order = openpype.api.ValidateMeshOrder
hosts = ["maya"]
- families = ["unrealStaticMesh"]
+ families = ["staticMesh"]
category = "geometry"
label = "Mesh is Triangulated"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ active = False
@classmethod
def get_invalid(cls, instance):
diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py
index 901a2ec75e..43f6c85827 100644
--- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py
+++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-
+"""Validator for correct naming of Static Meshes."""
from maya import cmds # noqa
import pyblish.api
import openpype.api
@@ -52,8 +52,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
optional = True
order = openpype.api.ValidateContentsOrder
hosts = ["maya"]
- families = ["unrealStaticMesh"]
- label = "Unreal StaticMesh Name"
+ families = ["staticMesh"]
+ label = "Unreal Static Mesh Name"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
regex_mesh = r"(?P.*))"
regex_collision = r"(?P.*)"
@@ -72,15 +72,13 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
["collision_prefixes"]
)
- combined_geometry_name = instance.data.get(
- "staticMeshCombinedName", None)
if cls.validate_mesh:
# compile regex for testing names
regex_mesh = "{}{}".format(
("_" + cls.static_mesh_prefix) or "", cls.regex_mesh
)
sm_r = re.compile(regex_mesh)
- if not sm_r.match(combined_geometry_name):
+ if not sm_r.match(instance.data.get("subset")):
cls.log.error("Mesh doesn't comply with name validation.")
return True
@@ -91,7 +89,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
cls.log.warning("No collision objects to validate.")
return False
- regex_collision = "{}{}".format(
+ regex_collision = "{}{}_(\\d+)".format(
"(?P({}))_".format(
"|".join("{0}".format(p) for p in collision_prefixes)
) or "", cls.regex_collision
@@ -99,6 +97,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
cl_r = re.compile(regex_collision)
+ mesh_name = "{}{}".format(instance.data["asset"],
+ instance.data.get("variant", []))
+
for obj in collision_set:
cl_m = cl_r.match(obj)
if not cl_m:
@@ -107,7 +108,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
else:
expected_collision = "{}_{}".format(
cl_m.group("prefix"),
- combined_geometry_name
+ mesh_name
)
if not obj.startswith(expected_collision):
@@ -116,11 +117,11 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
"Collision object name doesn't match "
"static mesh name"
)
- cls.log.error("{}_{} != {}_{}".format(
+ cls.log.error("{}_{} != {}_{}*".format(
cl_m.group("prefix"),
cl_m.group("renderName"),
cl_m.group("prefix"),
- combined_geometry_name,
+ mesh_name,
))
invalid.append(obj)
diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py
index 5a8c29c22d..5e1b04889f 100644
--- a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py
+++ b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py
@@ -9,9 +9,10 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin):
"""Validate if Z is set as up axis in Maya"""
optional = True
+ active = False
order = openpype.api.ValidateContentsOrder
hosts = ["maya"]
- families = ["unrealStaticMesh"]
+ families = ["staticMesh"]
label = "Unreal Up-Axis check"
actions = [openpype.api.RepairAction]
diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py
index fa181301ee..1f509365c7 100644
--- a/openpype/plugins/publish/collect_resources_path.py
+++ b/openpype/plugins/publish/collect_resources_path.py
@@ -53,7 +53,10 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
"textures",
"action",
"background",
- "effect"
+ "effect",
+ "staticMesh",
+ "skeletalMesh"
+
]
def process(self, instance):
diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py
index 2304f98713..acdb05dd93 100644
--- a/openpype/plugins/publish/integrate_new.py
+++ b/openpype/plugins/publish/integrate_new.py
@@ -106,6 +106,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"xgen",
"hda",
"usd",
+ "staticMesh",
+ "skeletalMesh",
"usdComposition",
"usdOverride"
]
diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json
index d46d449c77..7d01248653 100644
--- a/openpype/settings/defaults/project_anatomy/templates.json
+++ b/openpype/settings/defaults/project_anatomy/templates.json
@@ -28,9 +28,18 @@
},
"delivery": {},
"unreal": {
- "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}",
- "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}",
+ "folder": "{root[work]}/{project[name]}/unreal/{task[name]}",
+ "file": "{project[code]}_{asset}",
"path": "{@folder}/{@file}"
},
- "others": {}
+ "others": {
+ "maya2unreal": {
+ "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}",
+ "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}",
+ "path": "{@folder}/{@file}"
+ },
+ "__dynamic_keys_labels__": {
+ "maya2unreal": "Maya to Unreal"
+ }
+ }
}
\ No newline at end of file
diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json
index 24334b0045..ace760d994 100644
--- a/openpype/settings/defaults/project_settings/global.json
+++ b/openpype/settings/defaults/project_settings/global.json
@@ -178,6 +178,18 @@
"task_types": [],
"tasks": [],
"template_name": "render"
+ },
+ {
+ "families": [
+ "staticMesh",
+ "skeletalMesh"
+ ],
+ "hosts": [
+ "maya"
+ ],
+ "task_types": [],
+ "tasks": [],
+ "template_name": "maya2unreal"
}
],
"subset_grouping_profiles": [
@@ -289,7 +301,7 @@
},
{
"families": [
- "unrealStaticMesh"
+ "staticMesh"
],
"hosts": [
"maya"
@@ -297,6 +309,17 @@
"task_types": [],
"tasks": [],
"template": "S_{asset}{variant}"
+ },
+ {
+ "families": [
+ "skeletalMesh"
+ ],
+ "hosts": [
+ "maya"
+ ],
+ "task_types": [],
+ "tasks": [],
+ "template": "SK_{asset}{variant}"
}
]
},
@@ -306,6 +329,13 @@
"task_types": [],
"hosts": [],
"workfile_template": "work"
+ },
+ {
+ "task_types": [],
+ "hosts": [
+ "unreal"
+ ],
+ "workfile_template": "unreal"
}
],
"last_workfile_on_startup": [
diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json
index 19d9a95595..4cdfe1ca5d 100644
--- a/openpype/settings/defaults/project_settings/maya.json
+++ b/openpype/settings/defaults/project_settings/maya.json
@@ -52,7 +52,7 @@
"",
"_Main"
],
- "static_mesh_prefix": "S_",
+ "static_mesh_prefix": "S",
"collision_prefixes": [
"UBX",
"UCP",
@@ -60,6 +60,11 @@
"UCX"
]
},
+ "CreateUnrealSkeletalMesh": {
+ "enabled": true,
+ "defaults": [],
+ "joint_hints": "jnt_org"
+ },
"CreateAnimation": {
"enabled": true,
"defaults": [
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
index 0544b4bab7..6dc10ed2a5 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
@@ -97,6 +97,32 @@
}
]
+ },
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "CreateUnrealSkeletalMesh",
+ "label": "Create Unreal - Skeletal Mesh",
+ "checkbox_key": "enabled",
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "list",
+ "key": "defaults",
+ "label": "Default Subsets",
+ "object_type": "text"
+ },
+ {
+ "type": "text",
+ "key": "joint_hints",
+ "label": "Joint root hint"
+ }
+ ]
+
},
{
"type": "schema_template",