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

Feature/pype 617 ue basic integration #3

Merged
merged 15 commits into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]
# ignore = D203
ignore = BLK100, W504
ignore = BLK100, W504, W503
max-line-length = 79
exclude =
.git,
Expand Down
25 changes: 14 additions & 11 deletions pype/ftrack/lib/ftrack_app_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,6 @@ def launch(self, session, entities, event):
env = acre.merge(env, current_env=dict(os.environ))
env = acre.append(dict(os.environ), env)


#
# tools_env = acre.get_tools(tools)
# env = acre.compute(dict(tools_env))
# env = acre.merge(env, dict(os.environ))
# os.environ = acre.append(dict(os.environ), env)
# os.environ = acre.compute(os.environ)

# Get path to execute
st_temp_path = os.environ['PYPE_CONFIG']
os_plat = platform.system().lower()
Expand All @@ -275,6 +267,18 @@ def launch(self, session, entities, event):
# Full path to executable launcher
execfile = None

if application.get("launch_hook"):
hook = application.get("launch_hook")
self.log.info("launching hook: {}".format(hook))
ret_val = pypelib.execute_hook(
application.get("launch_hook"), env=env)
if not ret_val:
return {
'success': False,
'message': "Hook didn't finish successfully {0}"
.format(self.label)
}

if sys.platform == "win32":

for ext in os.environ["PATHEXT"].split(os.pathsep):
Expand All @@ -286,16 +290,15 @@ def launch(self, session, entities, event):

# Run SW if was found executable
if execfile is not None:
popen = avalonlib.launch(
avalonlib.launch(
executable=execfile, args=[], environment=env
)
else:
return {
'success': False,
'message': "We didn't found launcher for {0}"
.format(self.label)
}
pass
}

if sys.platform.startswith('linux'):
execfile = os.path.join(path.strip('"'), self.executable)
Expand Down
83 changes: 83 additions & 0 deletions pype/hooks/unreal/unreal_prelaunch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import logging
import os

from pype.lib import PypeHook
from pype.unreal import lib as unreal_lib
from pypeapp import Logger

log = logging.getLogger(__name__)


class UnrealPrelaunch(PypeHook):
"""
This hook will check if current workfile path has Unreal
project inside. IF not, it initialize it and finally it pass
path to the project by environment variable to Unreal launcher
shell script.
"""

def __init__(self, logger=None):
if not logger:
self.log = Logger().get_logger(self.__class__.__name__)
else:
self.log = logger

self.signature = "( {} )".format(self.__class__.__name__)

def execute(self, *args, env: dict = None) -> bool:
if not env:
env = os.environ
asset = env["AVALON_ASSET"]
task = env["AVALON_TASK"]
workdir = env["AVALON_WORKDIR"]
engine_version = env["AVALON_APP_NAME"].split("_")[-1]
project_name = f"{asset}_{task}"

# 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})!"))

# Unreal doesn't accept non alphabet characters at the start
# of the project name. This is because project name is then used
# in various places inside c++ code and there variable names cannot
# start with non-alpha. We append 'P' before project name to solve it.
# 😱
if not project_name[:1].isalpha():
self.log.warning(f"Project name doesn't start with alphabet "
f"character ({project_name}). Appending 'P'")
project_name = f"P{project_name}"

project_path = os.path.join(workdir, project_name)

self.log.info((f"{self.signature} requested UE4 version: "
f"[ {engine_version} ]"))

detected = unreal_lib.get_engine_versions()
detected_str = ', '.join(detected.keys()) or 'none'
self.log.info((f"{self.signature} detected UE4 versions: "
f"[ {detected_str} ]"))
del(detected_str)
engine_version = ".".join(engine_version.split(".")[:2])
if engine_version not in detected.keys():
self.log.error((f"{self.signature} requested version not "
f"detected [ {engine_version} ]"))
return False

os.makedirs(project_path, exist_ok=True)

project_file = os.path.join(project_path, f"{project_name}.uproject")
engine_path = detected[engine_version]
if not os.path.isfile(project_file):
self.log.info((f"{self.signature} creating unreal "
f"project [ {project_name} ]"))
if env.get("AVALON_UNREAL_PLUGIN"):
os.environ["AVALON_UNREAL_PLUGIN"] = env.get("AVALON_UNREAL_PLUGIN") # noqa: E501
unreal_lib.create_unreal_project(project_name,
engine_version,
project_path,
engine_path=engine_path)

env["PYPE_UNREAL_PROJECT_FILE"] = project_file
env["AVALON_CURRENT_UNREAL_ENGINE"] = engine_path
return True
79 changes: 72 additions & 7 deletions pype/lib.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import os
import sys
import types
import re
import logging
import itertools
import contextlib
import subprocess
import inspect
from abc import ABCMeta, abstractmethod

import six

from avalon import io
import avalon.api
Expand Down Expand Up @@ -177,7 +182,8 @@ def modified_environ(*remove, **update):
is sure to work in all situations.

:param remove: Environment variables to remove.
:param update: Dictionary of environment variables and values to add/update.
:param update: Dictionary of environment variables
and values to add/update.
"""
env = os.environ
update = update or {}
Expand Down Expand Up @@ -403,8 +409,8 @@ def switch_item(container,
"parent": version["_id"]}
)

assert representation, ("Could not find representation in the database with"
" the name '%s'" % representation_name)
assert representation, ("Could not find representation in the database "
"with the name '%s'" % representation_name)

avalon.api.switch(container, representation)

Expand Down Expand Up @@ -537,7 +543,9 @@ def get_subsets(asset_name,
"""
Query subsets with filter on name.

The method will return all found subsets and its defined version and subsets. Version could be specified with number. Representation can be filtered.
The method will return all found subsets and its defined version
and subsets. Version could be specified with number. Representation
can be filtered.

Arguments:
asset_name (str): asset (shot) name
Expand All @@ -554,8 +562,8 @@ def get_subsets(asset_name,
asset_io = io.find_one({"type": "asset", "name": asset_name})

# check if anything returned
assert asset_io, "Asset not existing. \
Check correct name: `{}`".format(asset_name)
assert asset_io, (
"Asset not existing. Check correct name: `{}`").format(asset_name)

# create subsets query filter
filter_query = {"type": "subset", "parent": asset_io["_id"]}
Expand All @@ -569,7 +577,9 @@ def get_subsets(asset_name,
# query all assets
subsets = [s for s in io.find(filter_query)]

assert subsets, "No subsets found. Check correct filter. Try this for start `r'.*'`: asset: `{}`".format(asset_name)
assert subsets, ("No subsets found. Check correct filter. "
"Try this for start `r'.*'`: "
"asset: `{}`").format(asset_name)

output_dict = {}
# Process subsets
Expand Down Expand Up @@ -643,3 +653,58 @@ def __str__(self):
def __repr__(self):
"""Representation of custom None."""
return "<CustomNone-{}>".format(str(self.identifier))


def execute_hook(hook, *args, **kwargs):
"""
This will load hook file, instantiate class and call `execute` method
on it. Hook must be in a form:

`$PYPE_ROOT/repos/pype/path/to/hook.py/HookClass`

This will load `hook.py`, instantiate HookClass and then execute_hook
`execute(*args, **kwargs)`

:param hook: path to hook class
:type hook: str
"""

class_name = hook.split("/")[-1]

abspath = os.path.join(os.getenv('PYPE_ROOT'),
'repos', 'pype', *hook.split("/")[:-1])

mod_name, mod_ext = os.path.splitext(os.path.basename(abspath))

if not mod_ext == ".py":
return False

module = types.ModuleType(mod_name)
module.__file__ = abspath

try:
with open(abspath) as f:
six.exec_(f.read(), module.__dict__)

sys.modules[abspath] = module

except Exception as exp:
log.exception("loading hook failed: {}".format(exp),
exc_info=True)
return False

obj = getattr(module, class_name)
hook_obj = obj()
ret_val = hook_obj.execute(*args, **kwargs)
return ret_val


@six.add_metaclass(ABCMeta)
class PypeHook:

def __init__(self):
pass

@abstractmethod
def execute(self, *args, **kwargs):
pass
3 changes: 3 additions & 0 deletions pype/plugins/global/publish/collect_scene_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def process(self, context):
if "standalonepublisher" in context.data.get("host", []):
return

if "unreal" in pyblish.api.registered_hosts():
return

filename = os.path.basename(context.data.get('currentFile'))

if '<shell>' in filename:
Expand Down
3 changes: 2 additions & 1 deletion pype/plugins/global/publish/integrate_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"image"
"source",
"assembly",
"textures"
"fbx",
"textures",
"action"
]
exclude_families = ["clip"]
Expand Down
11 changes: 11 additions & 0 deletions pype/plugins/maya/create/create_unreal_staticmesh.py
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions pype/plugins/maya/publish/collect_unreal_staticmesh.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 2 additions & 3 deletions pype/plugins/maya/publish/extract_fbx.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,11 @@ def process(self, instance):
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))
33 changes: 33 additions & 0 deletions pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py
Original file line number Diff line number Diff line change
@@ -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")
Loading