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

Maya to Unreal: Static and Skeletal Meshes #2978

Merged
merged 34 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1276637
fix case with single mesh and prefixes
antirotor Feb 14, 2022
1c06015
remove debug print
antirotor Feb 15, 2022
3f7602c
fix prefix
antirotor Feb 15, 2022
3cf4863
rename family to staticMesh
antirotor Feb 17, 2022
487b273
refactor fbx extractor
antirotor Feb 18, 2022
9604dca
fix logging
antirotor Feb 18, 2022
3a42aa5
fix family name in defaults
antirotor Feb 18, 2022
f5087f4
fix hound 🐶
antirotor Feb 18, 2022
a50ec29
Merge remote-tracking branch 'origin/develop' into bugfix/maya-static…
antirotor Feb 18, 2022
1121bc8
disable unnecessary plugins
antirotor Mar 1, 2022
691f23d
unify handles
antirotor Mar 1, 2022
901df52
remove ftrack submodules from old location
antirotor Mar 1, 2022
298da27
Merge remote-tracking branch 'origin/develop' into bugfix/maya-static…
antirotor Mar 1, 2022
28e11a5
fix frame handling and collision name determination
antirotor Mar 1, 2022
bd54787
change default templates
antirotor Mar 1, 2022
b63896c
Merge branch 'bugfix/maya-static-mesh-bugfix' into feature/OP-2795_ma…
antirotor Mar 4, 2022
8c0d7ed
initial work on skeletal mesh support
antirotor Mar 4, 2022
5e5f6e0
fix un-parenting
antirotor Mar 8, 2022
592c95b
fixes static mesh side of things
antirotor Mar 10, 2022
bc59689
fixed fbx settings
antirotor Mar 10, 2022
52dd761
simplify whole process
antirotor Mar 11, 2022
87b2563
fix for multiple subsets
antirotor Mar 14, 2022
d31bee0
fix parenting for skeletal meshes
antirotor Mar 14, 2022
08370f5
fix getting correct name with prefix
antirotor Mar 15, 2022
e67ac0c
Merge remote-tracking branch 'origin/develop' into feature/OP-2795_ma…
antirotor Mar 21, 2022
cc7a5e0
node renaming wip
antirotor Mar 22, 2022
8f77e92
rename top node for variants
antirotor Mar 22, 2022
76bc779
add top node validator
antirotor Mar 25, 2022
b711ba5
fix validator
antirotor Mar 27, 2022
3a55b80
fix docstring and remove unused code
antirotor Mar 27, 2022
228d3cf
fix hierarchy
antirotor Mar 27, 2022
14cb983
Merge branch 'develop' into feature/OP-2795_maya-to-unreal-skeletal-m…
antirotor Mar 30, 2022
85e2601
fix 🐺
antirotor Apr 1, 2022
557aafd
fixed skeletal root
antirotor Apr 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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