Merge pull request #2537 from pypeclub/feature/OP-2378_maya-to-unreal-static-meshes

This commit is contained in:
Milan Kolar 2022-01-24 17:53:17 +01:00 committed by GitHub
commit 2da3bce951
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 362 additions and 87 deletions

View file

@ -2854,3 +2854,27 @@ def set_colorspace():
cmds.colorManagementPrefs(e=True, renderingSpaceName=renderSpace)
viewTransform = root_dict["viewTransform"]
cmds.colorManagementPrefs(e=True, viewTransformName=viewTransform)
@contextlib.contextmanager
def root_parent(nodes):
# type: (list) -> list
"""Context manager to un-parent provided nodes and return then back."""
import pymel.core as pm # noqa
node_parents = []
for node in nodes:
n = pm.PyNode(node)
try:
root = pm.listRelatives(n, parent=1)[0]
except IndexError:
root = None
node_parents.append((n, root))
try:
for node in node_parents:
node[0].setParent(world=True)
yield
finally:
for node in node_parents:
if node[1]:
node[0].setParent(node[1])

View file

@ -1,11 +1,58 @@
from openpype.hosts.maya.api import plugin
# -*- coding: utf-8 -*-
"""Creator for Unreal Static Meshes."""
from openpype.hosts.maya.api import plugin, lib
from avalon.api import Session
from openpype.api import get_project_settings
from maya import cmds # noqa
class CreateUnrealStaticMesh(plugin.Creator):
"""Unreal Static Meshes with collisions."""
name = "staticMeshMain"
label = "Unreal - Static Mesh"
family = "unrealStaticMesh"
icon = "cube"
dynamic_subset_keys = ["asset"]
def __init__(self, *args, **kwargs):
"""Constructor."""
super(CreateUnrealStaticMesh, self).__init__(*args, **kwargs)
self._project_settings = get_project_settings(
Session["AVALON_PROJECT"])
@classmethod
def get_dynamic_data(
cls, variant, task_name, asset_id, project_name, host_name
):
dynamic_data = super(CreateUnrealStaticMesh, 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):
with lib.undo_chunk():
instance = super(CreateUnrealStaticMesh, 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)
collisions_set = cmds.sets(name="collisions_SET", empty=True)
cmds.sets([geometry_set, collisions_set], forceElement=instance)
members = cmds.ls(content, long=True) or []
children = cmds.listRelatives(members, allDescendents=True,
fullPath=True) or []
children = cmds.ls(children, type="transform")
for node in children:
if cmds.listRelatives(node, type="shape"):
if [
n for n in self.collision_prefixes
if node.startswith(n)
]:
cmds.sets(node, forceElement=collisions_set)
else:
cmds.sets(node, forceElement=geometry_set)

View file

@ -0,0 +1,31 @@
# -*- 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

View file

@ -4,25 +4,31 @@ import pyblish.api
class CollectUnrealStaticMesh(pyblish.api.InstancePlugin):
"""Collect unreal static mesh
"""Collect Unreal Static Mesh
Ensures always only a single frame is extracted (current frame). This
also sets correct FBX options for later extraction.
Note:
This is a workaround so that the `pype.model` family can use the
same pointcache extractor implementation as animation and pointcaches.
This always enforces the "current" frame to be published.
"""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Model Data"
label = "Collect Unreal Static Meshes"
families = ["unrealStaticMesh"]
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, query=True)
collision_set = [i for i in instance if i == "collisions_SET"]
instance.data["collisionMembers"] = cmds.sets(
collision_set, query=True)
# set fbx overrides on instance
instance.data["smoothingGroups"] = True
instance.data["smoothMesh"] = True

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
import os
from maya import cmds
import maya.mel as mel
from maya import cmds # noqa
import maya.mel as mel # noqa
from openpype.hosts.maya.api.lib import root_parent
import pyblish.api
import avalon.maya
@ -192,10 +194,7 @@ class ExtractFBX(openpype.api.Extractor):
if isinstance(value, bool):
value = str(value).lower()
template = "FBXExport{0} -v {1}"
if key == "UpAxis":
template = "FBXExport{0} {1}"
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)
@ -205,9 +204,16 @@ class ExtractFBX(openpype.api.Extractor):
mel.eval("FBXExportGenerateLog -v false")
# Export
with avalon.maya.maintained_selection():
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
if "unrealStaticMesh" in instance.data["families"]:
with avalon.maya.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 avalon.maya.maintained_selection():
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
if "representations" not in instance.data:
instance.data["representations"] = []

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""Create Unreal Static Mesh data to be extracted as FBX."""
import openpype.api
import pyblish.api
from maya import cmds # noqa
class ExtractUnrealStaticMesh(openpype.api.Extractor):
"""Extract FBX from Maya. """
order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Unreal Static Mesh"
families = ["unrealStaticMesh"]
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)
if not instance.data.get("cleanNodes"):
instance.data["cleanNodes"] = []
instance.data["cleanNodes"].append(static_mesh_name)
instance.data["cleanNodes"] += duplicates
instance.data["setMembers"] = [static_mesh_name]
instance.data["setMembers"] += instance.data["collisionMembers"]

View file

@ -30,7 +30,8 @@ class ValidateAssemblyName(pyblish.api.InstancePlugin):
descendants = cmds.listRelatives(content_instance,
allDescendents=True,
fullPath=True) or []
descendants = cmds.ls(descendants, noIntermediate=True, long=True)
descendants = cmds.ls(
descendants, noIntermediate=True, type="transform")
content_instance = list(set(content_instance + descendants))
assemblies = cmds.ls(content_instance, assemblies=True, long=True)

View file

@ -1,27 +1,30 @@
# -*- coding: utf-8 -*-
from maya import cmds
from maya import cmds # noqa
import pyblish.api
import openpype.api
import openpype.hosts.maya.api.action
from avalon.api import Session
from openpype.api import get_project_settings
import re
class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
"""Validate name of Unreal Static Mesh
Unreals naming convention states that staticMesh should start with `SM`
prefix - SM_[Name]_## (Eg. SM_sube_01). This plugin also validates other
types of meshes - collision meshes:
prefix - SM_[Name]_## (Eg. SM_sube_01).These prefixes can be configured
in Settings UI. This plugin also validates other types of
meshes - collision meshes:
UBX_[RenderMeshName]_##:
UBX_[RenderMeshName]*:
Boxes are created with the Box objects type in
Max or with the Cube polygonal primitive in Maya.
You cannot move the vertices around or deform it
in any way to make it something other than a
rectangular prism, or else it will not work.
UCP_[RenderMeshName]_##:
UCP_[RenderMeshName]*:
Capsules are created with the Capsule object type.
The capsule does not need to have many segments
(8 is a good number) at all because it is
@ -29,7 +32,7 @@ class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
boxes, you should not move the individual
vertices around.
USP_[RenderMeshName]_##:
USP_[RenderMeshName]*:
Spheres are created with the Sphere object type.
The sphere does not need to have many segments
(8 is a good number) at all because it is
@ -37,7 +40,7 @@ class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
boxes, you should not move the individual
vertices around.
UCX_[RenderMeshName]_##:
UCX_[RenderMeshName]*:
Convex objects can be any completely closed
convex 3D shape. For example, a box can also be
a convex object
@ -52,67 +55,86 @@ class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
families = ["unrealStaticMesh"]
label = "Unreal StaticMesh Name"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
regex_mesh = r"SM_(?P<renderName>.*)_(\d{2})"
regex_collision = r"((UBX)|(UCP)|(USP)|(UCX))_(?P<renderName>.*)_(\d{2})"
regex_mesh = r"(?P<renderName>.*))"
regex_collision = r"(?P<renderName>.*)"
@classmethod
def get_invalid(cls, instance):
# find out if supplied transform is group or not
def is_group(groupName):
try:
children = cmds.listRelatives(groupName, children=True)
for child in children:
if not cmds.ls(child, transforms=True):
return False
invalid = []
project_settings = get_project_settings(Session["AVALON_PROJECT"])
collision_prefixes = (
project_settings
["maya"]
["create"]
["CreateUnrealStaticMesh"]
["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):
cls.log.error("Mesh doesn't comply with name validation.")
return True
except Exception:
if cls.validate_collision:
collision_set = instance.data.get("collisionMembers", None)
# soft-fail is there are no collision objects
if not collision_set:
cls.log.warning("No collision objects to validate.")
return False
invalid = []
content_instance = instance.data.get("setMembers", None)
if not content_instance:
cls.log.error("Instance has no nodes!")
return True
pass
descendants = cmds.listRelatives(content_instance,
allDescendents=True,
fullPath=True) or []
regex_collision = "{}{}".format(
"(?P<prefix>({}))_".format(
"|".join("{0}".format(p) for p in collision_prefixes)
) or "", cls.regex_collision
)
descendants = cmds.ls(descendants, noIntermediate=True, long=True)
trns = cmds.ls(descendants, long=False, type=('transform'))
cl_r = re.compile(regex_collision)
# filter out groups
filter = [node for node in trns if not is_group(node)]
# compile regex for testing names
sm_r = re.compile(cls.regex_mesh)
cl_r = re.compile(cls.regex_collision)
sm_names = []
col_names = []
for obj in filter:
sm_m = sm_r.match(obj)
if sm_m is None:
# test if it matches collision mesh
cl_r = sm_r.match(obj)
if cl_r is None:
cls.log.error("invalid mesh name on: {}".format(obj))
for obj in collision_set:
cl_m = cl_r.match(obj)
if not cl_m:
cls.log.error("{} is invalid".format(obj))
invalid.append(obj)
else:
col_names.append((cl_r.group("renderName"), obj))
else:
sm_names.append(sm_m.group("renderName"))
expected_collision = "{}_{}".format(
cl_m.group("prefix"),
combined_geometry_name
)
for c_mesh in col_names:
if c_mesh[0] not in sm_names:
cls.log.error(("collision name {} doesn't match any "
"static mesh names.").format(obj))
invalid.append(c_mesh[1])
if not obj.startswith(expected_collision):
cls.log.error(
"Collision object name doesn't match "
"static mesh name"
)
cls.log.error("{}_{} != {}_{}".format(
cl_m.group("prefix"),
cl_m.group("renderName"),
cl_m.group("prefix"),
combined_geometry_name,
))
invalid.append(obj)
return invalid
def process(self, instance):
if not self.validate_mesh and not self.validate_collision:
self.log.info("Validation of both mesh and collision names"
"is disabled.")
return
if not instance.data.get("collisionMembers", None):
self.log.info("There are no collision objects to validate")
return
invalid = self.get_invalid(instance)

View file

@ -389,6 +389,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
repre["ext"] = ext
template_data["ext"] = ext
self.log.info(template_name)
template = os.path.normpath(
anatomy.templates[template_name]["path"])

View file

@ -27,5 +27,10 @@
"path": "{@folder}/{@file}"
},
"delivery": {},
"unreal": {
"folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}",
"file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}",
"path": "{@folder}/{@file}"
},
"others": {}
}

View file

@ -219,7 +219,7 @@
"hosts": [],
"task_types": [],
"tasks": [],
"template": "{family}{Variant}"
"template": "{family}{variant}"
},
{
"families": [
@ -264,6 +264,17 @@
"task_types": [],
"tasks": [],
"template": "render{Task}{Variant}"
},
{
"families": [
"unrealStaticMesh"
],
"hosts": [
"maya"
],
"task_types": [],
"tasks": [],
"template": "S_{asset}{variant}"
}
]
},

View file

@ -46,6 +46,20 @@
"aov_separator": "underscore",
"default_render_image_folder": "renders"
},
"CreateUnrealStaticMesh": {
"enabled": true,
"defaults": [
"",
"_Main"
],
"static_mesh_prefix": "S_",
"collision_prefixes": [
"UBX",
"UCP",
"USP",
"UCX"
]
},
"CreateAnimation": {
"enabled": true,
"defaults": [
@ -123,12 +137,6 @@
"Anim"
]
},
"CreateUnrealStaticMesh": {
"enabled": true,
"defaults": [
"Main"
]
},
"CreateVrayProxy": {
"enabled": true,
"defaults": [
@ -180,6 +188,11 @@
"whitelist_native_plugins": false,
"authorized_plugins": []
},
"ValidateUnrealStaticMeshName": {
"enabled": true,
"validate_mesh": false,
"validate_collision": true
},
"ValidateRenderSettings": {
"arnold_render_attributes": [],
"vray_render_attributes": [],
@ -197,6 +210,11 @@
"regex": "(.*)_(\\d)*_(?P<shader>.*)_(GEO)",
"top_level_regex": ".*_GRP"
},
"ValidateModelContent": {
"enabled": true,
"optional": false,
"validate_top_group": true
},
"ValidateTransformNamingSuffix": {
"enabled": true,
"SUFFIX_NAMING_TABLE": {
@ -281,11 +299,6 @@
"optional": true,
"active": true
},
"ValidateModelContent": {
"enabled": true,
"optional": false,
"validate_top_group": true
},
"ValidateNoAnimation": {
"enabled": false,
"optional": true,

View file

@ -143,6 +143,28 @@
"label": "Delivery",
"object_type": "text"
},
{
"type": "dict",
"key": "unreal",
"label": "Unreal",
"children": [
{
"type": "text",
"key": "folder",
"label": "Folder"
},
{
"type": "text",
"key": "file",
"label": "File"
},
{
"type": "text",
"key": "path",
"label": "Path"
}
]
},
{
"type": "dict-modifiable",
"key": "others",

View file

@ -66,6 +66,38 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CreateUnrealStaticMesh",
"label": "Create Unreal - Static Mesh",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "list",
"key": "defaults",
"label": "Default Subsets",
"object_type": "text"
},
{
"type": "text",
"key": "static_mesh_prefix",
"label": "Static Mesh Prefix"
},
{
"type": "list",
"key": "collision_prefixes",
"label": "Collision Mesh Prefixes",
"object_type": "text"
}
]
},
{
"type": "schema_template",
"name": "template_create_plugin",
@ -118,10 +150,6 @@
"key": "CreateSetDress",
"label": "Create Set Dress"
},
{
"key": "CreateUnrealStaticMesh",
"label": "Create Unreal - Static Mesh"
},
{
"key": "CreateVrayProxy",
"label": "Create VRay Proxy"

View file

@ -129,6 +129,31 @@
]
},
{
"type": "dict",
"collapsible": true,
"key": "ValidateUnrealStaticMeshName",
"label": "Validate Unreal Static Mesh Name",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "validate_mesh",
"label": "Validate mesh Names "
},
{
"type": "boolean",
"key": "validate_collision",
"label": "Validate collision names"
}
]
},
{
"type": "dict",
"collapsible": true,