mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge pull request #3 from pypeclub/feature/PYPE-617-UE-basic-integration
Feature/pype 617 ue basic integration
This commit is contained in:
commit
b74b651310
19 changed files with 1073 additions and 23 deletions
2
.flake8
2
.flake8
|
|
@ -1,6 +1,6 @@
|
|||
[flake8]
|
||||
# ignore = D203
|
||||
ignore = BLK100, W504
|
||||
ignore = BLK100, W504, W503
|
||||
max-line-length = 79
|
||||
exclude =
|
||||
.git,
|
||||
|
|
|
|||
|
|
@ -258,14 +258,6 @@ class AppAction(BaseHandler):
|
|||
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()
|
||||
|
|
@ -275,6 +267,18 @@ class AppAction(BaseHandler):
|
|||
# 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):
|
||||
|
|
@ -286,7 +290,7 @@ class AppAction(BaseHandler):
|
|||
|
||||
# Run SW if was found executable
|
||||
if execfile is not None:
|
||||
popen = avalonlib.launch(
|
||||
avalonlib.launch(
|
||||
executable=execfile, args=[], environment=env
|
||||
)
|
||||
else:
|
||||
|
|
@ -294,8 +298,7 @@ class AppAction(BaseHandler):
|
|||
'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)
|
||||
|
|
|
|||
83
pype/hooks/unreal/unreal_prelaunch.py
Normal file
83
pype/hooks/unreal/unreal_prelaunch.py
Normal file
|
|
@ -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
pype/lib.py
79
pype/lib.py
|
|
@ -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
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]}
|
||||
|
|
@ -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
|
||||
|
|
@ -643,3 +653,58 @@ class CustomNone:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"image"
|
||||
"source",
|
||||
"assembly",
|
||||
"textures"
|
||||
"fbx",
|
||||
"textures",
|
||||
"action"
|
||||
]
|
||||
exclude_families = ["clip"]
|
||||
|
|
|
|||
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)
|
||||
33
pype/plugins/unreal/create/create_staticmeshfbx.py
Normal file
33
pype/plugins/unreal/create/create_staticmeshfbx.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import unreal
|
||||
from pype.unreal.plugin import Creator
|
||||
from avalon.unreal import (
|
||||
instantiate,
|
||||
)
|
||||
|
||||
|
||||
class CreateStaticMeshFBX(Creator):
|
||||
"""Static FBX geometry"""
|
||||
|
||||
name = "unrealStaticMeshMain"
|
||||
label = "Unreal - Static Mesh"
|
||||
family = "unrealStaticMesh"
|
||||
icon = "cube"
|
||||
asset_types = ["StaticMesh"]
|
||||
|
||||
root = "/Game"
|
||||
suffix = "_INS"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateStaticMeshFBX, self).__init__(*args, **kwargs)
|
||||
|
||||
def process(self):
|
||||
|
||||
name = self.data["subset"]
|
||||
|
||||
selection = []
|
||||
if (self.options or {}).get("useSelection"):
|
||||
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
selection = [a.get_path_name() for a in sel_objects]
|
||||
|
||||
unreal.log("selection: {}".format(selection))
|
||||
instantiate(self.root, name, self.data, selection, self.suffix)
|
||||
101
pype/plugins/unreal/load/load_staticmeshfbx.py
Normal file
101
pype/plugins/unreal/load/load_staticmeshfbx.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
from avalon import api
|
||||
from avalon import unreal as avalon_unreal
|
||||
import unreal
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Load and containerise representation into Content Browser.
|
||||
|
||||
This is two step process. First, import FBX to temporary path and
|
||||
then call `containerise()` on it - this moves all content to new
|
||||
directory and then it will create AssetContainer there and imprint it
|
||||
with metadata. This will mark this path as container.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): subset name
|
||||
namespace (str): in Unreal this is basically path to container.
|
||||
This is not passed here, so namespace is set
|
||||
by `containerise()` because only then we know
|
||||
real path.
|
||||
data (dict): Those would be data to be imprinted. This is not used
|
||||
now, data are imprinted by `containerise()`.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
temp_dir, temp_name = tools.create_unique_asset_name(
|
||||
"/Game/{}".format(name), "_TMP"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def update(self, container, representation):
|
||||
node = container["objectName"]
|
||||
source_path = api.get_representation_path(representation)
|
||||
destination_path = container["namespace"]
|
||||
|
||||
task = unreal.AssetImportTask()
|
||||
|
||||
task.filename = source_path
|
||||
task.destination_path = destination_path
|
||||
# strip suffix
|
||||
task.destination_name = node[:-4]
|
||||
task.replace_existing = True
|
||||
task.automated = True
|
||||
task.save = True
|
||||
|
||||
task.options = unreal.FbxImportUI()
|
||||
task.options.import_animations = False
|
||||
|
||||
# do import fbx and replace existing data
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
container_path = "{}/{}".format(container["namespace"],
|
||||
container["objectName"])
|
||||
# update metadata
|
||||
avalon_unreal.imprint(
|
||||
container_path, {"_id": str(representation["_id"])})
|
||||
|
||||
def remove(self, container):
|
||||
unreal.EditorAssetLibrary.delete_directory(container["namespace"])
|
||||
59
pype/plugins/unreal/publish/collect_instances.py
Normal file
59
pype/plugins/unreal/publish/collect_instances.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import unreal
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Gather instances by AvalonPublishInstance class
|
||||
|
||||
This collector finds all paths containing `AvalonPublishInstance` class
|
||||
asset
|
||||
|
||||
Identifier:
|
||||
id (str): "pyblish.avalon.instance"
|
||||
|
||||
"""
|
||||
|
||||
label = "Collect Instances"
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["unreal"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
instance_containers = ar.get_assets_by_class(
|
||||
"AvalonPublishInstance", True)
|
||||
|
||||
for container_data in instance_containers:
|
||||
asset = container_data.get_asset()
|
||||
data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)
|
||||
data["objectName"] = container_data.asset_name
|
||||
# convert to strings
|
||||
data = {str(key): str(value) for (key, value) in data.items()}
|
||||
assert data.get("family"), (
|
||||
"instance has no family"
|
||||
)
|
||||
|
||||
# content of container
|
||||
members = unreal.EditorAssetLibrary.list_assets(
|
||||
asset.get_path_name(), recursive=True, include_folder=True
|
||||
)
|
||||
self.log.debug(members)
|
||||
self.log.debug(asset.get_path_name())
|
||||
# remove instance container
|
||||
members.remove(asset.get_path_name())
|
||||
self.log.info("Creating instance for {}".format(asset.get_name()))
|
||||
|
||||
instance = context.create_instance(asset.get_name())
|
||||
instance[:] = members
|
||||
|
||||
# Store the exact members of the object set
|
||||
instance.data["setMembers"] = members
|
||||
instance.data["families"] = [data.get("family")]
|
||||
|
||||
label = "{0} ({1})".format(asset.get_name()[:-4],
|
||||
data["asset"])
|
||||
|
||||
instance.data["label"] = label
|
||||
|
||||
instance.data.update(data)
|
||||
45
pype/unreal/__init__.py
Normal file
45
pype/unreal/__init__.py
Normal 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))
|
||||
425
pype/unreal/lib.py
Normal file
425
pype/unreal/lib.py
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
import sys
|
||||
import os
|
||||
import platform
|
||||
import json
|
||||
from distutils import dir_util
|
||||
import subprocess
|
||||
from pypeapp import config
|
||||
|
||||
|
||||
def get_engine_versions():
|
||||
"""
|
||||
This will try to detect location and versions of installed Unreal Engine.
|
||||
Location can be overridden by `UNREAL_ENGINE_LOCATION` environment
|
||||
variable.
|
||||
|
||||
Returns:
|
||||
|
||||
dict: dictionary with version as a key and dir as value.
|
||||
|
||||
Example:
|
||||
|
||||
>>> get_engine_version()
|
||||
{
|
||||
"4.23": "C:/Epic Games/UE_4.23",
|
||||
"4.24": "C:/Epic Games/UE_4.24"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
engine_locations = {}
|
||||
root, dirs, files = next(os.walk(os.environ["UNREAL_ENGINE_LOCATION"]))
|
||||
|
||||
for dir in dirs:
|
||||
if dir.startswith("UE_"):
|
||||
ver = dir.split("_")[1]
|
||||
engine_locations[ver] = os.path.join(root, dir)
|
||||
except KeyError:
|
||||
# environment variable not set
|
||||
pass
|
||||
except OSError:
|
||||
# specified directory doesn't exists
|
||||
pass
|
||||
|
||||
# if we've got something, terminate autodetection process
|
||||
if engine_locations:
|
||||
return engine_locations
|
||||
|
||||
# else kick in platform specific detection
|
||||
if platform.system().lower() == "windows":
|
||||
return _win_get_engine_versions()
|
||||
elif platform.system().lower() == "linux":
|
||||
# on linux, there is no installation and getting Unreal Engine involves
|
||||
# git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`.
|
||||
pass
|
||||
elif platform.system().lower() == "darwin":
|
||||
return _darwin_get_engine_version()
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _win_get_engine_versions():
|
||||
"""
|
||||
If engines are installed via Epic Games Launcher then there is:
|
||||
`%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat`
|
||||
This file is JSON file listing installed stuff, Unreal engines
|
||||
are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24`
|
||||
"""
|
||||
install_json_path = os.path.join(
|
||||
os.environ.get("PROGRAMDATA"),
|
||||
"Epic",
|
||||
"UnrealEngineLauncher",
|
||||
"LauncherInstalled.dat",
|
||||
)
|
||||
|
||||
return _parse_launcher_locations(install_json_path)
|
||||
|
||||
|
||||
def _darwin_get_engine_version() -> dict:
|
||||
"""
|
||||
It works the same as on Windows, just JSON file location is different.
|
||||
"""
|
||||
install_json_path = os.path.join(
|
||||
os.environ.get("HOME"),
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Epic",
|
||||
"UnrealEngineLauncher",
|
||||
"LauncherInstalled.dat",
|
||||
)
|
||||
|
||||
return _parse_launcher_locations(install_json_path)
|
||||
|
||||
|
||||
def _parse_launcher_locations(install_json_path: str) -> dict:
|
||||
"""
|
||||
This will parse locations from json file.
|
||||
|
||||
:param install_json_path: path to `LauncherInstalled.dat`
|
||||
:type install_json_path: str
|
||||
:returns: returns dict with unreal engine versions as keys and
|
||||
paths to those engine installations as value.
|
||||
:rtype: dict
|
||||
"""
|
||||
engine_locations = {}
|
||||
if os.path.isfile(install_json_path):
|
||||
with open(install_json_path, "r") as ilf:
|
||||
try:
|
||||
install_data = json.load(ilf)
|
||||
except json.JSONDecodeError:
|
||||
raise Exception(
|
||||
"Invalid `LauncherInstalled.dat file. `"
|
||||
"Cannot determine Unreal Engine location."
|
||||
)
|
||||
|
||||
for installation in install_data.get("InstallationList", []):
|
||||
if installation.get("AppName").startswith("UE_"):
|
||||
ver = installation.get("AppName").split("_")[1]
|
||||
engine_locations[ver] = installation.get("InstallLocation")
|
||||
|
||||
return engine_locations
|
||||
|
||||
|
||||
def create_unreal_project(project_name: str,
|
||||
ue_version: str,
|
||||
pr_dir: str,
|
||||
engine_path: str,
|
||||
dev_mode: bool = False) -> None:
|
||||
"""
|
||||
This will create `.uproject` file at specified location. As there is no
|
||||
way I know to create project via command line, this is easiest option.
|
||||
Unreal project file is basically JSON file. If we find
|
||||
`AVALON_UNREAL_PLUGIN` environment variable we assume this is location
|
||||
of Avalon Integration Plugin and we copy its content to project folder
|
||||
and enable this plugin.
|
||||
|
||||
:param project_name: project name
|
||||
:type project_name: str
|
||||
:param ue_version: unreal engine version (like 4.23)
|
||||
:type ue_version: str
|
||||
:param pr_dir: path to directory where project will be created
|
||||
:type pr_dir: str
|
||||
:param engine_path: Path to Unreal Engine installation
|
||||
:type engine_path: str
|
||||
:param dev_mode: Flag to trigger C++ style Unreal project needing
|
||||
Visual Studio and other tools to compile plugins from
|
||||
sources. This will trigger automatically if `Binaries`
|
||||
directory is not found in plugin folders as this indicates
|
||||
this is only source distribution of the plugin. Dev mode
|
||||
is also set by preset file `unreal/project_setup.json` in
|
||||
**PYPE_CONFIG**.
|
||||
:type dev_mode: bool
|
||||
:returns: None
|
||||
"""
|
||||
preset = config.get_presets()["unreal"]["project_setup"]
|
||||
|
||||
if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")):
|
||||
# copy plugin to correct path under project
|
||||
plugins_path = os.path.join(pr_dir, "Plugins")
|
||||
avalon_plugin_path = os.path.join(plugins_path, "Avalon")
|
||||
if not os.path.isdir(avalon_plugin_path):
|
||||
os.makedirs(avalon_plugin_path, exist_ok=True)
|
||||
dir_util._path_created = {}
|
||||
dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"),
|
||||
avalon_plugin_path)
|
||||
|
||||
if (not os.path.isdir(os.path.join(avalon_plugin_path, "Binaries"))
|
||||
or not os.path.join(avalon_plugin_path, "Intermediate")):
|
||||
dev_mode = True
|
||||
|
||||
# data for project file
|
||||
data = {
|
||||
"FileVersion": 3,
|
||||
"EngineAssociation": ue_version,
|
||||
"Category": "",
|
||||
"Description": "",
|
||||
"Plugins": [
|
||||
{"Name": "PythonScriptPlugin", "Enabled": True},
|
||||
{"Name": "EditorScriptingUtilities", "Enabled": True},
|
||||
{"Name": "Avalon", "Enabled": True}
|
||||
]
|
||||
}
|
||||
|
||||
if preset["install_unreal_python_engine"]:
|
||||
# If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to
|
||||
# support offline installation.
|
||||
# Otherwise clone UnrealEnginePython to Plugins directory
|
||||
# https://github.com/20tab/UnrealEnginePython.git
|
||||
uep_path = os.path.join(plugins_path, "UnrealEnginePython")
|
||||
if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"):
|
||||
|
||||
os.makedirs(uep_path, exist_ok=True)
|
||||
dir_util._path_created = {}
|
||||
dir_util.copy_tree(
|
||||
os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"),
|
||||
uep_path)
|
||||
else:
|
||||
# WARNING: this will trigger dev_mode, because we need to compile
|
||||
# this plugin.
|
||||
dev_mode = True
|
||||
import git
|
||||
git.Repo.clone_from(
|
||||
"https://github.com/20tab/UnrealEnginePython.git",
|
||||
uep_path)
|
||||
|
||||
data["Plugins"].append(
|
||||
{"Name": "UnrealEnginePython", "Enabled": True})
|
||||
|
||||
if (not os.path.isdir(os.path.join(uep_path, "Binaries"))
|
||||
or not os.path.join(uep_path, "Intermediate")):
|
||||
dev_mode = True
|
||||
|
||||
if dev_mode or preset["dev_mode"]:
|
||||
# this will add project module and necessary source file to make it
|
||||
# C++ project and to (hopefully) make Unreal Editor to compile all
|
||||
# sources at start
|
||||
|
||||
data["Modules"] = [{
|
||||
"Name": project_name,
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"AdditionalDependencies": ["Engine"],
|
||||
}]
|
||||
|
||||
if preset["install_unreal_python_engine"]:
|
||||
# now we need to fix python path in:
|
||||
# `UnrealEnginePython.Build.cs`
|
||||
# to point to our python
|
||||
with open(os.path.join(
|
||||
uep_path, "Source",
|
||||
"UnrealEnginePython",
|
||||
"UnrealEnginePython.Build.cs"), mode="r") as f:
|
||||
build_file = f.read()
|
||||
|
||||
fix = build_file.replace(
|
||||
'private string pythonHome = "";',
|
||||
'private string pythonHome = "{}";'.format(
|
||||
sys.base_prefix.replace("\\", "/")))
|
||||
|
||||
with open(os.path.join(
|
||||
uep_path, "Source",
|
||||
"UnrealEnginePython",
|
||||
"UnrealEnginePython.Build.cs"), mode="w") as f:
|
||||
f.write(fix)
|
||||
|
||||
# write project file
|
||||
project_file = os.path.join(pr_dir, "{}.uproject".format(project_name))
|
||||
with open(project_file, mode="w") as pf:
|
||||
json.dump(data, pf, indent=4)
|
||||
|
||||
# ensure we have PySide installed in engine
|
||||
# TODO: make it work for other platforms 🍎 🐧
|
||||
if platform.system().lower() == "windows":
|
||||
python_path = os.path.join(engine_path, "Engine", "Binaries",
|
||||
"ThirdParty", "Python", "Win64",
|
||||
"python.exe")
|
||||
|
||||
subprocess.run([python_path, "-m",
|
||||
"pip", "install", "pyside"])
|
||||
|
||||
if dev_mode or preset["dev_mode"]:
|
||||
_prepare_cpp_project(project_file, engine_path)
|
||||
|
||||
|
||||
def _prepare_cpp_project(project_file: str, engine_path: str) -> None:
|
||||
"""
|
||||
This function will add source files needed for project to be
|
||||
rebuild along with the avalon integration plugin.
|
||||
|
||||
There seems not to be automated way to do it from command line.
|
||||
But there might be way to create at least those target and build files
|
||||
by some generator. This needs more research as manually writing
|
||||
those files is rather hackish. :skull_and_crossbones:
|
||||
|
||||
:param project_file: path to .uproject file
|
||||
:type project_file: str
|
||||
:param engine_path: path to unreal engine associated with project
|
||||
:type engine_path: str
|
||||
"""
|
||||
|
||||
project_name = os.path.splitext(os.path.basename(project_file))[0]
|
||||
project_dir = os.path.dirname(project_file)
|
||||
targets_dir = os.path.join(project_dir, "Source")
|
||||
sources_dir = os.path.join(targets_dir, project_name)
|
||||
|
||||
os.makedirs(sources_dir, exist_ok=True)
|
||||
os.makedirs(os.path.join(project_dir, "Content"), exist_ok=True)
|
||||
|
||||
module_target = '''
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class {0}Target : TargetRules
|
||||
{{
|
||||
public {0}Target( TargetInfo Target) : base(Target)
|
||||
{{
|
||||
Type = TargetType.Game;
|
||||
ExtraModuleNames.AddRange( new string[] {{ "{0}" }} );
|
||||
}}
|
||||
}}
|
||||
'''.format(project_name)
|
||||
|
||||
editor_module_target = '''
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class {0}EditorTarget : TargetRules
|
||||
{{
|
||||
public {0}EditorTarget( TargetInfo Target) : base(Target)
|
||||
{{
|
||||
Type = TargetType.Editor;
|
||||
|
||||
ExtraModuleNames.AddRange( new string[] {{ "{0}" }} );
|
||||
}}
|
||||
}}
|
||||
'''.format(project_name)
|
||||
|
||||
module_build = '''
|
||||
using UnrealBuildTool;
|
||||
public class {0} : ModuleRules
|
||||
{{
|
||||
public {0}(ReadOnlyTargetRules Target) : base(Target)
|
||||
{{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
PublicDependencyModuleNames.AddRange(new string[] {{ "Core",
|
||||
"CoreUObject", "Engine", "InputCore" }});
|
||||
PrivateDependencyModuleNames.AddRange(new string[] {{ }});
|
||||
}}
|
||||
}}
|
||||
'''.format(project_name)
|
||||
|
||||
module_cpp = '''
|
||||
#include "{0}.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, {0}, "{0}" );
|
||||
'''.format(project_name)
|
||||
|
||||
module_header = '''
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
'''
|
||||
|
||||
game_mode_cpp = '''
|
||||
#include "{0}GameModeBase.h"
|
||||
'''.format(project_name)
|
||||
|
||||
game_mode_h = '''
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "{0}GameModeBase.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class {1}_API A{0}GameModeBase : public AGameModeBase
|
||||
{{
|
||||
GENERATED_BODY()
|
||||
}};
|
||||
'''.format(project_name, project_name.upper())
|
||||
|
||||
with open(os.path.join(
|
||||
targets_dir, f"{project_name}.Target.cs"), mode="w") as f:
|
||||
f.write(module_target)
|
||||
|
||||
with open(os.path.join(
|
||||
targets_dir, f"{project_name}Editor.Target.cs"), mode="w") as f:
|
||||
f.write(editor_module_target)
|
||||
|
||||
with open(os.path.join(
|
||||
sources_dir, f"{project_name}.Build.cs"), mode="w") as f:
|
||||
f.write(module_build)
|
||||
|
||||
with open(os.path.join(
|
||||
sources_dir, f"{project_name}.cpp"), mode="w") as f:
|
||||
f.write(module_cpp)
|
||||
|
||||
with open(os.path.join(
|
||||
sources_dir, f"{project_name}.h"), mode="w") as f:
|
||||
f.write(module_header)
|
||||
|
||||
with open(os.path.join(
|
||||
sources_dir, f"{project_name}GameModeBase.cpp"), mode="w") as f:
|
||||
f.write(game_mode_cpp)
|
||||
|
||||
with open(os.path.join(
|
||||
sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f:
|
||||
f.write(game_mode_h)
|
||||
|
||||
if platform.system().lower() == "windows":
|
||||
u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/"
|
||||
"UnrealBuildTool.exe")
|
||||
u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/"
|
||||
f"UnrealHeaderTool.exe")
|
||||
elif platform.system().lower() == "linux":
|
||||
# WARNING: there is no UnrealBuildTool on linux?
|
||||
u_build_tool = ""
|
||||
u_header_tool = ""
|
||||
elif platform.system().lower() == "darwin":
|
||||
# WARNING: there is no UnrealBuildTool on Mac?
|
||||
u_build_tool = ""
|
||||
u_header_tool = ""
|
||||
|
||||
u_build_tool = u_build_tool.replace("\\", "/")
|
||||
u_header_tool = u_header_tool.replace("\\", "/")
|
||||
|
||||
command1 = [u_build_tool, "-projectfiles", f"-project={project_file}",
|
||||
"-progress"]
|
||||
|
||||
subprocess.run(command1)
|
||||
|
||||
command2 = [u_build_tool, f"-ModuleWithSuffix={project_name},3555"
|
||||
"Win64", "Development", "-TargetType=Editor"
|
||||
f'-Project="{project_file}"', f'"{project_file}"'
|
||||
"-IgnoreJunk"]
|
||||
|
||||
subprocess.run(command2)
|
||||
|
||||
"""
|
||||
uhtmanifest = os.path.join(os.path.dirname(project_file),
|
||||
f"{project_name}.uhtmanifest")
|
||||
|
||||
command3 = [u_header_tool, f'"{project_file}"', f'"{uhtmanifest}"',
|
||||
"-Unattended", "-WarningsAsErrors", "-installed"]
|
||||
|
||||
subprocess.run(command3)
|
||||
"""
|
||||
11
pype/unreal/plugin.py
Normal file
11
pype/unreal/plugin.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from avalon import api
|
||||
|
||||
|
||||
class Creator(api.Creator):
|
||||
"""This serves as skeleton for future Pype specific functionality"""
|
||||
pass
|
||||
|
||||
|
||||
class Loader(api.Loader):
|
||||
"""This serves as skeleton for future Pype specific functionality"""
|
||||
pass
|
||||
BIN
res/app_icons/ue4.png
Normal file
BIN
res/app_icons/ue4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Loading…
Add table
Add a link
Reference in a new issue