publishing unreal static mesh from maya

This commit is contained in:
Ondrej Samohel 2020-03-14 00:41:53 +01:00
parent 05ea8b14b9
commit 9a8655be1d
No known key found for this signature in database
GPG key ID: 8A29C663C672C2B7
14 changed files with 503 additions and 5 deletions

View file

@ -36,7 +36,7 @@ class UnrealPrelaunch(PypeHook):
# Unreal is sensitive about project names longer then 20 chars
if len(project_name) > 20:
self.log.warning((f"Project name exceed 20 characters "
f"[ {project_name} ]!"))
f"({project_name})!"))
# Unreal doesn't accept non alphabet characters at the start
# of the project name. This is because project name is then used

View file

@ -16,6 +16,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
if "standalonepublisher" in context.data.get("host", []):
return
if "unreal" in context.data.get("host", []):
return
filename = os.path.basename(context.data.get('currentFile'))
if '<shell>' in filename:

View file

@ -80,7 +80,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"matchmove",
"image"
"source",
"assembly"
"assembly",
"fbx"
]
exclude_families = ["clip"]
db_representation_context_keys = [

View file

@ -0,0 +1,11 @@
import avalon.maya
class CreateUnrealStaticMesh(avalon.maya.Creator):
name = "staticMeshMain"
label = "Unreal - Static Mesh"
family = "unrealStaticMesh"
icon = "cube"
def __init__(self, *args, **kwargs):
super(CreateUnrealStaticMesh, self).__init__(*args, **kwargs)

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from maya import cmds
import pyblish.api
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.
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"
families = ["unrealStaticMesh"]
def process(self, instance):
# add fbx family to trigger fbx extractor
instance.data["families"].append("fbx")
# set fbx overrides on instance
instance.data["smoothingGroups"] = True
instance.data["smoothMesh"] = True
instance.data["triangulate"] = True
frame = cmds.currentTime(query=True)
instance.data["frameStart"] = frame
instance.data["frameEnd"] = frame

View file

@ -212,12 +212,11 @@ class ExtractFBX(pype.api.Extractor):
instance.data["representations"] = []
representation = {
'name': 'mov',
'ext': 'mov',
'name': 'fbx',
'ext': 'fbx',
'files': filename,
"stagingDir": stagingDir,
}
instance.data["representations"].append(representation)
self.log.info("Extract FBX successful to: {0}".format(path))

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from maya import cmds
import pyblish.api
import pype.api
class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin):
"""Validate if mesh is made of triangles for Unreal Engine"""
order = pype.api.ValidateMeshOder
hosts = ["maya"]
families = ["unrealStaticMesh"]
category = "geometry"
label = "Mesh is Triangulated"
actions = [pype.maya.action.SelectInvalidAction]
@classmethod
def get_invalid(cls, instance):
invalid = []
meshes = cmds.ls(instance, type="mesh", long=True)
for mesh in meshes:
faces = cmds.polyEvaluate(mesh, f=True)
tris = cmds.polyEvaluate(mesh, t=True)
if faces != tris:
invalid.append(mesh)
return invalid
def process(self, instance):
invalid = self.get_invalid(instance)
assert len(invalid) == 0, (
"Found meshes without triangles")

View file

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
from maya import cmds
import pyblish.api
import pype.api
import pype.maya.action
import re
class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin):
"""Validate name of Unreal Static Mesh
Unreals naming convention states that staticMesh sould start with `SM`
prefix - SM_[Name]_## (Eg. SM_sube_01). This plugin also validates other
types of meshes - collision meshes:
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]_##:
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
converted into a true capsule for collision. Like
boxes, you should not move the individual
vertices around.
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
converted into a true sphere for collision. Like
boxes, you should not move the individual
vertices around.
UCX_[RenderMeshName]_##:
Convex objects can be any completely closed
convex 3D shape. For example, a box can also be
a convex object
This validator also checks if collision mesh [RenderMeshName] matches one
of SM_[RenderMeshName].
"""
optional = True
order = pype.api.ValidateContentsOrder
hosts = ["maya"]
families = ["unrealStaticMesh"]
label = "Unreal StaticMesh Name"
actions = [pype.maya.action.SelectInvalidAction]
regex_mesh = r"SM_(?P<renderName>.*)_(\d{2})"
regex_collision = r"((UBX)|(UCP)|(USP)|(UCX))_(?P<renderName>.*)_(\d{2})"
@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
return True
except Exception:
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 []
descendants = cmds.ls(descendants, noIntermediate=True, long=True)
trns = cmds.ls(descendants, long=False, type=('transform'))
# 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))
invalid.append(obj)
else:
col_names.append((cl_r.group("renderName"), obj))
else:
sm_names.append(sm_m.group("renderName"))
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])
return invalid
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError("Model naming is invalid. See log.")

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from maya import cmds
import pyblish.api
import pype.api
class ValidateUnrealUpAxis(pyblish.api.ContextPlugin):
"""Validate if Z is set as up axis in Maya"""
optional = True
order = pype.api.ValidateContentsOrder
hosts = ["maya"]
families = ["unrealStaticMesh"]
label = "Unreal Up-Axis check"
actions = [pype.api.RepairAction]
def process(self, context):
assert cmds.upAxis(q=True, axis=True) == "z", (
"Invalid axis set as up axis"
)
@classmethod
def repair(cls, instance):
cmds.upAxis(axis="z", rotateView=True)

View file

@ -0,0 +1,14 @@
from pype.unreal.plugin import Creator
class CreateFbx(Creator):
"""Static FBX geometry"""
name = "modelMain"
label = "Model"
family = "model"
icon = "cube"
asset_types = ["StaticMesh"]
def __init__(self, *args, **kwargs):
super(CreateFbx, self).__init__(*args, **kwargs)

View file

@ -0,0 +1,53 @@
from avalon import api
from avalon import unreal as avalon_unreal
import unreal
import time
class StaticMeshFBXLoader(api.Loader):
"""Load Unreal StaticMesh from FBX"""
families = ["unrealStaticMesh"]
label = "Import FBX Static Mesh"
representations = ["fbx"]
icon = "cube"
color = "orange"
def load(self, context, name, namespace, data):
tools = unreal.AssetToolsHelpers().get_asset_tools()
temp_dir, temp_name = tools.create_unique_asset_name(
"/Game/{}".format(name), "_TMP"
)
# asset_path = "/Game/{}".format(namespace)
unreal.EditorAssetLibrary.make_directory(temp_dir)
task = unreal.AssetImportTask()
task.filename = self.fname
task.destination_path = temp_dir
task.destination_name = name
task.replace_existing = False
task.automated = True
task.save = True
# set import options here
task.options = unreal.FbxImportUI()
task.options.import_animations = False
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501
imported_assets = unreal.EditorAssetLibrary.list_assets(
temp_dir, recursive=True, include_folder=True
)
new_dir = avalon_unreal.containerise(
name, namespace, imported_assets, context, self.__class__.__name__)
asset_content = unreal.EditorAssetLibrary.list_assets(
new_dir, recursive=True, include_folder=True
)
unreal.EditorAssetLibrary.delete_directory(temp_dir)
return asset_content

View file

@ -0,0 +1,152 @@
import unreal
import pyblish.api
class CollectInstances(pyblish.api.ContextPlugin):
"""Gather instances by objectSet and pre-defined attribute
This collector takes into account assets that are associated with
an objectSet and marked with a unique identifier;
Identifier:
id (str): "pyblish.avalon.instance"
Limitations:
- Does not take into account nodes connected to those
within an objectSet. Extractors are assumed to export
with history preserved, but this limits what they will
be able to achieve and the amount of data available
to validators. An additional collector could also
append this input data into the instance, as we do
for `pype.rig` with collect_history.
"""
label = "Collect Instances"
order = pyblish.api.CollectorOrder
hosts = ["unreal"]
def process(self, context):
objectset = cmds.ls("*.id", long=True, type="objectSet",
recursive=True, objectsOnly=True)
context.data['objectsets'] = objectset
for objset in objectset:
if not cmds.attributeQuery("id", node=objset, exists=True):
continue
id_attr = "{}.id".format(objset)
if cmds.getAttr(id_attr) != "pyblish.avalon.instance":
continue
# The developer is responsible for specifying
# the family of each instance.
has_family = cmds.attributeQuery("family",
node=objset,
exists=True)
assert has_family, "\"%s\" was missing a family" % objset
members = cmds.sets(objset, query=True)
if members is None:
self.log.warning("Skipped empty instance: \"%s\" " % objset)
continue
self.log.info("Creating instance for {}".format(objset))
data = dict()
# Apply each user defined attribute as data
for attr in cmds.listAttr(objset, userDefined=True) or list():
try:
value = cmds.getAttr("%s.%s" % (objset, attr))
except Exception:
# Some attributes cannot be read directly,
# such as mesh and color attributes. These
# are considered non-essential to this
# particular publishing pipeline.
value = None
data[attr] = value
# temporarily translation of `active` to `publish` till issue has
# been resolved, https://github.com/pyblish/pyblish-base/issues/307
if "active" in data:
data["publish"] = data["active"]
# Collect members
members = cmds.ls(members, long=True) or []
# `maya.cmds.listRelatives(noIntermediate=True)` only works when
# `shapes=True` argument is passed, since we also want to include
# transforms we filter afterwards.
children = cmds.listRelatives(members,
allDescendents=True,
fullPath=True) or []
children = cmds.ls(children, noIntermediate=True, long=True)
parents = []
if data.get("includeParentHierarchy", True):
# If `includeParentHierarchy` then include the parents
# so they will also be picked up in the instance by validators
parents = self.get_all_parents(members)
members_hierarchy = list(set(members + children + parents))
if 'families' not in data:
data['families'] = [data.get('family')]
# Create the instance
instance = context.create_instance(objset)
instance[:] = members_hierarchy
# Store the exact members of the object set
instance.data["setMembers"] = members
# Define nice label
name = cmds.ls(objset, long=False)[0] # use short name
label = "{0} ({1})".format(name,
data["asset"])
# Append start frame and end frame to label if present
if "frameStart" and "frameEnd" in data:
label += " [{0}-{1}]".format(int(data["frameStart"]),
int(data["frameEnd"]))
instance.data["label"] = label
instance.data.update(data)
# Produce diagnostic message for any graphical
# user interface interested in visualising it.
self.log.info("Found: \"%s\" " % instance.data["name"])
self.log.debug("DATA: \"%s\" " % instance.data)
def sort_by_family(instance):
"""Sort by family"""
return instance.data.get("families", instance.data.get("family"))
# Sort/grouped by family (preserving local index)
context[:] = sorted(context, key=sort_by_family)
return context
def get_all_parents(self, nodes):
"""Get all parents by using string operations (optimization)
Args:
nodes (list): the nodes which are found in the objectSet
Returns:
list
"""
parents = []
for node in nodes:
splitted = node.split("|")
items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))]
parents.extend(items)
return list(set(parents))

View file

@ -0,0 +1,45 @@
import os
import logging
from avalon import api as avalon
from pyblish import api as pyblish
logger = logging.getLogger("pype.unreal")
PARENT_DIR = os.path.dirname(__file__)
PACKAGE_DIR = os.path.dirname(PARENT_DIR)
PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "unreal", "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "unreal", "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "unreal", "create")
def install():
"""Install Unreal configuration for Avalon."""
print("-=" * 40)
logo = '''.
.
____________
/ \\ __ \\
\\ \\ \\/_\\ \\
\\ \\ _____/ ______
\\ \\ \\___// \\ \\
\\ \\____\\ \\ \\_____\\
\\/_____/ \\/______/ PYPE Club .
.
'''
print(logo)
print("installing Pype for Unreal ...")
print("-=" * 40)
logger.info("installing Pype for Unreal")
pyblish.register_plugin_path(str(PUBLISH_PATH))
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
def uninstall():
"""Uninstall Unreal configuration for Avalon."""
pyblish.deregister_plugin_path(str(PUBLISH_PATH))
avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH))

9
pype/unreal/plugin.py Normal file
View file

@ -0,0 +1,9 @@
from avalon import api
class Creator(api.Creator):
pass
class Loader(api.Loader):
pass