Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2978 from pypeclub/feature/OP-2795_maya-to-unreal…
Browse files Browse the repository at this point in the history
…-skeletal-meshes

Maya to Unreal > Static and Skeletal Meshes
  • Loading branch information
antirotor authored Apr 5, 2022
2 parents 143f7b8 + 557aafd commit 1d3a2c2
Show file tree
Hide file tree
Showing 22 changed files with 621 additions and 275 deletions.
202 changes: 202 additions & 0 deletions openpype/hosts/maya/api/fbx.py
Original file line number Diff line number Diff line change
@@ -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))
20 changes: 17 additions & 3 deletions openpype/hosts/maya/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3138,11 +3138,20 @@ def _colormanage(**kwargs):


@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)
Expand All @@ -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)
50 changes: 50 additions & 0 deletions openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -28,10 +28,10 @@ def 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(CreateUnrealStaticMesh, self).process()
content = cmds.sets(instance, query=True)
Expand Down
3 changes: 2 additions & 1 deletion openpype/hosts/maya/plugins/load/load_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 0 additions & 31 deletions openpype/hosts/maya/plugins/publish/clean_nodes.py

This file was deleted.

39 changes: 39 additions & 0 deletions openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 1d3a2c2

Please sign in to comment.