mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 13:52:15 +01:00
publishing unreal static mesh from maya
This commit is contained in:
parent
05ea8b14b9
commit
9a8655be1d
14 changed files with 503 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"matchmove",
|
||||
"image"
|
||||
"source",
|
||||
"assembly"
|
||||
"assembly",
|
||||
"fbx"
|
||||
]
|
||||
exclude_families = ["clip"]
|
||||
db_representation_context_keys = [
|
||||
|
|
|
|||
11
pype/plugins/maya/create/create_unreal_staticmesh.py
Normal file
11
pype/plugins/maya/create/create_unreal_staticmesh.py
Normal 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)
|
||||
33
pype/plugins/maya/publish/collect_unreal_staticmesh.py
Normal file
33
pype/plugins/maya/publish/collect_unreal_staticmesh.py
Normal 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
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
120
pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py
Normal file
120
pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py
Normal 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.")
|
||||
25
pype/plugins/maya/publish/validate_unreal_up_axis.py
Normal file
25
pype/plugins/maya/publish/validate_unreal_up_axis.py
Normal 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)
|
||||
14
pype/plugins/unreal/create/create_fbx.py
Normal file
14
pype/plugins/unreal/create/create_fbx.py
Normal 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)
|
||||
53
pype/plugins/unreal/load/load_staticmeshfbx.py
Normal file
53
pype/plugins/unreal/load/load_staticmeshfbx.py
Normal 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
|
||||
152
pype/plugins/unreal/publish/collect_instances.py
Normal file
152
pype/plugins/unreal/publish/collect_instances.py
Normal 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))
|
||||
|
|
@ -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
9
pype/unreal/plugin.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from avalon import api
|
||||
|
||||
|
||||
class Creator(api.Creator):
|
||||
pass
|
||||
|
||||
|
||||
class Loader(api.Loader):
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue