Merge branch 'develop' into feature/AY-742_copy-to-breakdown-project
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "client/ayon_core/hosts/unreal/integration"]
|
||||
path = client/ayon_core/hosts/unreal/integration
|
||||
url = https://github.com/ynput/ayon-unreal-plugin.git
|
||||
|
|
@ -35,14 +35,14 @@ AYON addons should contain separated logic of specific kind of implementation, s
|
|||
- addon has more logic when used in a tray
|
||||
- it is possible that addon can be used only in the tray
|
||||
- abstract methods
|
||||
- `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_addons`
|
||||
- `tray_init` - initialization triggered after `initialize` when used in `TrayAddonsManager` and before `connect_with_addons`
|
||||
- `tray_menu` - add actions to tray widget's menu that represent the addon
|
||||
- `tray_start` - start of addon's login in tray
|
||||
- addon is initialized and connected with other addons
|
||||
- `tray_exit` - addon's cleanup like stop and join threads etc.
|
||||
- order of calling is based on implementation this order is how it works with `TrayModulesManager`
|
||||
- order of calling is based on implementation this order is how it works with `TrayAddonsManager`
|
||||
- it is recommended to import and use GUI implementation only in these methods
|
||||
- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init`
|
||||
- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayAddonsManager` to True after `tray_init`
|
||||
- if addon has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
|
||||
|
||||
### ITrayService
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import inspect
|
|||
import logging
|
||||
import threading
|
||||
import collections
|
||||
|
||||
from uuid import uuid4
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
|
@ -29,32 +28,45 @@ from .interfaces import (
|
|||
)
|
||||
|
||||
# Files that will be always ignored on addons import
|
||||
IGNORED_FILENAMES = (
|
||||
IGNORED_FILENAMES = {
|
||||
"__pycache__",
|
||||
)
|
||||
}
|
||||
# Files ignored on addons import from "./ayon_core/modules"
|
||||
IGNORED_DEFAULT_FILENAMES = (
|
||||
IGNORED_DEFAULT_FILENAMES = {
|
||||
"__init__.py",
|
||||
"base.py",
|
||||
"interfaces.py",
|
||||
"click_wrap.py",
|
||||
"example_addons",
|
||||
"default_modules",
|
||||
)
|
||||
IGNORED_HOSTS_IN_AYON = {
|
||||
"flame",
|
||||
"harmony",
|
||||
}
|
||||
IGNORED_MODULES_IN_AYON = set()
|
||||
|
||||
# When addon was moved from ayon-core codebase
|
||||
# - this is used to log the missing addon
|
||||
MOVED_ADDON_MILESTONE_VERSIONS = {
|
||||
"aftereffects": VersionInfo(0, 2, 0),
|
||||
"applications": VersionInfo(0, 2, 0),
|
||||
"blender": VersionInfo(0, 2, 0),
|
||||
"celaction": VersionInfo(0, 2, 0),
|
||||
"clockify": VersionInfo(0, 2, 0),
|
||||
"deadline": VersionInfo(0, 2, 0),
|
||||
"flame": VersionInfo(0, 2, 0),
|
||||
"fusion": VersionInfo(0, 2, 0),
|
||||
"harmony": VersionInfo(0, 2, 0),
|
||||
"hiero": VersionInfo(0, 2, 0),
|
||||
"max": VersionInfo(0, 2, 0),
|
||||
"photoshop": VersionInfo(0, 2, 0),
|
||||
"timers_manager": VersionInfo(0, 2, 0),
|
||||
"traypublisher": VersionInfo(0, 2, 0),
|
||||
"tvpaint": VersionInfo(0, 2, 0),
|
||||
"maya": VersionInfo(0, 2, 0),
|
||||
"nuke": VersionInfo(0, 2, 0),
|
||||
"resolve": VersionInfo(0, 2, 0),
|
||||
"royalrender": VersionInfo(0, 2, 0),
|
||||
"substancepainter": VersionInfo(0, 2, 0),
|
||||
"houdini": VersionInfo(0, 3, 0),
|
||||
"unreal": VersionInfo(0, 2, 0),
|
||||
}
|
||||
|
||||
|
||||
# Inherit from `object` for Python 2 hosts
|
||||
class _ModuleClass(object):
|
||||
"""Fake module class for storing AYON addons.
|
||||
|
|
@ -399,95 +411,59 @@ def _load_addons_in_core(
|
|||
):
|
||||
# Add current directory at first place
|
||||
# - has small differences in import logic
|
||||
hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts")
|
||||
modules_dir = os.path.join(AYON_CORE_ROOT, "modules")
|
||||
if not os.path.exists(modules_dir):
|
||||
log.warning(
|
||||
f"Could not find path when loading AYON addons \"{modules_dir}\""
|
||||
)
|
||||
return
|
||||
|
||||
ignored_host_names = set(IGNORED_HOSTS_IN_AYON)
|
||||
ignored_module_dir_filenames = (
|
||||
set(IGNORED_DEFAULT_FILENAMES)
|
||||
| IGNORED_MODULES_IN_AYON
|
||||
)
|
||||
ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES
|
||||
|
||||
for dirpath in {hosts_dir, modules_dir}:
|
||||
if not os.path.exists(dirpath):
|
||||
log.warning((
|
||||
"Could not find path when loading AYON addons \"{}\""
|
||||
).format(dirpath))
|
||||
for filename in os.listdir(modules_dir):
|
||||
# Ignore filenames
|
||||
if filename in ignored_filenames:
|
||||
continue
|
||||
|
||||
is_in_modules_dir = dirpath == modules_dir
|
||||
if is_in_modules_dir:
|
||||
ignored_filenames = ignored_module_dir_filenames
|
||||
else:
|
||||
ignored_filenames = ignored_host_names
|
||||
fullpath = os.path.join(modules_dir, filename)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
|
||||
for filename in os.listdir(dirpath):
|
||||
# Ignore filenames
|
||||
if filename in IGNORED_FILENAMES or filename in ignored_filenames:
|
||||
if basename in ignore_addon_names:
|
||||
continue
|
||||
|
||||
# Validations
|
||||
if os.path.isdir(fullpath):
|
||||
# Check existence of init file
|
||||
init_path = os.path.join(fullpath, "__init__.py")
|
||||
if not os.path.exists(init_path):
|
||||
log.debug((
|
||||
"Addon directory does not contain __init__.py"
|
||||
f" file {fullpath}"
|
||||
))
|
||||
continue
|
||||
|
||||
fullpath = os.path.join(dirpath, filename)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
elif ext != ".py":
|
||||
continue
|
||||
|
||||
if basename in ignore_addon_names:
|
||||
continue
|
||||
# TODO add more logic how to define if folder is addon or not
|
||||
# - check manifest and content of manifest
|
||||
try:
|
||||
# Don't import dynamically current directory modules
|
||||
new_import_str = f"{modules_key}.{basename}"
|
||||
|
||||
# Validations
|
||||
if os.path.isdir(fullpath):
|
||||
# Check existence of init file
|
||||
init_path = os.path.join(fullpath, "__init__.py")
|
||||
if not os.path.exists(init_path):
|
||||
log.debug((
|
||||
"Addon directory does not contain __init__.py"
|
||||
" file {}"
|
||||
).format(fullpath))
|
||||
continue
|
||||
import_str = f"ayon_core.modules.{basename}"
|
||||
default_module = __import__(import_str, fromlist=("", ))
|
||||
sys.modules[new_import_str] = default_module
|
||||
setattr(openpype_modules, basename, default_module)
|
||||
|
||||
elif ext not in (".py", ):
|
||||
continue
|
||||
|
||||
# TODO add more logic how to define if folder is addon or not
|
||||
# - check manifest and content of manifest
|
||||
try:
|
||||
# Don't import dynamically current directory modules
|
||||
new_import_str = "{}.{}".format(modules_key, basename)
|
||||
if is_in_modules_dir:
|
||||
import_str = "ayon_core.modules.{}".format(basename)
|
||||
default_module = __import__(import_str, fromlist=("", ))
|
||||
sys.modules[new_import_str] = default_module
|
||||
setattr(openpype_modules, basename, default_module)
|
||||
|
||||
else:
|
||||
import_str = "ayon_core.hosts.{}".format(basename)
|
||||
# Until all hosts are converted to be able use them as
|
||||
# modules is this error check needed
|
||||
try:
|
||||
default_module = __import__(
|
||||
import_str, fromlist=("", )
|
||||
)
|
||||
sys.modules[new_import_str] = default_module
|
||||
setattr(openpype_modules, basename, default_module)
|
||||
|
||||
except Exception:
|
||||
log.warning(
|
||||
"Failed to import host folder {}".format(basename),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
except Exception:
|
||||
if is_in_modules_dir:
|
||||
msg = "Failed to import in-core addon '{}'.".format(
|
||||
basename
|
||||
)
|
||||
else:
|
||||
msg = "Failed to import addon '{}'.".format(fullpath)
|
||||
log.error(msg, exc_info=True)
|
||||
except Exception:
|
||||
log.error(
|
||||
f"Failed to import in-core addon '{basename}'.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
def _load_addons():
|
||||
# Support to use 'openpype' imports
|
||||
sys.modules["openpype"] = sys.modules["ayon_core"]
|
||||
|
||||
# Key under which will be modules imported in `sys.modules`
|
||||
modules_key = "openpype_modules"
|
||||
|
||||
|
|
@ -540,6 +516,9 @@ class AYONAddon(object):
|
|||
enabled = True
|
||||
_id = None
|
||||
|
||||
# Temporary variable for 'version' property
|
||||
_missing_version_warned = False
|
||||
|
||||
def __init__(self, manager, settings):
|
||||
self.manager = manager
|
||||
|
||||
|
|
@ -570,6 +549,26 @@ class AYONAddon(object):
|
|||
|
||||
pass
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""Addon version.
|
||||
|
||||
Todo:
|
||||
Should be abstract property (required). Introduced in
|
||||
ayon-core 0.3.3 .
|
||||
|
||||
Returns:
|
||||
str: Addon version as semver compatible string.
|
||||
|
||||
"""
|
||||
if not self.__class__._missing_version_warned:
|
||||
self.__class__._missing_version_warned = True
|
||||
print(
|
||||
f"DEV WARNING: Addon '{self.name}' does not have"
|
||||
f" defined version."
|
||||
)
|
||||
return "0.0.0"
|
||||
|
||||
def initialize(self, settings):
|
||||
"""Initialization of addon attributes.
|
||||
|
||||
|
|
@ -685,6 +684,30 @@ class OpenPypeAddOn(OpenPypeModule):
|
|||
enabled = True
|
||||
|
||||
|
||||
class _AddonReportInfo:
|
||||
def __init__(
|
||||
self, class_name, name, version, report_value_by_label
|
||||
):
|
||||
self.class_name = class_name
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.report_value_by_label = report_value_by_label
|
||||
|
||||
@classmethod
|
||||
def from_addon(cls, addon, report):
|
||||
class_name = addon.__class__.__name__
|
||||
report_value_by_label = {
|
||||
label: reported.get(class_name)
|
||||
for label, reported in report.items()
|
||||
}
|
||||
return cls(
|
||||
addon.__class__.__name__,
|
||||
addon.name,
|
||||
addon.version,
|
||||
report_value_by_label
|
||||
)
|
||||
|
||||
|
||||
class AddonsManager:
|
||||
"""Manager of addons that helps to load and prepare them to work.
|
||||
|
||||
|
|
@ -861,10 +884,6 @@ class AddonsManager:
|
|||
name_alias = getattr(addon, "openpype_alias", None)
|
||||
if name_alias:
|
||||
aliased_names.append((name_alias, addon))
|
||||
enabled_str = "X"
|
||||
if not addon.enabled:
|
||||
enabled_str = " "
|
||||
self.log.debug("[{}] {}".format(enabled_str, name))
|
||||
|
||||
now = time.time()
|
||||
report[addon.__class__.__name__] = now - prev_start_time
|
||||
|
|
@ -876,6 +895,13 @@ class AddonsManager:
|
|||
exc_info=True
|
||||
)
|
||||
|
||||
for addon_name in sorted(self._addons_by_name.keys()):
|
||||
addon = self._addons_by_name[addon_name]
|
||||
enabled_str = "X" if addon.enabled else " "
|
||||
self.log.debug(
|
||||
f"[{enabled_str}] {addon.name} ({addon.version})"
|
||||
)
|
||||
|
||||
for item in aliased_names:
|
||||
name_alias, addon = item
|
||||
if name_alias not in self._addons_by_name:
|
||||
|
|
@ -1164,39 +1190,55 @@ class AddonsManager:
|
|||
available_col_names |= set(addon_names.keys())
|
||||
|
||||
# Prepare ordered dictionary for columns
|
||||
cols = collections.OrderedDict()
|
||||
# Add addon names to first columnt
|
||||
cols["Addon name"] = list(sorted(
|
||||
addon.__class__.__name__
|
||||
addons_info = [
|
||||
_AddonReportInfo.from_addon(addon, self._report)
|
||||
for addon in self.addons
|
||||
if addon.__class__.__name__ in available_col_names
|
||||
))
|
||||
]
|
||||
addons_info.sort(key=lambda x: x.name)
|
||||
|
||||
addon_name_rows = [
|
||||
addon_info.name
|
||||
for addon_info in addons_info
|
||||
]
|
||||
addon_version_rows = [
|
||||
addon_info.version
|
||||
for addon_info in addons_info
|
||||
]
|
||||
|
||||
# Add total key (as last addon)
|
||||
cols["Addon name"].append(self._report_total_key)
|
||||
addon_name_rows.append(self._report_total_key)
|
||||
addon_version_rows.append(f"({len(addons_info)})")
|
||||
|
||||
cols = collections.OrderedDict()
|
||||
# Add addon names to first columnt
|
||||
cols["Addon name"] = addon_name_rows
|
||||
cols["Version"] = addon_version_rows
|
||||
|
||||
# Add columns from report
|
||||
total_by_addon = {
|
||||
row: 0
|
||||
for row in addon_name_rows
|
||||
}
|
||||
for label in self._report.keys():
|
||||
cols[label] = []
|
||||
|
||||
total_addon_times = {}
|
||||
for addon_name in cols["Addon name"]:
|
||||
total_addon_times[addon_name] = 0
|
||||
|
||||
for label, reported in self._report.items():
|
||||
for addon_name in cols["Addon name"]:
|
||||
col_time = reported.get(addon_name)
|
||||
if col_time is None:
|
||||
cols[label].append("N/A")
|
||||
rows = []
|
||||
col_total = 0
|
||||
for addon_info in addons_info:
|
||||
value = addon_info.report_value_by_label.get(label)
|
||||
if value is None:
|
||||
rows.append("N/A")
|
||||
continue
|
||||
cols[label].append("{:.3f}".format(col_time))
|
||||
total_addon_times[addon_name] += col_time
|
||||
|
||||
rows.append("{:.3f}".format(value))
|
||||
total_by_addon[addon_info.name] += value
|
||||
col_total += value
|
||||
total_by_addon[self._report_total_key] += col_total
|
||||
rows.append("{:.3f}".format(col_total))
|
||||
cols[label] = rows
|
||||
# Add to also total column that should sum the row
|
||||
cols[self._report_total_key] = []
|
||||
for addon_name in cols["Addon name"]:
|
||||
cols[self._report_total_key].append(
|
||||
"{:.3f}".format(total_addon_times[addon_name])
|
||||
)
|
||||
cols[self._report_total_key] = [
|
||||
"{:.3f}".format(total_by_addon[addon_name])
|
||||
for addon_name in cols["Addon name"]
|
||||
]
|
||||
|
||||
# Prepare column widths and total row count
|
||||
# - column width is by
|
||||
|
|
@ -1323,7 +1365,7 @@ class TrayAddonsManager(AddonsManager):
|
|||
self.doubleclick_callback = None
|
||||
|
||||
def add_doubleclick_callback(self, addon, callback):
|
||||
"""Register doubleclick callbacks on tray icon.
|
||||
"""Register double-click callbacks on tray icon.
|
||||
|
||||
Currently, there is no way how to determine which is launched. Name of
|
||||
callback can be defined with `doubleclick_callback` attribute.
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ def main(*args, **kwargs):
|
|||
os.path.join(AYON_CORE_ROOT, "tools"),
|
||||
# add common AYON vendor
|
||||
# (common for multiple Python interpreter versions)
|
||||
os.path.join(AYON_CORE_ROOT, "vendor", "python", "common")
|
||||
os.path.join(AYON_CORE_ROOT, "vendor", "python")
|
||||
]
|
||||
for path in additional_paths:
|
||||
if path not in split_paths:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class Commands:
|
|||
log.warning(
|
||||
"Failed to add cli command for module \"{}\"".format(
|
||||
addon.name
|
||||
)
|
||||
), exc_info=True
|
||||
)
|
||||
return click_func
|
||||
|
||||
|
|
@ -64,9 +64,10 @@ class Commands:
|
|||
get_global_context,
|
||||
)
|
||||
|
||||
# Register target and host
|
||||
import ayon_api
|
||||
import pyblish.util
|
||||
|
||||
# Register target and host
|
||||
if not isinstance(path, str):
|
||||
raise RuntimeError("Path to JSON must be a string.")
|
||||
|
||||
|
|
@ -86,6 +87,19 @@ class Commands:
|
|||
|
||||
log = Logger.get_logger("CLI-publish")
|
||||
|
||||
# Make public ayon api behave as other user
|
||||
# - this works only if public ayon api is using service user
|
||||
username = os.environ.get("AYON_USERNAME")
|
||||
if username:
|
||||
# NOTE: ayon-python-api does not have public api function to find
|
||||
# out if is used service user. So we need to have try > except
|
||||
# block.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
try:
|
||||
con.set_default_service_username(username)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
install_ayon_plugins()
|
||||
|
||||
manager = AddonsManager()
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
from .addon import BlenderAddon
|
||||
|
||||
|
||||
__all__ = (
|
||||
"BlenderAddon",
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from .addon import (
|
||||
HOST_DIR,
|
||||
FlameAddon,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"HOST_DIR",
|
||||
"FlameAddon",
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from .addon import (
|
||||
HIERO_ROOT_DIR,
|
||||
HieroAddon,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"HIERO_ROOT_DIR",
|
||||
"HieroAddon",
|
||||
)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectRemoveMarked(pyblish.api.ContextPlugin):
|
||||
"""Remove marked data
|
||||
|
||||
Remove instances that have 'remove' in their instance.data
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.499
|
||||
label = 'Remove Marked Instances'
|
||||
|
||||
def process(self, context):
|
||||
|
||||
self.log.debug(context)
|
||||
# make ftrack publishable
|
||||
instances_to_remove = []
|
||||
for instance in context:
|
||||
if instance.data.get('remove'):
|
||||
instances_to_remove.append(instance)
|
||||
|
||||
for instance in instances_to_remove:
|
||||
context.remove(instance)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from .addon import ResolveAddon
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ResolveAddon",
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
## Unreal Integration
|
||||
|
||||
Supported Unreal Engine version is 4.26+ (mainly because of major Python changes done there).
|
||||
|
||||
### Project naming
|
||||
Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are
|
||||
invalid. If Ayon detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject`
|
||||
will become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters.
|
||||
Longer names will issue warning in Unreal Editor that there might be possible side effects.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from .addon import UnrealAddon
|
||||
|
||||
|
||||
__all__ = (
|
||||
"UnrealAddon",
|
||||
)
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
from ayon_core.addon import AYONAddon, IHostAddon
|
||||
|
||||
UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class UnrealAddon(AYONAddon, IHostAddon):
|
||||
name = "unreal"
|
||||
host_name = "unreal"
|
||||
|
||||
def get_global_environments(self):
|
||||
return {
|
||||
"AYON_UNREAL_ROOT": UNREAL_ROOT_DIR,
|
||||
}
|
||||
|
||||
def add_implementation_envs(self, env, app):
|
||||
"""Modify environments to contain all required for implementation."""
|
||||
# Set AYON_UNREAL_PLUGIN required for Unreal implementation
|
||||
# Imports are in this method for Python 2 compatiblity of an addon
|
||||
from pathlib import Path
|
||||
|
||||
from .lib import get_compatible_integration
|
||||
|
||||
from ayon_core.tools.utils import show_message_dialog
|
||||
|
||||
pattern = re.compile(r'^\d+-\d+$')
|
||||
|
||||
if not pattern.match(app.name):
|
||||
msg = (
|
||||
"Unreal application key in the settings must be in format"
|
||||
"'5-0' or '5-1'"
|
||||
)
|
||||
show_message_dialog(
|
||||
parent=None,
|
||||
title="Unreal application name format",
|
||||
message=msg,
|
||||
level="critical")
|
||||
raise ValueError(msg)
|
||||
|
||||
ue_version = app.name.replace("-", ".")
|
||||
unreal_plugin_path = os.path.join(
|
||||
UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon"
|
||||
)
|
||||
if not Path(unreal_plugin_path).exists():
|
||||
compatible_versions = get_compatible_integration(
|
||||
ue_version, Path(UNREAL_ROOT_DIR) / "integration"
|
||||
)
|
||||
if compatible_versions:
|
||||
unreal_plugin_path = compatible_versions[-1] / "Ayon"
|
||||
unreal_plugin_path = unreal_plugin_path.as_posix()
|
||||
|
||||
if not env.get("AYON_UNREAL_PLUGIN") or \
|
||||
env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path:
|
||||
env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path
|
||||
|
||||
# Set default environments if are not set via settings
|
||||
defaults = {
|
||||
"AYON_LOG_NO_COLORS": "1",
|
||||
"UE_PYTHONPATH": os.environ.get("PYTHONPATH", ""),
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if not env.get(key):
|
||||
env[key] = value
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
if app.host_name != self.host_name:
|
||||
return []
|
||||
return [
|
||||
os.path.join(UNREAL_ROOT_DIR, "hooks")
|
||||
]
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".uproject"]
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Unreal Editor Ayon host API."""
|
||||
|
||||
from .plugin import (
|
||||
UnrealActorCreator,
|
||||
UnrealAssetCreator,
|
||||
Loader
|
||||
)
|
||||
|
||||
from .pipeline import (
|
||||
install,
|
||||
uninstall,
|
||||
ls,
|
||||
publish,
|
||||
containerise,
|
||||
show_creator,
|
||||
show_loader,
|
||||
show_publisher,
|
||||
show_manager,
|
||||
show_experimental_tools,
|
||||
show_tools_dialog,
|
||||
show_tools_popup,
|
||||
instantiate,
|
||||
UnrealHost,
|
||||
set_sequence_hierarchy,
|
||||
generate_sequence,
|
||||
maintained_selection
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"UnrealActorCreator",
|
||||
"UnrealAssetCreator",
|
||||
"Loader",
|
||||
"install",
|
||||
"uninstall",
|
||||
"ls",
|
||||
"publish",
|
||||
"containerise",
|
||||
"show_creator",
|
||||
"show_loader",
|
||||
"show_publisher",
|
||||
"show_manager",
|
||||
"show_experimental_tools",
|
||||
"show_tools_dialog",
|
||||
"show_tools_popup",
|
||||
"instantiate",
|
||||
"UnrealHost",
|
||||
"set_sequence_hierarchy",
|
||||
"generate_sequence",
|
||||
"maintained_selection"
|
||||
]
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class AyonUnrealException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@unreal.uclass()
|
||||
class AyonHelpers(unreal.AyonLib):
|
||||
"""Class wrapping some useful functions for Ayon.
|
||||
|
||||
This class is extending native BP class in Ayon Integration Plugin.
|
||||
|
||||
"""
|
||||
|
||||
@unreal.ufunction(params=[str, unreal.LinearColor, bool])
|
||||
def set_folder_color(self, path: str, color: unreal.LinearColor) -> None:
|
||||
"""Set color on folder in Content Browser.
|
||||
|
||||
This method sets color on folder in Content Browser. Unfortunately
|
||||
there is no way to refresh Content Browser so new color isn't applied
|
||||
immediately. They are saved to config file and appears correctly
|
||||
only after Editor is restarted.
|
||||
|
||||
Args:
|
||||
path (str): Path to folder
|
||||
color (:class:`unreal.LinearColor`): Color of the folder
|
||||
|
||||
Example:
|
||||
|
||||
AyonHelpers().set_folder_color(
|
||||
"/Game/Path", unreal.LinearColor(a=1.0, r=1.0, g=0.5, b=0)
|
||||
)
|
||||
|
||||
Note:
|
||||
This will take effect only after Editor is restarted. I couldn't
|
||||
find a way to refresh it. Also, this saves the color definition
|
||||
into the project config, binding this path with color. So if you
|
||||
delete this path and later re-create, it will set this color
|
||||
again.
|
||||
|
||||
"""
|
||||
self.c_set_folder_color(path, color, False)
|
||||
|
|
@ -1,804 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import List
|
||||
from contextlib import contextmanager
|
||||
import time
|
||||
|
||||
import semver
|
||||
import pyblish.api
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
register_inventory_action_path,
|
||||
deregister_loader_plugin_path,
|
||||
deregister_creator_plugin_path,
|
||||
deregister_inventory_action_path,
|
||||
AYON_CONTAINER_ID,
|
||||
get_current_project_name,
|
||||
)
|
||||
from ayon_core.tools.utils import host_tools
|
||||
import ayon_core.hosts.unreal
|
||||
from ayon_core.host import HostBase, ILoadHost, IPublishHost
|
||||
|
||||
import unreal # noqa
|
||||
|
||||
# Rename to Ayon once parent module renames
|
||||
logger = logging.getLogger("ayon_core.hosts.unreal")
|
||||
|
||||
AYON_CONTAINERS = "AyonContainers"
|
||||
AYON_ASSET_DIR = "/Game/Ayon/Assets"
|
||||
CONTEXT_CONTAINER = "Ayon/context.json"
|
||||
UNREAL_VERSION = semver.VersionInfo(
|
||||
*os.getenv("AYON_UNREAL_VERSION").split(".")
|
||||
)
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(ayon_core.hosts.unreal.__file__))
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
|
||||
class UnrealHost(HostBase, ILoadHost, IPublishHost):
|
||||
"""Unreal host implementation.
|
||||
|
||||
For some time this class will re-use functions from module based
|
||||
implementation for backwards compatibility of older unreal projects.
|
||||
"""
|
||||
|
||||
name = "unreal"
|
||||
|
||||
def install(self):
|
||||
install()
|
||||
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
@staticmethod
|
||||
def show_tools_popup():
|
||||
"""Show tools popup with actions leading to show other tools."""
|
||||
show_tools_popup()
|
||||
|
||||
@staticmethod
|
||||
def show_tools_dialog():
|
||||
"""Show tools dialog with actions leading to show other tools."""
|
||||
show_tools_dialog()
|
||||
|
||||
def update_context_data(self, data, changes):
|
||||
content_path = unreal.Paths.project_content_dir()
|
||||
op_ctx = content_path + CONTEXT_CONTAINER
|
||||
attempts = 3
|
||||
for i in range(attempts):
|
||||
try:
|
||||
with open(op_ctx, "w+") as f:
|
||||
json.dump(data, f)
|
||||
break
|
||||
except IOError as e:
|
||||
if i == attempts - 1:
|
||||
raise Exception(
|
||||
"Failed to write context data. Aborting.") from e
|
||||
unreal.log_warning("Failed to write context data. Retrying...")
|
||||
i += 1
|
||||
time.sleep(3)
|
||||
continue
|
||||
|
||||
def get_context_data(self):
|
||||
content_path = unreal.Paths.project_content_dir()
|
||||
op_ctx = content_path + CONTEXT_CONTAINER
|
||||
if not os.path.isfile(op_ctx):
|
||||
return {}
|
||||
with open(op_ctx, "r") as fp:
|
||||
data = json.load(fp)
|
||||
return data
|
||||
|
||||
|
||||
def install():
|
||||
"""Install Unreal configuration for AYON."""
|
||||
print("-=" * 40)
|
||||
logo = '''.
|
||||
.
|
||||
·
|
||||
│
|
||||
·∙/
|
||||
·-∙•∙-·
|
||||
/ \\ /∙· / \\
|
||||
∙ \\ │ / ∙
|
||||
\\ \\ · / /
|
||||
\\\\ ∙ ∙ //
|
||||
\\\\/ \\//
|
||||
___
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│___│
|
||||
-·
|
||||
|
||||
·-─═─-∙ A Y O N ∙-─═─-·
|
||||
by YNPUT
|
||||
.
|
||||
'''
|
||||
print(logo)
|
||||
print("installing Ayon for Unreal ...")
|
||||
print("-=" * 40)
|
||||
logger.info("installing Ayon for Unreal")
|
||||
pyblish.api.register_host("unreal")
|
||||
pyblish.api.register_plugin_path(str(PUBLISH_PATH))
|
||||
register_loader_plugin_path(str(LOAD_PATH))
|
||||
register_creator_plugin_path(str(CREATE_PATH))
|
||||
register_inventory_action_path(str(INVENTORY_PATH))
|
||||
_register_callbacks()
|
||||
_register_events()
|
||||
|
||||
|
||||
def uninstall():
|
||||
"""Uninstall Unreal configuration for Ayon."""
|
||||
pyblish.api.deregister_plugin_path(str(PUBLISH_PATH))
|
||||
deregister_loader_plugin_path(str(LOAD_PATH))
|
||||
deregister_creator_plugin_path(str(CREATE_PATH))
|
||||
deregister_inventory_action_path(str(INVENTORY_PATH))
|
||||
|
||||
|
||||
def _register_callbacks():
|
||||
"""
|
||||
TODO: Implement callbacks if supported by UE
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _register_events():
|
||||
"""
|
||||
TODO: Implement callbacks if supported by UE
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def ls():
|
||||
"""List all containers.
|
||||
|
||||
List all found in *Content Manager* of Unreal and return
|
||||
metadata from them. Adding `objectName` to set.
|
||||
|
||||
"""
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
# UE 5.1 changed how class name is specified
|
||||
class_name = ["/Script/Ayon", "AyonAssetContainer"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else "AyonAssetContainer" # noqa
|
||||
ayon_containers = ar.get_assets_by_class(class_name, True)
|
||||
|
||||
# get_asset_by_class returns AssetData. To get all metadata we need to
|
||||
# load asset. get_tag_values() work only on metadata registered in
|
||||
# Asset Registry Project settings (and there is no way to set it with
|
||||
# python short of editing ini configuration file).
|
||||
for asset_data in ayon_containers:
|
||||
asset = asset_data.get_asset()
|
||||
data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)
|
||||
data["objectName"] = asset_data.asset_name
|
||||
yield cast_map_to_str_dict(data)
|
||||
|
||||
|
||||
def ls_inst():
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
# UE 5.1 changed how class name is specified
|
||||
class_name = [
|
||||
"/Script/Ayon",
|
||||
"AyonPublishInstance"
|
||||
] if (
|
||||
UNREAL_VERSION.major == 5
|
||||
and UNREAL_VERSION.minor > 0
|
||||
) else "AyonPublishInstance" # noqa
|
||||
instances = ar.get_assets_by_class(class_name, True)
|
||||
|
||||
# get_asset_by_class returns AssetData. To get all metadata we need to
|
||||
# load asset. get_tag_values() work only on metadata registered in
|
||||
# Asset Registry Project settings (and there is no way to set it with
|
||||
# python short of editing ini configuration file).
|
||||
for asset_data in instances:
|
||||
asset = asset_data.get_asset()
|
||||
data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)
|
||||
data["objectName"] = asset_data.asset_name
|
||||
yield cast_map_to_str_dict(data)
|
||||
|
||||
|
||||
def parse_container(container):
|
||||
"""To get data from container, AyonAssetContainer must be loaded.
|
||||
|
||||
Args:
|
||||
container(str): path to container
|
||||
|
||||
Returns:
|
||||
dict: metadata stored on container
|
||||
"""
|
||||
asset = unreal.EditorAssetLibrary.load_asset(container)
|
||||
data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)
|
||||
data["objectName"] = asset.get_name()
|
||||
data = cast_map_to_str_dict(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def publish():
|
||||
"""Shorthand to publish from within host."""
|
||||
import pyblish.util
|
||||
|
||||
return pyblish.util.publish()
|
||||
|
||||
|
||||
def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"):
|
||||
"""Bundles *nodes* (assets) into a *container* and add metadata to it.
|
||||
|
||||
Unreal doesn't support *groups* of assets that you can add metadata to.
|
||||
But it does support folders that helps to organize asset. Unfortunately
|
||||
those folders are just that - you cannot add any additional information
|
||||
to them. Ayon Integration Plugin is providing way out - Implementing
|
||||
`AssetContainer` Blueprint class. This class when added to folder can
|
||||
handle metadata on it using standard
|
||||
:func:`unreal.EditorAssetLibrary.set_metadata_tag()` and
|
||||
:func:`unreal.EditorAssetLibrary.get_metadata_tag_values()`. It also
|
||||
stores and monitor all changes in assets in path where it resides. List of
|
||||
those assets is available as `assets` property.
|
||||
|
||||
This is list of strings starting with asset type and ending with its path:
|
||||
`Material /Game/Ayon/Test/TestMaterial.TestMaterial`
|
||||
|
||||
"""
|
||||
# 1 - create directory for container
|
||||
root = "/Game"
|
||||
container_name = f"{name}{suffix}"
|
||||
new_name = move_assets_to_path(root, container_name, nodes)
|
||||
|
||||
# 2 - create Asset Container there
|
||||
path = f"{root}/{new_name}"
|
||||
create_container(container=container_name, path=path)
|
||||
|
||||
namespace = path
|
||||
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"name": new_name,
|
||||
"namespace": namespace,
|
||||
"loader": str(loader),
|
||||
"representation": context["representation"]["id"],
|
||||
}
|
||||
# 3 - imprint data
|
||||
imprint(f"{path}/{container_name}", data)
|
||||
return path
|
||||
|
||||
|
||||
def instantiate(root, name, data, assets=None, suffix="_INS"):
|
||||
"""Bundles *nodes* into *container*.
|
||||
|
||||
Marking it with metadata as publishable instance. If assets are provided,
|
||||
they are moved to new path where `AyonPublishInstance` class asset is
|
||||
created and imprinted with metadata.
|
||||
|
||||
This can then be collected for publishing by Pyblish for example.
|
||||
|
||||
Args:
|
||||
root (str): root path where to create instance container
|
||||
name (str): name of the container
|
||||
data (dict): data to imprint on container
|
||||
assets (list of str): list of asset paths to include in publish
|
||||
instance
|
||||
suffix (str): suffix string to append to instance name
|
||||
|
||||
"""
|
||||
container_name = f"{name}{suffix}"
|
||||
|
||||
# if we specify assets, create new folder and move them there. If not,
|
||||
# just create empty folder
|
||||
if assets:
|
||||
new_name = move_assets_to_path(root, container_name, assets)
|
||||
else:
|
||||
new_name = create_folder(root, name)
|
||||
|
||||
path = f"{root}/{new_name}"
|
||||
create_publish_instance(instance=container_name, path=path)
|
||||
|
||||
imprint(f"{path}/{container_name}", data)
|
||||
|
||||
|
||||
def imprint(node, data):
|
||||
loaded_asset = unreal.EditorAssetLibrary.load_asset(node)
|
||||
for key, value in data.items():
|
||||
# Support values evaluated at imprint
|
||||
if callable(value):
|
||||
value = value()
|
||||
# Unreal doesn't support NoneType in metadata values
|
||||
if value is None:
|
||||
value = ""
|
||||
unreal.EditorAssetLibrary.set_metadata_tag(
|
||||
loaded_asset, key, str(value)
|
||||
)
|
||||
|
||||
with unreal.ScopedEditorTransaction("Ayon containerising"):
|
||||
unreal.EditorAssetLibrary.save_asset(node)
|
||||
|
||||
|
||||
def show_tools_popup():
|
||||
"""Show popup with tools.
|
||||
|
||||
Popup will disappear on click or losing focus.
|
||||
"""
|
||||
from ayon_core.hosts.unreal.api import tools_ui
|
||||
|
||||
tools_ui.show_tools_popup()
|
||||
|
||||
|
||||
def show_tools_dialog():
|
||||
"""Show dialog with tools.
|
||||
|
||||
Dialog will stay visible.
|
||||
"""
|
||||
from ayon_core.hosts.unreal.api import tools_ui
|
||||
|
||||
tools_ui.show_tools_dialog()
|
||||
|
||||
|
||||
def show_creator():
|
||||
host_tools.show_creator()
|
||||
|
||||
|
||||
def show_loader():
|
||||
host_tools.show_loader(use_context=True)
|
||||
|
||||
|
||||
def show_publisher():
|
||||
host_tools.show_publish()
|
||||
|
||||
|
||||
def show_manager():
|
||||
host_tools.show_scene_inventory()
|
||||
|
||||
|
||||
def show_experimental_tools():
|
||||
host_tools.show_experimental_tools_dialog()
|
||||
|
||||
|
||||
def create_folder(root: str, name: str) -> str:
|
||||
"""Create new folder.
|
||||
|
||||
If folder exists, append number at the end and try again, incrementing
|
||||
if needed.
|
||||
|
||||
Args:
|
||||
root (str): path root
|
||||
name (str): folder name
|
||||
|
||||
Returns:
|
||||
str: folder name
|
||||
|
||||
Example:
|
||||
>>> create_folder("/Game/Foo")
|
||||
/Game/Foo
|
||||
>>> create_folder("/Game/Foo")
|
||||
/Game/Foo1
|
||||
|
||||
"""
|
||||
eal = unreal.EditorAssetLibrary
|
||||
index = 1
|
||||
while True:
|
||||
if eal.does_directory_exist(f"{root}/{name}"):
|
||||
name = f"{name}{index}"
|
||||
index += 1
|
||||
else:
|
||||
eal.make_directory(f"{root}/{name}")
|
||||
break
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def move_assets_to_path(root: str, name: str, assets: List[str]) -> str:
|
||||
"""Moving (renaming) list of asset paths to new destination.
|
||||
|
||||
Args:
|
||||
root (str): root of the path (eg. `/Game`)
|
||||
name (str): name of destination directory (eg. `Foo` )
|
||||
assets (list of str): list of asset paths
|
||||
|
||||
Returns:
|
||||
str: folder name
|
||||
|
||||
Example:
|
||||
This will get paths of all assets under `/Game/Test` and move them
|
||||
to `/Game/NewTest`. If `/Game/NewTest` already exists, then resulting
|
||||
path will be `/Game/NewTest1`
|
||||
|
||||
>>> assets = unreal.EditorAssetLibrary.list_assets("/Game/Test")
|
||||
>>> move_assets_to_path("/Game", "NewTest", assets)
|
||||
NewTest
|
||||
|
||||
"""
|
||||
eal = unreal.EditorAssetLibrary
|
||||
name = create_folder(root, name)
|
||||
|
||||
unreal.log(assets)
|
||||
for asset in assets:
|
||||
loaded = eal.load_asset(asset)
|
||||
eal.rename_asset(asset, f"{root}/{name}/{loaded.get_name()}")
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def create_container(container: str, path: str) -> unreal.Object:
|
||||
"""Helper function to create Asset Container class on given path.
|
||||
|
||||
This Asset Class helps to mark given path as Container
|
||||
and enable asset version control on it.
|
||||
|
||||
Args:
|
||||
container (str): Asset Container name
|
||||
path (str): Path where to create Asset Container. This path should
|
||||
point into container folder
|
||||
|
||||
Returns:
|
||||
:class:`unreal.Object`: instance of created asset
|
||||
|
||||
Example:
|
||||
|
||||
create_container(
|
||||
"/Game/modelingFooCharacter_CON",
|
||||
"modelingFooCharacter_CON"
|
||||
)
|
||||
|
||||
"""
|
||||
factory = unreal.AyonAssetContainerFactory()
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
|
||||
return tools.create_asset(container, path, None, factory)
|
||||
|
||||
|
||||
def create_publish_instance(instance: str, path: str) -> unreal.Object:
|
||||
"""Helper function to create Ayon Publish Instance on given path.
|
||||
|
||||
This behaves similarly as :func:`create_ayon_container`.
|
||||
|
||||
Args:
|
||||
path (str): Path where to create Publish Instance.
|
||||
This path should point into container folder
|
||||
instance (str): Publish Instance name
|
||||
|
||||
Returns:
|
||||
:class:`unreal.Object`: instance of created asset
|
||||
|
||||
Example:
|
||||
|
||||
create_publish_instance(
|
||||
"/Game/modelingFooCharacter_INST",
|
||||
"modelingFooCharacter_INST"
|
||||
)
|
||||
|
||||
"""
|
||||
factory = unreal.AyonPublishInstanceFactory()
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
return tools.create_asset(instance, path, None, factory)
|
||||
|
||||
|
||||
def cast_map_to_str_dict(umap) -> dict:
|
||||
"""Cast Unreal Map to dict.
|
||||
|
||||
Helper function to cast Unreal Map object to plain old python
|
||||
dict. This will also cast values and keys to str. Useful for
|
||||
metadata dicts.
|
||||
|
||||
Args:
|
||||
umap: Unreal Map object
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
return {str(key): str(value) for (key, value) in umap.items()}
|
||||
|
||||
|
||||
def get_subsequences(sequence: unreal.LevelSequence):
|
||||
"""Get list of subsequences from sequence.
|
||||
|
||||
Args:
|
||||
sequence (unreal.LevelSequence): Sequence
|
||||
|
||||
Returns:
|
||||
list(unreal.LevelSequence): List of subsequences
|
||||
|
||||
"""
|
||||
tracks = sequence.get_master_tracks()
|
||||
subscene_track = next(
|
||||
(
|
||||
t
|
||||
for t in tracks
|
||||
if t.get_class() == unreal.MovieSceneSubTrack.static_class()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if subscene_track is not None and subscene_track.get_sections():
|
||||
return subscene_track.get_sections()
|
||||
return []
|
||||
|
||||
|
||||
def set_sequence_hierarchy(
|
||||
seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
|
||||
):
|
||||
# Get existing sequencer tracks or create them if they don't exist
|
||||
tracks = seq_i.get_master_tracks()
|
||||
subscene_track = None
|
||||
visibility_track = None
|
||||
for t in tracks:
|
||||
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
|
||||
subscene_track = t
|
||||
if (t.get_class() ==
|
||||
unreal.MovieSceneLevelVisibilityTrack.static_class()):
|
||||
visibility_track = t
|
||||
if not subscene_track:
|
||||
subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack)
|
||||
if not visibility_track:
|
||||
visibility_track = seq_i.add_master_track(
|
||||
unreal.MovieSceneLevelVisibilityTrack)
|
||||
|
||||
# Create the sub-scene section
|
||||
subscenes = subscene_track.get_sections()
|
||||
subscene = None
|
||||
for s in subscenes:
|
||||
if s.get_editor_property('sub_sequence') == seq_j:
|
||||
subscene = s
|
||||
break
|
||||
if not subscene:
|
||||
subscene = subscene_track.add_section()
|
||||
subscene.set_row_index(len(subscene_track.get_sections()))
|
||||
subscene.set_editor_property('sub_sequence', seq_j)
|
||||
subscene.set_range(
|
||||
min_frame_j,
|
||||
max_frame_j + 1)
|
||||
|
||||
# Create the visibility section
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
maps = []
|
||||
for m in map_paths:
|
||||
# Unreal requires to load the level to get the map name
|
||||
unreal.EditorLevelLibrary.save_all_dirty_levels()
|
||||
unreal.EditorLevelLibrary.load_level(m)
|
||||
maps.append(str(ar.get_asset_by_object_path(m).asset_name))
|
||||
|
||||
vis_section = visibility_track.add_section()
|
||||
index = len(visibility_track.get_sections())
|
||||
|
||||
vis_section.set_range(
|
||||
min_frame_j,
|
||||
max_frame_j + 1)
|
||||
vis_section.set_visibility(unreal.LevelVisibility.VISIBLE)
|
||||
vis_section.set_row_index(index)
|
||||
vis_section.set_level_names(maps)
|
||||
|
||||
if min_frame_j > 1:
|
||||
hid_section = visibility_track.add_section()
|
||||
hid_section.set_range(
|
||||
1,
|
||||
min_frame_j)
|
||||
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
|
||||
hid_section.set_row_index(index)
|
||||
hid_section.set_level_names(maps)
|
||||
if max_frame_j < max_frame_i:
|
||||
hid_section = visibility_track.add_section()
|
||||
hid_section.set_range(
|
||||
max_frame_j + 1,
|
||||
max_frame_i + 1)
|
||||
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
|
||||
hid_section.set_row_index(index)
|
||||
hid_section.set_level_names(maps)
|
||||
|
||||
|
||||
def generate_sequence(h, h_dir):
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
|
||||
sequence = tools.create_asset(
|
||||
asset_name=h,
|
||||
package_path=h_dir,
|
||||
asset_class=unreal.LevelSequence,
|
||||
factory=unreal.LevelSequenceFactoryNew()
|
||||
)
|
||||
|
||||
project_name = get_current_project_name()
|
||||
# TODO Fix this does not return folder path
|
||||
folder_path = h_dir.split('/')[-1],
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name,
|
||||
folder_path,
|
||||
fields={"id", "attrib.fps"}
|
||||
)
|
||||
|
||||
start_frames = []
|
||||
end_frames = []
|
||||
|
||||
elements = list(ayon_api.get_folders(
|
||||
project_name,
|
||||
parent_ids=[folder_entity["id"]],
|
||||
fields={"id", "attrib.clipIn", "attrib.clipOut"}
|
||||
))
|
||||
for e in elements:
|
||||
start_frames.append(e["attrib"].get("clipIn"))
|
||||
end_frames.append(e["attrib"].get("clipOut"))
|
||||
|
||||
elements.extend(ayon_api.get_folders(
|
||||
project_name,
|
||||
parent_ids=[e["id"]],
|
||||
fields={"id", "attrib.clipIn", "attrib.clipOut"}
|
||||
))
|
||||
|
||||
min_frame = min(start_frames)
|
||||
max_frame = max(end_frames)
|
||||
|
||||
fps = folder_entity["attrib"].get("fps")
|
||||
|
||||
sequence.set_display_rate(
|
||||
unreal.FrameRate(fps, 1.0))
|
||||
sequence.set_playback_start(min_frame)
|
||||
sequence.set_playback_end(max_frame)
|
||||
|
||||
sequence.set_work_range_start(min_frame / fps)
|
||||
sequence.set_work_range_end(max_frame / fps)
|
||||
sequence.set_view_range_start(min_frame / fps)
|
||||
sequence.set_view_range_end(max_frame / fps)
|
||||
|
||||
tracks = sequence.get_master_tracks()
|
||||
track = None
|
||||
for t in tracks:
|
||||
if (t.get_class() ==
|
||||
unreal.MovieSceneCameraCutTrack.static_class()):
|
||||
track = t
|
||||
break
|
||||
if not track:
|
||||
track = sequence.add_master_track(
|
||||
unreal.MovieSceneCameraCutTrack)
|
||||
|
||||
return sequence, (min_frame, max_frame)
|
||||
|
||||
|
||||
def _get_comps_and_assets(
|
||||
component_class, asset_class, old_assets, new_assets, selected
|
||||
):
|
||||
eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
||||
|
||||
components = []
|
||||
if selected:
|
||||
sel_actors = eas.get_selected_level_actors()
|
||||
for actor in sel_actors:
|
||||
comps = actor.get_components_by_class(component_class)
|
||||
components.extend(comps)
|
||||
else:
|
||||
comps = eas.get_all_level_actors_components()
|
||||
components = [
|
||||
c for c in comps if isinstance(c, component_class)
|
||||
]
|
||||
|
||||
# Get all the static meshes among the old assets in a dictionary with
|
||||
# the name as key
|
||||
selected_old_assets = {}
|
||||
for a in old_assets:
|
||||
asset = unreal.EditorAssetLibrary.load_asset(a)
|
||||
if isinstance(asset, asset_class):
|
||||
selected_old_assets[asset.get_name()] = asset
|
||||
|
||||
# Get all the static meshes among the new assets in a dictionary with
|
||||
# the name as key
|
||||
selected_new_assets = {}
|
||||
for a in new_assets:
|
||||
asset = unreal.EditorAssetLibrary.load_asset(a)
|
||||
if isinstance(asset, asset_class):
|
||||
selected_new_assets[asset.get_name()] = asset
|
||||
|
||||
return components, selected_old_assets, selected_new_assets
|
||||
|
||||
|
||||
def replace_static_mesh_actors(old_assets, new_assets, selected):
|
||||
smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem)
|
||||
|
||||
static_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets(
|
||||
unreal.StaticMeshComponent,
|
||||
unreal.StaticMesh,
|
||||
old_assets,
|
||||
new_assets,
|
||||
selected
|
||||
)
|
||||
|
||||
for old_name, old_mesh in old_meshes.items():
|
||||
new_mesh = new_meshes.get(old_name)
|
||||
|
||||
if not new_mesh:
|
||||
continue
|
||||
|
||||
smes.replace_mesh_components_meshes(
|
||||
static_mesh_comps, old_mesh, new_mesh)
|
||||
|
||||
|
||||
def replace_skeletal_mesh_actors(old_assets, new_assets, selected):
|
||||
skeletal_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets(
|
||||
unreal.SkeletalMeshComponent,
|
||||
unreal.SkeletalMesh,
|
||||
old_assets,
|
||||
new_assets,
|
||||
selected
|
||||
)
|
||||
|
||||
for old_name, old_mesh in old_meshes.items():
|
||||
new_mesh = new_meshes.get(old_name)
|
||||
|
||||
if not new_mesh:
|
||||
continue
|
||||
|
||||
for comp in skeletal_mesh_comps:
|
||||
if comp.get_skeletal_mesh_asset() == old_mesh:
|
||||
comp.set_skeletal_mesh_asset(new_mesh)
|
||||
|
||||
|
||||
def replace_geometry_cache_actors(old_assets, new_assets, selected):
|
||||
geometry_cache_comps, old_caches, new_caches = _get_comps_and_assets(
|
||||
unreal.GeometryCacheComponent,
|
||||
unreal.GeometryCache,
|
||||
old_assets,
|
||||
new_assets,
|
||||
selected
|
||||
)
|
||||
|
||||
for old_name, old_mesh in old_caches.items():
|
||||
new_mesh = new_caches.get(old_name)
|
||||
|
||||
if not new_mesh:
|
||||
continue
|
||||
|
||||
for comp in geometry_cache_comps:
|
||||
if comp.get_editor_property("geometry_cache") == old_mesh:
|
||||
comp.set_geometry_cache(new_mesh)
|
||||
|
||||
|
||||
def delete_asset_if_unused(container, asset_content):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
references = set()
|
||||
|
||||
for asset_path in asset_content:
|
||||
asset = ar.get_asset_by_object_path(asset_path)
|
||||
refs = ar.get_referencers(
|
||||
asset.package_name,
|
||||
unreal.AssetRegistryDependencyOptions(
|
||||
include_soft_package_references=False,
|
||||
include_hard_package_references=True,
|
||||
include_searchable_names=False,
|
||||
include_soft_management_references=False,
|
||||
include_hard_management_references=False
|
||||
))
|
||||
if not refs:
|
||||
continue
|
||||
references = references.union(set(refs))
|
||||
|
||||
# Filter out references that are in the Temp folder
|
||||
cleaned_references = {
|
||||
ref for ref in references if not str(ref).startswith("/Temp/")}
|
||||
|
||||
# Check which of the references are Levels
|
||||
for ref in cleaned_references:
|
||||
loaded_asset = unreal.EditorAssetLibrary.load_asset(ref)
|
||||
if isinstance(loaded_asset, unreal.World):
|
||||
# If there is at least a level, we can stop, we don't want to
|
||||
# delete the container
|
||||
return
|
||||
|
||||
unreal.log("Previous version unused, deleting...")
|
||||
|
||||
# No levels, delete the asset
|
||||
unreal.EditorAssetLibrary.delete_directory(container["namespace"])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def maintained_selection():
|
||||
"""Stub to be either implemented or replaced.
|
||||
|
||||
This is needed for old publisher implementation, but
|
||||
it is not supported (yet) in UE.
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
pass
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import ast
|
||||
import collections
|
||||
import sys
|
||||
import six
|
||||
from abc import (
|
||||
ABC,
|
||||
ABCMeta,
|
||||
)
|
||||
|
||||
import unreal
|
||||
|
||||
from .pipeline import (
|
||||
create_publish_instance,
|
||||
imprint,
|
||||
ls_inst,
|
||||
UNREAL_VERSION
|
||||
)
|
||||
from ayon_core.lib import (
|
||||
BoolDef,
|
||||
UILabelDef
|
||||
)
|
||||
from ayon_core.pipeline import (
|
||||
Creator,
|
||||
LoaderPlugin,
|
||||
CreatorError,
|
||||
CreatedInstance
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class UnrealBaseCreator(Creator):
|
||||
"""Base class for Unreal creator plugins."""
|
||||
root = "/Game/Ayon/AyonPublishInstances"
|
||||
suffix = "_INS"
|
||||
|
||||
@staticmethod
|
||||
def cache_instance_data(shared_data):
|
||||
"""Cache instances for Creators to shared data.
|
||||
|
||||
Create `unreal_cached_instances` key when needed in shared data and
|
||||
fill it with all collected instances from the scene under its
|
||||
respective creator identifiers.
|
||||
|
||||
If legacy instances are detected in the scene, create
|
||||
`unreal_cached_legacy_instances` there and fill it with
|
||||
all legacy products under family as a key.
|
||||
|
||||
Args:
|
||||
Dict[str, Any]: Shared data.
|
||||
|
||||
"""
|
||||
if "unreal_cached_instances" in shared_data:
|
||||
return
|
||||
|
||||
unreal_cached_instances = collections.defaultdict(list)
|
||||
unreal_cached_legacy_instances = collections.defaultdict(list)
|
||||
for instance in ls_inst():
|
||||
creator_id = instance.get("creator_identifier")
|
||||
if creator_id:
|
||||
unreal_cached_instances[creator_id].append(instance)
|
||||
else:
|
||||
family = instance.get("family")
|
||||
unreal_cached_legacy_instances[family].append(instance)
|
||||
|
||||
shared_data["unreal_cached_instances"] = unreal_cached_instances
|
||||
shared_data["unreal_cached_legacy_instances"] = (
|
||||
unreal_cached_legacy_instances
|
||||
)
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
try:
|
||||
instance_name = f"{product_name}{self.suffix}"
|
||||
pub_instance = create_publish_instance(instance_name, self.root)
|
||||
|
||||
instance_data["productName"] = product_name
|
||||
instance_data["instance_path"] = f"{self.root}/{instance_name}"
|
||||
|
||||
instance = CreatedInstance(
|
||||
self.product_type,
|
||||
product_name,
|
||||
instance_data,
|
||||
self)
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
pub_instance.set_editor_property('add_external_assets', True)
|
||||
assets = pub_instance.get_editor_property('asset_data_external')
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
for member in pre_create_data.get("members", []):
|
||||
obj = ar.get_asset_by_object_path(member).get_asset()
|
||||
assets.add(obj)
|
||||
|
||||
imprint(f"{self.root}/{instance_name}", instance.data_to_store())
|
||||
|
||||
return instance
|
||||
|
||||
except Exception as er:
|
||||
six.reraise(
|
||||
CreatorError,
|
||||
CreatorError(f"Creator error: {er}"),
|
||||
sys.exc_info()[2])
|
||||
|
||||
def collect_instances(self):
|
||||
# cache instances if missing
|
||||
self.cache_instance_data(self.collection_shared_data)
|
||||
for instance in self.collection_shared_data[
|
||||
"unreal_cached_instances"].get(self.identifier, []):
|
||||
# Unreal saves metadata as string, so we need to convert it back
|
||||
instance['creator_attributes'] = ast.literal_eval(
|
||||
instance.get('creator_attributes', '{}'))
|
||||
instance['publish_attributes'] = ast.literal_eval(
|
||||
instance.get('publish_attributes', '{}'))
|
||||
created_instance = CreatedInstance.from_existing(instance, self)
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, changes in update_list:
|
||||
instance_node = created_inst.get("instance_path", "")
|
||||
|
||||
if not instance_node:
|
||||
unreal.log_warning(
|
||||
f"Instance node not found for {created_inst}")
|
||||
continue
|
||||
|
||||
new_values = {
|
||||
key: changes[key].new_value
|
||||
for key in changes.changed_keys
|
||||
}
|
||||
imprint(
|
||||
instance_node,
|
||||
new_values
|
||||
)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
for instance in instances:
|
||||
instance_node = instance.data.get("instance_path", "")
|
||||
if instance_node:
|
||||
unreal.EditorAssetLibrary.delete_asset(instance_node)
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class UnrealAssetCreator(UnrealBaseCreator):
|
||||
"""Base class for Unreal creator plugins based on assets."""
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
"""Create instance of the asset.
|
||||
|
||||
Args:
|
||||
product_name (str): Name of the product.
|
||||
instance_data (dict): Data for the instance.
|
||||
pre_create_data (dict): Data for the instance.
|
||||
|
||||
Returns:
|
||||
CreatedInstance: Created instance.
|
||||
"""
|
||||
try:
|
||||
# Check if instance data has members, filled by the plugin.
|
||||
# If not, use selection.
|
||||
if not pre_create_data.get("members"):
|
||||
pre_create_data["members"] = []
|
||||
|
||||
if pre_create_data.get("use_selection"):
|
||||
utilib = unreal.EditorUtilityLibrary
|
||||
sel_objects = utilib.get_selected_assets()
|
||||
pre_create_data["members"] = [
|
||||
a.get_path_name() for a in sel_objects]
|
||||
|
||||
super(UnrealAssetCreator, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
except Exception as er:
|
||||
six.reraise(
|
||||
CreatorError,
|
||||
CreatorError(f"Creator error: {er}"),
|
||||
sys.exc_info()[2])
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
BoolDef("use_selection", label="Use selection", default=True)
|
||||
]
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class UnrealActorCreator(UnrealBaseCreator):
|
||||
"""Base class for Unreal creator plugins based on actors."""
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
"""Create instance of the asset.
|
||||
|
||||
Args:
|
||||
product_name (str): Name of the product.
|
||||
instance_data (dict): Data for the instance.
|
||||
pre_create_data (dict): Data for the instance.
|
||||
|
||||
Returns:
|
||||
CreatedInstance: Created instance.
|
||||
"""
|
||||
try:
|
||||
if UNREAL_VERSION.major == 5:
|
||||
world = unreal.UnrealEditorSubsystem().get_editor_world()
|
||||
else:
|
||||
world = unreal.EditorLevelLibrary.get_editor_world()
|
||||
|
||||
# Check if the level is saved
|
||||
if world.get_path_name().startswith("/Temp/"):
|
||||
raise CreatorError(
|
||||
"Level must be saved before creating instances.")
|
||||
|
||||
# Check if instance data has members, filled by the plugin.
|
||||
# If not, use selection.
|
||||
if not instance_data.get("members"):
|
||||
actor_subsystem = unreal.EditorActorSubsystem()
|
||||
sel_actors = actor_subsystem.get_selected_level_actors()
|
||||
selection = [a.get_path_name() for a in sel_actors]
|
||||
|
||||
instance_data["members"] = selection
|
||||
|
||||
instance_data["level"] = world.get_path_name()
|
||||
|
||||
super(UnrealActorCreator, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
except Exception as er:
|
||||
six.reraise(
|
||||
CreatorError,
|
||||
CreatorError(f"Creator error: {er}"),
|
||||
sys.exc_info()[2])
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
UILabelDef("Select actors to create instance from them.")
|
||||
]
|
||||
|
||||
|
||||
class Loader(LoaderPlugin, ABC):
|
||||
"""This serves as skeleton for future Ayon specific functionality"""
|
||||
pass
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import Anatomy
|
||||
from ayon_core.hosts.unreal.api import pipeline
|
||||
from ayon_core.tools.utils import show_message_dialog
|
||||
|
||||
|
||||
queue = None
|
||||
executor = None
|
||||
|
||||
|
||||
def _queue_finish_callback(exec, success):
|
||||
unreal.log("Render completed. Success: " + str(success))
|
||||
|
||||
# Delete our reference so we don't keep it alive.
|
||||
global executor
|
||||
global queue
|
||||
del executor
|
||||
del queue
|
||||
|
||||
|
||||
def _job_finish_callback(job, success):
|
||||
# You can make any edits you want to the editor world here, and the world
|
||||
# will be duplicated when the next render happens. Make sure you undo your
|
||||
# edits in OnQueueFinishedCallback if you don't want to leak state changes
|
||||
# into the editor world.
|
||||
unreal.log("Individual job completed.")
|
||||
|
||||
|
||||
def start_rendering():
|
||||
"""
|
||||
Start the rendering process.
|
||||
"""
|
||||
unreal.log("Starting rendering...")
|
||||
|
||||
# Get selected sequences
|
||||
assets = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
|
||||
if not assets:
|
||||
show_message_dialog(
|
||||
title="No assets selected",
|
||||
message="No assets selected. Select a render instance.",
|
||||
level="warning")
|
||||
raise RuntimeError(
|
||||
"No assets selected. You need to select a render instance.")
|
||||
|
||||
# instances = pipeline.ls_inst()
|
||||
instances = [
|
||||
a for a in assets
|
||||
if a.get_class().get_name() == "AyonPublishInstance"]
|
||||
|
||||
inst_data = []
|
||||
|
||||
for i in instances:
|
||||
data = pipeline.parse_container(i.get_path_name())
|
||||
if data["productType"] == "render":
|
||||
inst_data.append(data)
|
||||
|
||||
try:
|
||||
project = os.environ.get("AYON_PROJECT_NAME")
|
||||
anatomy = Anatomy(project)
|
||||
root = anatomy.roots['renders']
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
"Could not find render root in anatomy settings.") from e
|
||||
|
||||
render_dir = f"{root}/{project}"
|
||||
|
||||
# subsystem = unreal.get_editor_subsystem(
|
||||
# unreal.MoviePipelineQueueSubsystem)
|
||||
# queue = subsystem.get_queue()
|
||||
global queue
|
||||
queue = unreal.MoviePipelineQueue()
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
data = get_project_settings(project)
|
||||
config = None
|
||||
config_path = str(data.get("unreal").get("render_config_path"))
|
||||
if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path):
|
||||
unreal.log("Found saved render configuration")
|
||||
config = ar.get_asset_by_object_path(config_path).get_asset()
|
||||
|
||||
for i in inst_data:
|
||||
sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset()
|
||||
|
||||
sequences = [{
|
||||
"sequence": sequence,
|
||||
"output": f"{i['output']}",
|
||||
"frame_range": (
|
||||
int(float(i["frameStart"])),
|
||||
int(float(i["frameEnd"])) + 1)
|
||||
}]
|
||||
render_list = []
|
||||
|
||||
# Get all the sequences to render. If there are subsequences,
|
||||
# add them and their frame ranges to the render list. We also
|
||||
# use the names for the output paths.
|
||||
for seq in sequences:
|
||||
subscenes = pipeline.get_subsequences(seq.get('sequence'))
|
||||
|
||||
if subscenes:
|
||||
for sub_seq in subscenes:
|
||||
sequences.append({
|
||||
"sequence": sub_seq.get_sequence(),
|
||||
"output": (f"{seq.get('output')}/"
|
||||
f"{sub_seq.get_sequence().get_name()}"),
|
||||
"frame_range": (
|
||||
sub_seq.get_start_frame(), sub_seq.get_end_frame())
|
||||
})
|
||||
else:
|
||||
# Avoid rendering camera sequences
|
||||
if "_camera" not in seq.get('sequence').get_name():
|
||||
render_list.append(seq)
|
||||
|
||||
# Create the rendering jobs and add them to the queue.
|
||||
for render_setting in render_list:
|
||||
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
|
||||
job.sequence = unreal.SoftObjectPath(i["master_sequence"])
|
||||
job.map = unreal.SoftObjectPath(i["master_level"])
|
||||
job.author = "Ayon"
|
||||
|
||||
# If we have a saved configuration, copy it to the job.
|
||||
if config:
|
||||
job.get_configuration().copy_from(config)
|
||||
|
||||
# User data could be used to pass data to the job, that can be
|
||||
# read in the job's OnJobFinished callback. We could,
|
||||
# for instance, pass the AyonPublishInstance's path to the job.
|
||||
# job.user_data = ""
|
||||
|
||||
output_dir = render_setting.get('output')
|
||||
shot_name = render_setting.get('sequence').get_name()
|
||||
|
||||
settings = job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineOutputSetting)
|
||||
settings.output_resolution = unreal.IntPoint(1920, 1080)
|
||||
settings.custom_start_frame = render_setting.get("frame_range")[0]
|
||||
settings.custom_end_frame = render_setting.get("frame_range")[1]
|
||||
settings.use_custom_playback_range = True
|
||||
settings.file_name_format = f"{shot_name}" + ".{frame_number}"
|
||||
settings.output_directory.path = f"{render_dir}/{output_dir}"
|
||||
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineDeferredPassBase)
|
||||
|
||||
render_format = data.get("unreal").get("render_format", "png")
|
||||
|
||||
if render_format == "png":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_PNG)
|
||||
elif render_format == "exr":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_EXR)
|
||||
elif render_format == "jpg":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_JPG)
|
||||
elif render_format == "bmp":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_BMP)
|
||||
|
||||
# If there are jobs in the queue, start the rendering process.
|
||||
if queue.get_jobs():
|
||||
global executor
|
||||
executor = unreal.MoviePipelinePIEExecutor()
|
||||
|
||||
preroll_frames = data.get("unreal").get("preroll_frames", 0)
|
||||
|
||||
settings = unreal.MoviePipelinePIEExecutorSettings()
|
||||
settings.set_editor_property(
|
||||
"initial_delay_frame_count", preroll_frames)
|
||||
|
||||
executor.on_executor_finished_delegate.add_callable_unique(
|
||||
_queue_finish_callback)
|
||||
executor.on_individual_job_finished_delegate.add_callable_unique(
|
||||
_job_finish_callback) # Only available on PIE Executor
|
||||
executor.execute(queue)
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
import sys
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core import (
|
||||
resources,
|
||||
style
|
||||
)
|
||||
from ayon_core.tools.utils import host_tools
|
||||
from ayon_core.tools.utils.lib import qt_app_context
|
||||
from ayon_core.hosts.unreal.api import rendering
|
||||
|
||||
|
||||
class ToolsBtnsWidget(QtWidgets.QWidget):
|
||||
"""Widget containing buttons which are clickable."""
|
||||
tool_required = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ToolsBtnsWidget, self).__init__(parent)
|
||||
|
||||
load_btn = QtWidgets.QPushButton("Load...", self)
|
||||
publish_btn = QtWidgets.QPushButton("Publisher...", self)
|
||||
manage_btn = QtWidgets.QPushButton("Manage...", self)
|
||||
render_btn = QtWidgets.QPushButton("Render...", self)
|
||||
experimental_tools_btn = QtWidgets.QPushButton(
|
||||
"Experimental tools...", self
|
||||
)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(load_btn, 0)
|
||||
layout.addWidget(publish_btn, 0)
|
||||
layout.addWidget(manage_btn, 0)
|
||||
layout.addWidget(render_btn, 0)
|
||||
layout.addWidget(experimental_tools_btn, 0)
|
||||
layout.addStretch(1)
|
||||
|
||||
load_btn.clicked.connect(self._on_load)
|
||||
publish_btn.clicked.connect(self._on_publish)
|
||||
manage_btn.clicked.connect(self._on_manage)
|
||||
render_btn.clicked.connect(self._on_render)
|
||||
experimental_tools_btn.clicked.connect(self._on_experimental)
|
||||
|
||||
def _on_create(self):
|
||||
self.tool_required.emit("creator")
|
||||
|
||||
def _on_load(self):
|
||||
self.tool_required.emit("loader")
|
||||
|
||||
def _on_publish(self):
|
||||
self.tool_required.emit("publisher")
|
||||
|
||||
def _on_manage(self):
|
||||
self.tool_required.emit("sceneinventory")
|
||||
|
||||
def _on_render(self):
|
||||
rendering.start_rendering()
|
||||
|
||||
def _on_experimental(self):
|
||||
self.tool_required.emit("experimental_tools")
|
||||
|
||||
|
||||
class ToolsDialog(QtWidgets.QDialog):
|
||||
"""Dialog with tool buttons that will stay opened until user close it."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ToolsDialog, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle("Ayon tools")
|
||||
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
tools_widget = ToolsBtnsWidget(self)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(tools_widget)
|
||||
|
||||
tools_widget.tool_required.connect(self._on_tool_require)
|
||||
self._tools_widget = tools_widget
|
||||
|
||||
self._first_show = True
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ToolsDialog, self).sizeHint()
|
||||
result.setWidth(result.width() * 2)
|
||||
return result
|
||||
|
||||
def showEvent(self, event):
|
||||
super(ToolsDialog, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self._first_show = False
|
||||
|
||||
def _on_tool_require(self, tool_name):
|
||||
host_tools.show_tool_by_name(tool_name, parent=self)
|
||||
|
||||
|
||||
class ToolsPopup(ToolsDialog):
|
||||
"""Popup with tool buttons that will close when loose focus."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ToolsPopup, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.FramelessWindowHint
|
||||
| QtCore.Qt.Popup
|
||||
)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(ToolsPopup, self).showEvent(event)
|
||||
app = QtWidgets.QApplication.instance()
|
||||
app.processEvents()
|
||||
pos = QtGui.QCursor.pos()
|
||||
self.move(pos)
|
||||
|
||||
|
||||
class WindowCache:
|
||||
"""Cached objects and methods to be used in global scope."""
|
||||
_dialog = None
|
||||
_popup = None
|
||||
_first_show = True
|
||||
|
||||
@classmethod
|
||||
def _before_show(cls):
|
||||
"""Create QApplication if does not exist yet."""
|
||||
if not cls._first_show:
|
||||
return
|
||||
|
||||
cls._first_show = False
|
||||
if not QtWidgets.QApplication.instance():
|
||||
QtWidgets.QApplication(sys.argv)
|
||||
|
||||
@classmethod
|
||||
def show_popup(cls):
|
||||
cls._before_show()
|
||||
with qt_app_context():
|
||||
if cls._popup is None:
|
||||
cls._popup = ToolsPopup()
|
||||
|
||||
cls._popup.show()
|
||||
|
||||
@classmethod
|
||||
def show_dialog(cls):
|
||||
cls._before_show()
|
||||
with qt_app_context():
|
||||
if cls._dialog is None:
|
||||
cls._dialog = ToolsDialog()
|
||||
|
||||
cls._dialog.show()
|
||||
cls._dialog.raise_()
|
||||
cls._dialog.activateWindow()
|
||||
|
||||
|
||||
def show_tools_popup():
|
||||
WindowCache.show_popup()
|
||||
|
||||
|
||||
def show_tools_dialog():
|
||||
WindowCache.show_dialog()
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Hook to launch Unreal and prepare projects."""
|
||||
import os
|
||||
import copy
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from qtpy import QtCore
|
||||
|
||||
from ayon_core import resources
|
||||
from ayon_applications import (
|
||||
PreLaunchHook,
|
||||
ApplicationLaunchFailed,
|
||||
LaunchTypes,
|
||||
)
|
||||
from ayon_core.pipeline.workfile import get_workfile_template_key
|
||||
import ayon_core.hosts.unreal.lib as unreal_lib
|
||||
from ayon_core.hosts.unreal.ue_workers import (
|
||||
UEProjectGenerationWorker,
|
||||
UEPluginInstallWorker
|
||||
)
|
||||
from ayon_core.hosts.unreal.ui import SplashScreen
|
||||
|
||||
|
||||
class UnrealPrelaunchHook(PreLaunchHook):
|
||||
"""Hook to handle launching Unreal.
|
||||
|
||||
This hook will check if current workfile path has Unreal
|
||||
project inside. IF not, it initializes it, and finally it pass
|
||||
path to the project by environment variable to Unreal launcher
|
||||
shell script.
|
||||
|
||||
"""
|
||||
app_groups = {"unreal"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.signature = f"( {self.__class__.__name__} )"
|
||||
|
||||
def _get_work_filename(self):
|
||||
# Use last workfile if was found
|
||||
if self.data.get("last_workfile_path"):
|
||||
last_workfile = Path(self.data.get("last_workfile_path"))
|
||||
if last_workfile and last_workfile.exists():
|
||||
return last_workfile.name
|
||||
|
||||
# Prepare data for fill data and for getting workfile template key
|
||||
anatomy = self.data["anatomy"]
|
||||
project_entity = self.data["project_entity"]
|
||||
|
||||
# Use already prepared workdir data
|
||||
workdir_data = copy.deepcopy(self.data["workdir_data"])
|
||||
task_type = workdir_data.get("task", {}).get("type")
|
||||
|
||||
# QUESTION raise exception if version is part of filename template?
|
||||
workdir_data["version"] = 1
|
||||
workdir_data["ext"] = "uproject"
|
||||
|
||||
# Get workfile template key for current context
|
||||
workfile_template_key = get_workfile_template_key(
|
||||
project_entity["name"],
|
||||
task_type,
|
||||
self.host_name,
|
||||
)
|
||||
# Fill templates
|
||||
template_obj = anatomy.get_template_item(
|
||||
"work", workfile_template_key, "file"
|
||||
)
|
||||
|
||||
# Return filename
|
||||
return template_obj.format_strict(workdir_data)
|
||||
|
||||
def exec_plugin_install(self, engine_path: Path, env: dict = None):
|
||||
# set up the QThread and worker with necessary signals
|
||||
env = env or os.environ
|
||||
q_thread = QtCore.QThread()
|
||||
ue_plugin_worker = UEPluginInstallWorker()
|
||||
|
||||
q_thread.started.connect(ue_plugin_worker.run)
|
||||
ue_plugin_worker.setup(engine_path, env)
|
||||
ue_plugin_worker.moveToThread(q_thread)
|
||||
|
||||
splash_screen = SplashScreen(
|
||||
"Installing plugin",
|
||||
resources.get_resource("app_icons", "ue4.png")
|
||||
)
|
||||
|
||||
# set up the splash screen with necessary triggers
|
||||
ue_plugin_worker.installing.connect(
|
||||
splash_screen.update_top_label_text
|
||||
)
|
||||
ue_plugin_worker.progress.connect(splash_screen.update_progress)
|
||||
ue_plugin_worker.log.connect(splash_screen.append_log)
|
||||
ue_plugin_worker.finished.connect(splash_screen.quit_and_close)
|
||||
ue_plugin_worker.failed.connect(splash_screen.fail)
|
||||
|
||||
splash_screen.start_thread(q_thread)
|
||||
splash_screen.show_ui()
|
||||
|
||||
if not splash_screen.was_proc_successful():
|
||||
raise ApplicationLaunchFailed("Couldn't run the application! "
|
||||
"Plugin failed to install!")
|
||||
|
||||
def exec_ue_project_gen(self,
|
||||
engine_version: str,
|
||||
unreal_project_name: str,
|
||||
engine_path: Path,
|
||||
project_dir: Path):
|
||||
self.log.info((
|
||||
f"{self.signature} Creating unreal "
|
||||
f"project [ {unreal_project_name} ]"
|
||||
))
|
||||
|
||||
q_thread = QtCore.QThread()
|
||||
ue_project_worker = UEProjectGenerationWorker()
|
||||
ue_project_worker.setup(
|
||||
engine_version,
|
||||
self.data["project_name"],
|
||||
unreal_project_name,
|
||||
engine_path,
|
||||
project_dir
|
||||
)
|
||||
ue_project_worker.moveToThread(q_thread)
|
||||
q_thread.started.connect(ue_project_worker.run)
|
||||
|
||||
splash_screen = SplashScreen(
|
||||
"Initializing UE project",
|
||||
resources.get_resource("app_icons", "ue4.png")
|
||||
)
|
||||
|
||||
ue_project_worker.stage_begin.connect(
|
||||
splash_screen.update_top_label_text
|
||||
)
|
||||
ue_project_worker.progress.connect(splash_screen.update_progress)
|
||||
ue_project_worker.log.connect(splash_screen.append_log)
|
||||
ue_project_worker.finished.connect(splash_screen.quit_and_close)
|
||||
ue_project_worker.failed.connect(splash_screen.fail)
|
||||
|
||||
splash_screen.start_thread(q_thread)
|
||||
splash_screen.show_ui()
|
||||
|
||||
if not splash_screen.was_proc_successful():
|
||||
raise ApplicationLaunchFailed("Couldn't run the application! "
|
||||
"Failed to generate the project!")
|
||||
|
||||
def execute(self):
|
||||
"""Hook entry method."""
|
||||
workdir = self.launch_context.env["AYON_WORKDIR"]
|
||||
executable = str(self.launch_context.executable)
|
||||
engine_version = self.app_name.split("/")[-1].replace("-", ".")
|
||||
try:
|
||||
if int(engine_version.split(".")[0]) < 4 and \
|
||||
int(engine_version.split(".")[1]) < 26:
|
||||
raise ApplicationLaunchFailed((
|
||||
f"{self.signature} Old unsupported version of UE "
|
||||
f"detected - {engine_version}"))
|
||||
except ValueError:
|
||||
# there can be string in minor version and in that case
|
||||
# int cast is failing. This probably happens only with
|
||||
# early access versions and is of no concert for this check
|
||||
# so let's keep it quiet.
|
||||
...
|
||||
|
||||
unreal_project_filename = self._get_work_filename()
|
||||
unreal_project_name = os.path.splitext(unreal_project_filename)[0]
|
||||
# Unreal is sensitive about project names longer then 20 chars
|
||||
if len(unreal_project_name) > 20:
|
||||
raise ApplicationLaunchFailed(
|
||||
f"Project name exceeds 20 characters ({unreal_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 unreal_project_name[:1].isalpha():
|
||||
self.log.warning((
|
||||
"Project name doesn't start with alphabet "
|
||||
f"character ({unreal_project_name}). Appending 'P'"
|
||||
))
|
||||
unreal_project_name = f"P{unreal_project_name}"
|
||||
unreal_project_filename = f'{unreal_project_name}.uproject'
|
||||
|
||||
project_path = Path(os.path.join(workdir, unreal_project_name))
|
||||
|
||||
self.log.info((
|
||||
f"{self.signature} requested UE version: "
|
||||
f"[ {engine_version} ]"
|
||||
))
|
||||
|
||||
project_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# engine_path points to the specific Unreal Engine root
|
||||
# so, we are going up from the executable itself 3 levels.
|
||||
engine_path: Path = Path(executable).parents[3]
|
||||
|
||||
# Check if new env variable exists, and if it does, if the path
|
||||
# actually contains the plugin. If not, install it.
|
||||
|
||||
built_plugin_path = self.launch_context.env.get(
|
||||
"AYON_BUILT_UNREAL_PLUGIN", None)
|
||||
|
||||
if unreal_lib.check_built_plugin_existance(built_plugin_path):
|
||||
self.log.info((
|
||||
f"{self.signature} using existing built Ayon plugin from "
|
||||
f"{built_plugin_path}"
|
||||
))
|
||||
unreal_lib.copy_built_plugin(engine_path, Path(built_plugin_path))
|
||||
else:
|
||||
# Set "AYON_UNREAL_PLUGIN" to current process environment for
|
||||
# execution of `create_unreal_project`
|
||||
env_key = "AYON_UNREAL_PLUGIN"
|
||||
if self.launch_context.env.get(env_key):
|
||||
self.log.info((
|
||||
f"{self.signature} using Ayon plugin from "
|
||||
f"{self.launch_context.env.get(env_key)}"
|
||||
))
|
||||
if self.launch_context.env.get(env_key):
|
||||
os.environ[env_key] = self.launch_context.env[env_key]
|
||||
|
||||
if not unreal_lib.check_plugin_existence(engine_path):
|
||||
self.exec_plugin_install(engine_path)
|
||||
|
||||
project_file = project_path / unreal_project_filename
|
||||
|
||||
if not project_file.is_file():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.exec_ue_project_gen(engine_version,
|
||||
unreal_project_name,
|
||||
engine_path,
|
||||
Path(temp_dir))
|
||||
try:
|
||||
self.log.info((
|
||||
f"Moving from {temp_dir} to "
|
||||
f"{project_path.as_posix()}"
|
||||
))
|
||||
shutil.copytree(
|
||||
temp_dir, project_path, dirs_exist_ok=True)
|
||||
|
||||
except shutil.Error as e:
|
||||
raise ApplicationLaunchFailed((
|
||||
f"{self.signature} Cannot copy directory {temp_dir} "
|
||||
f"to {project_path.as_posix()} - {e}"
|
||||
)) from e
|
||||
|
||||
self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version
|
||||
# Append project file to launch arguments
|
||||
self.launch_context.launch_args.append(
|
||||
f"\"{project_file.as_posix()}\"")
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 04b35dbf5fc42d905281fc30d3a22b139c1855e5
|
||||
|
|
@ -1,551 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Unreal launching and project tools."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from collections import OrderedDict
|
||||
from distutils import dir_util
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
|
||||
def get_engine_versions(env=None):
|
||||
"""Detect Unreal Engine versions.
|
||||
|
||||
This will try to detect location and versions of installed Unreal Engine.
|
||||
Location can be overridden by `UNREAL_ENGINE_LOCATION` environment
|
||||
variable.
|
||||
|
||||
.. deprecated:: 3.15.4
|
||||
|
||||
Args:
|
||||
env (dict, optional): Environment to use.
|
||||
|
||||
Returns:
|
||||
OrderedDict: dictionary with version as a key and dir as value.
|
||||
so the highest version is first.
|
||||
|
||||
Example:
|
||||
>>> get_engine_versions()
|
||||
{
|
||||
"4.23": "C:/Epic Games/UE_4.23",
|
||||
"4.24": "C:/Epic Games/UE_4.24"
|
||||
}
|
||||
|
||||
"""
|
||||
env = env or os.environ
|
||||
engine_locations = {}
|
||||
try:
|
||||
root, dirs, _ = next(os.walk(env["UNREAL_ENGINE_LOCATION"]))
|
||||
|
||||
for directory in dirs:
|
||||
if directory.startswith("UE"):
|
||||
try:
|
||||
ver = re.split(r"[-_]", directory)[1]
|
||||
except IndexError:
|
||||
continue
|
||||
engine_locations[ver] = os.path.join(root, directory)
|
||||
except KeyError:
|
||||
# environment variable not set
|
||||
pass
|
||||
except OSError:
|
||||
# specified directory doesn't exist
|
||||
pass
|
||||
except StopIteration:
|
||||
# specified directory doesn't exist
|
||||
pass
|
||||
|
||||
# if we've got something, terminate auto-detection process
|
||||
if engine_locations:
|
||||
return OrderedDict(sorted(engine_locations.items()))
|
||||
|
||||
# else kick in platform specific detection
|
||||
if platform.system().lower() == "windows":
|
||||
return OrderedDict(sorted(_win_get_engine_versions().items()))
|
||||
if 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
|
||||
if platform.system().lower() == "darwin":
|
||||
return OrderedDict(sorted(_darwin_get_engine_version().items()))
|
||||
|
||||
return OrderedDict()
|
||||
|
||||
|
||||
def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path:
|
||||
"""Get UE Editor executable path."""
|
||||
ue_path = engine_path / "Engine/Binaries"
|
||||
|
||||
ue_name = "UnrealEditor"
|
||||
|
||||
# handle older versions of Unreal Engine
|
||||
if engine_version.split(".")[0] == "4":
|
||||
ue_name = "UE4Editor"
|
||||
|
||||
if platform.system().lower() == "windows":
|
||||
ue_path /= f"Win64/{ue_name}.exe"
|
||||
|
||||
elif platform.system().lower() == "linux":
|
||||
ue_path /= f"Linux/{ue_name}"
|
||||
|
||||
elif platform.system().lower() == "darwin":
|
||||
ue_path /= f"Mac/{ue_name}"
|
||||
|
||||
return ue_path
|
||||
|
||||
|
||||
def _win_get_engine_versions():
|
||||
"""Get Unreal Engine versions on Windows.
|
||||
|
||||
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`
|
||||
|
||||
.. deprecated:: 3.15.4
|
||||
|
||||
Returns:
|
||||
dict: version as a key and path as a value.
|
||||
|
||||
"""
|
||||
install_json_path = os.path.join(
|
||||
os.getenv("PROGRAMDATA"),
|
||||
"Epic",
|
||||
"UnrealEngineLauncher",
|
||||
"LauncherInstalled.dat",
|
||||
)
|
||||
|
||||
return _parse_launcher_locations(install_json_path)
|
||||
|
||||
|
||||
def _darwin_get_engine_version() -> dict:
|
||||
"""Get Unreal Engine versions on MacOS.
|
||||
|
||||
It works the same as on Windows, just JSON file location is different.
|
||||
|
||||
.. deprecated:: 3.15.4
|
||||
|
||||
Returns:
|
||||
dict: version as a key and path as a value.
|
||||
|
||||
See Also:
|
||||
:func:`_win_get_engine_versions`.
|
||||
|
||||
"""
|
||||
install_json_path = os.path.join(
|
||||
os.getenv("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.
|
||||
|
||||
.. deprecated:: 3.15.4
|
||||
|
||||
Args:
|
||||
install_json_path (str): Path to `LauncherInstalled.dat`.
|
||||
|
||||
Returns:
|
||||
dict: with unreal engine versions as keys and
|
||||
paths to those engine installations as value.
|
||||
|
||||
"""
|
||||
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 as e:
|
||||
raise Exception(
|
||||
"Invalid `LauncherInstalled.dat file. `"
|
||||
"Cannot determine Unreal Engine location."
|
||||
) from e
|
||||
|
||||
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,
|
||||
unreal_project_name: str,
|
||||
ue_version: str,
|
||||
pr_dir: Path,
|
||||
engine_path: Path,
|
||||
dev_mode: bool = False,
|
||||
env: dict = None) -> None:
|
||||
"""This will create `.uproject` file at specified location.
|
||||
|
||||
As there is no way I know to create a project via command line, this is
|
||||
easiest option. Unreal project file is basically a JSON file. If we find
|
||||
the `AYON_UNREAL_PLUGIN` environment variable we assume this is the
|
||||
location of the Integration Plugin and we copy its content to the project
|
||||
folder and enable this plugin.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of the project in AYON.
|
||||
unreal_project_name (str): Name of the project in Unreal.
|
||||
ue_version (str): Unreal engine version (like 4.23).
|
||||
pr_dir (Path): Path to directory where project will be created.
|
||||
engine_path (Path): Path to Unreal Engine installation.
|
||||
dev_mode (bool, optional): 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 in Settings.
|
||||
env (dict, optional): Environment to use. If not set, `os.environ`.
|
||||
|
||||
Throws:
|
||||
NotImplementedError: For unsupported platforms.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Deprecated:
|
||||
since 3.16.0
|
||||
|
||||
"""
|
||||
|
||||
preset = get_project_settings(project_name)["unreal"]["project_setup"]
|
||||
# get unreal engine identifier
|
||||
# -------------------------------------------------------------------------
|
||||
# FIXME (antirotor): As of 4.26 this is problem with UE4 built from
|
||||
# sources. In that case Engine ID is calculated per machine/user and not
|
||||
# from Engine files as this code then reads. This then prevents UE4
|
||||
# to directly open project as it will complain about project being
|
||||
# created in different UE4 version. When user convert such project
|
||||
# to his UE4 version, Engine ID is replaced in uproject file. If some
|
||||
# other user tries to open it, it will present him with similar error.
|
||||
|
||||
# engine_path should be the location of UE_X.X folder
|
||||
|
||||
ue_editor_exe: Path = get_editor_exe_path(engine_path, ue_version)
|
||||
cmdlet_project: Path = get_path_to_cmdlet_project(ue_version)
|
||||
|
||||
project_file = pr_dir / f"{unreal_project_name}.uproject"
|
||||
|
||||
print("--- Generating a new project ...")
|
||||
commandlet_cmd = [
|
||||
ue_editor_exe.as_posix(),
|
||||
cmdlet_project.as_posix(),
|
||||
"-run=AyonGenerateProject",
|
||||
project_file.resolve().as_posix()
|
||||
]
|
||||
|
||||
if dev_mode or preset["dev_mode"]:
|
||||
commandlet_cmd.append('-GenerateCode')
|
||||
|
||||
gen_process = subprocess.Popen(commandlet_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
for line in gen_process.stdout:
|
||||
print(line.decode(), end='')
|
||||
gen_process.stdout.close()
|
||||
return_code = gen_process.wait()
|
||||
|
||||
if return_code and return_code != 0:
|
||||
raise RuntimeError(
|
||||
(f"Failed to generate '{unreal_project_name}' project! "
|
||||
f"Exited with return code {return_code}"))
|
||||
|
||||
print("--- Project has been generated successfully.")
|
||||
|
||||
with open(project_file.as_posix(), mode="r+") as pf:
|
||||
pf_json = json.load(pf)
|
||||
pf_json["EngineAssociation"] = get_build_id(engine_path, ue_version)
|
||||
pf.seek(0)
|
||||
json.dump(pf_json, pf, indent=4)
|
||||
pf.truncate()
|
||||
print("--- Engine ID has been written into the project file")
|
||||
|
||||
if dev_mode or preset["dev_mode"]:
|
||||
u_build_tool = get_path_to_ubt(engine_path, ue_version)
|
||||
|
||||
arch = "Win64"
|
||||
if platform.system().lower() == "windows":
|
||||
arch = "Win64"
|
||||
elif platform.system().lower() == "linux":
|
||||
arch = "Linux"
|
||||
elif platform.system().lower() == "darwin":
|
||||
# we need to test this out
|
||||
arch = "Mac"
|
||||
|
||||
command1 = [
|
||||
u_build_tool.as_posix(),
|
||||
"-projectfiles",
|
||||
f"-project={project_file}",
|
||||
"-progress"
|
||||
]
|
||||
|
||||
subprocess.run(command1)
|
||||
|
||||
command2 = [
|
||||
u_build_tool.as_posix(),
|
||||
f"-ModuleWithSuffix={unreal_project_name},3555",
|
||||
arch,
|
||||
"Development",
|
||||
"-TargetType=Editor",
|
||||
f"-Project={project_file}",
|
||||
project_file,
|
||||
"-IgnoreJunk"
|
||||
]
|
||||
|
||||
subprocess.run(command2)
|
||||
|
||||
# ensure we have PySide2 installed in engine
|
||||
python_path = None
|
||||
if platform.system().lower() == "windows":
|
||||
python_path = engine_path / ("Engine/Binaries/ThirdParty/"
|
||||
"Python3/Win64/python.exe")
|
||||
|
||||
if platform.system().lower() == "linux":
|
||||
python_path = engine_path / ("Engine/Binaries/ThirdParty/"
|
||||
"Python3/Linux/bin/python3")
|
||||
|
||||
if platform.system().lower() == "darwin":
|
||||
python_path = engine_path / ("Engine/Binaries/ThirdParty/"
|
||||
"Python3/Mac/bin/python3")
|
||||
|
||||
if not python_path:
|
||||
raise NotImplementedError("Unsupported platform")
|
||||
if not python_path.exists():
|
||||
raise RuntimeError(f"Unreal Python not found at {python_path}")
|
||||
subprocess.check_call(
|
||||
[python_path.as_posix(), "-m", "pip", "install", "pyside2"])
|
||||
|
||||
|
||||
def get_path_to_uat(engine_path: Path) -> Path:
|
||||
if platform.system().lower() == "windows":
|
||||
return engine_path / "Engine/Build/BatchFiles/RunUAT.bat"
|
||||
|
||||
if platform.system().lower() in ["linux", "darwin"]:
|
||||
return engine_path / "Engine/Build/BatchFiles/RunUAT.sh"
|
||||
|
||||
|
||||
def get_compatible_integration(
|
||||
ue_version: str, integration_root: Path) -> List[Path]:
|
||||
"""Get path to compatible version of integration plugin.
|
||||
|
||||
This will try to get the closest compatible versions to the one
|
||||
specified in sorted list.
|
||||
|
||||
Args:
|
||||
ue_version (str): version of the current Unreal Engine.
|
||||
integration_root (Path): path to built-in integration plugins.
|
||||
|
||||
Returns:
|
||||
list of Path: Sorted list of paths closest to the specified
|
||||
version.
|
||||
|
||||
"""
|
||||
major, minor = ue_version.split(".")
|
||||
integration_paths = [p for p in integration_root.iterdir()
|
||||
if p.is_dir()]
|
||||
|
||||
compatible_versions = []
|
||||
for i in integration_paths:
|
||||
# parse version from path
|
||||
try:
|
||||
i_major, i_minor = re.search(
|
||||
r"(?P<major>\d+).(?P<minor>\d+)$", i.name).groups()
|
||||
except AttributeError:
|
||||
# in case there is no match, just skip to next
|
||||
continue
|
||||
|
||||
# consider versions with different major so different that they
|
||||
# are incompatible
|
||||
if int(major) != int(i_major):
|
||||
continue
|
||||
|
||||
compatible_versions.append(i)
|
||||
|
||||
sorted(set(compatible_versions))
|
||||
return compatible_versions
|
||||
|
||||
|
||||
def get_path_to_cmdlet_project(ue_version: str) -> Path:
|
||||
cmd_project = Path(
|
||||
os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# For now, only tested on Windows (For Linux and Mac
|
||||
# it has to be implemented)
|
||||
cmd_project /= f"integration/UE_{ue_version}"
|
||||
|
||||
# if the integration doesn't exist for current engine version
|
||||
# try to find the closest to it.
|
||||
if cmd_project.exists():
|
||||
return cmd_project / "CommandletProject/CommandletProject.uproject"
|
||||
|
||||
if compatible_versions := get_compatible_integration(
|
||||
ue_version, cmd_project.parent
|
||||
):
|
||||
return compatible_versions[-1] / "CommandletProject/CommandletProject.uproject" # noqa: E501
|
||||
else:
|
||||
raise RuntimeError(
|
||||
("There are no compatible versions of Unreal "
|
||||
"integration plugin compatible with running version "
|
||||
f"of Unreal Engine {ue_version}"))
|
||||
|
||||
|
||||
def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path:
|
||||
u_build_tool_path = engine_path / "Engine/Binaries/DotNET"
|
||||
|
||||
if ue_version.split(".")[0] == "4":
|
||||
u_build_tool_path /= "UnrealBuildTool.exe"
|
||||
elif ue_version.split(".")[0] == "5":
|
||||
u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe"
|
||||
|
||||
return Path(u_build_tool_path)
|
||||
|
||||
|
||||
def get_build_id(engine_path: Path, ue_version: str) -> str:
|
||||
ue_modules = Path()
|
||||
if platform.system().lower() == "windows":
|
||||
ue_modules_path = engine_path / "Engine/Binaries/Win64"
|
||||
if ue_version.split(".")[0] == "4":
|
||||
ue_modules_path /= "UE4Editor.modules"
|
||||
elif ue_version.split(".")[0] == "5":
|
||||
ue_modules_path /= "UnrealEditor.modules"
|
||||
ue_modules = Path(ue_modules_path)
|
||||
|
||||
if platform.system().lower() == "linux":
|
||||
ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
|
||||
"Linux", "UE4Editor.modules"))
|
||||
|
||||
if platform.system().lower() == "darwin":
|
||||
ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
|
||||
"Mac", "UE4Editor.modules"))
|
||||
|
||||
if ue_modules.exists():
|
||||
print("--- Loading Engine ID from modules file ...")
|
||||
with open(ue_modules, "r") as mp:
|
||||
loaded_modules = json.load(mp)
|
||||
|
||||
if loaded_modules.get("BuildId"):
|
||||
return "{" + loaded_modules.get("BuildId") + "}"
|
||||
|
||||
|
||||
def check_built_plugin_existance(plugin_path) -> bool:
|
||||
if not plugin_path:
|
||||
return False
|
||||
|
||||
integration_plugin_path = Path(plugin_path)
|
||||
|
||||
if not integration_plugin_path.is_dir():
|
||||
raise RuntimeError("Path to the integration plugin is null!")
|
||||
|
||||
if not (integration_plugin_path / "Binaries").is_dir() \
|
||||
or not (integration_plugin_path / "Intermediate").is_dir():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def copy_built_plugin(engine_path: Path, plugin_path: Path) -> None:
|
||||
ayon_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon"
|
||||
|
||||
if not ayon_plugin_path.is_dir():
|
||||
ayon_plugin_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
engine_plugin_config_path: Path = ayon_plugin_path / "Config"
|
||||
engine_plugin_config_path.mkdir(exist_ok=True)
|
||||
|
||||
dir_util._path_created = {}
|
||||
|
||||
dir_util.copy_tree(plugin_path.as_posix(), ayon_plugin_path.as_posix())
|
||||
|
||||
|
||||
def check_plugin_existence(engine_path: Path, env: dict = None) -> bool:
|
||||
env = env or os.environ
|
||||
integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", ""))
|
||||
|
||||
if not os.path.isdir(integration_plugin_path):
|
||||
raise RuntimeError("Path to the integration plugin is null!")
|
||||
|
||||
# Create a path to the plugin in the engine
|
||||
op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon"
|
||||
|
||||
if not op_plugin_path.is_dir():
|
||||
return False
|
||||
|
||||
if not (op_plugin_path / "Binaries").is_dir() \
|
||||
or not (op_plugin_path / "Intermediate").is_dir():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def try_installing_plugin(engine_path: Path, env: dict = None) -> None:
|
||||
env = env or os.environ
|
||||
|
||||
integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", ""))
|
||||
|
||||
if not os.path.isdir(integration_plugin_path):
|
||||
raise RuntimeError("Path to the integration plugin is null!")
|
||||
|
||||
# Create a path to the plugin in the engine
|
||||
op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon"
|
||||
|
||||
if not op_plugin_path.is_dir():
|
||||
op_plugin_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
engine_plugin_config_path: Path = op_plugin_path / "Config"
|
||||
engine_plugin_config_path.mkdir(exist_ok=True)
|
||||
|
||||
dir_util._path_created = {}
|
||||
|
||||
if not (op_plugin_path / "Binaries").is_dir() \
|
||||
or not (op_plugin_path / "Intermediate").is_dir():
|
||||
_build_and_move_plugin(engine_path, op_plugin_path, env)
|
||||
|
||||
|
||||
def _build_and_move_plugin(engine_path: Path,
|
||||
plugin_build_path: Path,
|
||||
env: dict = None) -> None:
|
||||
uat_path: Path = get_path_to_uat(engine_path)
|
||||
|
||||
env = env or os.environ
|
||||
integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", ""))
|
||||
|
||||
if uat_path.is_file():
|
||||
temp_dir: Path = integration_plugin_path.parent / "Temp"
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
uplugin_path: Path = integration_plugin_path / "Ayon.uplugin"
|
||||
|
||||
# in order to successfully build the plugin,
|
||||
# It must be built outside the Engine directory and then moved
|
||||
build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}',
|
||||
'BuildPlugin',
|
||||
f'-Plugin={uplugin_path.as_posix()}',
|
||||
f'-Package={temp_dir.as_posix()}']
|
||||
subprocess.run(build_plugin_cmd)
|
||||
|
||||
# Copy the contents of the 'Temp' dir into the
|
||||
# 'Ayon' directory in the engine
|
||||
dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix())
|
||||
|
||||
# We need to also copy the config folder.
|
||||
# The UAT doesn't include the Config folder in the build
|
||||
plugin_install_config_path: Path = plugin_build_path / "Config"
|
||||
integration_plugin_config_path = integration_plugin_path / "Config"
|
||||
|
||||
dir_util.copy_tree(integration_plugin_config_path.as_posix(),
|
||||
plugin_install_config_path.as_posix())
|
||||
|
||||
dir_util.remove_tree(temp_dir.as_posix())
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unreal
|
||||
|
||||
from ayon_core.pipeline import CreatorError
|
||||
from ayon_core.hosts.unreal.api.pipeline import UNREAL_VERSION
|
||||
from ayon_core.hosts.unreal.api.plugin import (
|
||||
UnrealAssetCreator,
|
||||
)
|
||||
|
||||
|
||||
class CreateCamera(UnrealAssetCreator):
|
||||
"""Create Camera."""
|
||||
|
||||
identifier = "io.ayon.creators.unreal.camera"
|
||||
label = "Camera"
|
||||
product_type = "camera"
|
||||
icon = "fa.camera"
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
if pre_create_data.get("use_selection"):
|
||||
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
selection = [a.get_path_name() for a in sel_objects]
|
||||
|
||||
if len(selection) != 1:
|
||||
raise CreatorError("Please select only one object.")
|
||||
|
||||
# Add the current level path to the metadata
|
||||
if UNREAL_VERSION.major == 5:
|
||||
world = unreal.UnrealEditorSubsystem().get_editor_world()
|
||||
else:
|
||||
world = unreal.EditorLevelLibrary.get_editor_world()
|
||||
|
||||
instance_data["level"] = world.get_path_name()
|
||||
|
||||
super(CreateCamera, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ayon_core.hosts.unreal.api.plugin import (
|
||||
UnrealActorCreator,
|
||||
)
|
||||
|
||||
|
||||
class CreateLayout(UnrealActorCreator):
|
||||
"""Layout output for character rigs."""
|
||||
|
||||
identifier = "io.ayon.creators.unreal.layout"
|
||||
label = "Layout"
|
||||
product_type = "layout"
|
||||
icon = "cubes"
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unreal
|
||||
|
||||
from ayon_core.pipeline import CreatorError
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
create_folder
|
||||
)
|
||||
from ayon_core.hosts.unreal.api.plugin import (
|
||||
UnrealAssetCreator
|
||||
)
|
||||
from ayon_core.lib import UILabelDef
|
||||
|
||||
|
||||
class CreateLook(UnrealAssetCreator):
|
||||
"""Shader connections defining shape look."""
|
||||
|
||||
identifier = "io.ayon.creators.unreal.look"
|
||||
label = "Look"
|
||||
product_type = "look"
|
||||
icon = "paint-brush"
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
# We need to set this to True for the parent class to work
|
||||
pre_create_data["use_selection"] = True
|
||||
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
selection = [a.get_path_name() for a in sel_objects]
|
||||
|
||||
if len(selection) != 1:
|
||||
raise CreatorError("Please select only one asset.")
|
||||
|
||||
selected_asset = selection[0]
|
||||
|
||||
look_directory = "/Game/Ayon/Looks"
|
||||
|
||||
# Create the folder
|
||||
folder_name = create_folder(look_directory, product_name)
|
||||
path = f"{look_directory}/{folder_name}"
|
||||
|
||||
instance_data["look"] = path
|
||||
|
||||
# Create a new cube static mesh
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube")
|
||||
|
||||
# Get the mesh of the selected object
|
||||
original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset()
|
||||
materials = original_mesh.get_editor_property('static_materials')
|
||||
|
||||
pre_create_data["members"] = []
|
||||
|
||||
# Add the materials to the cube
|
||||
for material in materials:
|
||||
mat_name = material.get_editor_property('material_slot_name')
|
||||
object_path = f"{path}/{mat_name}.{mat_name}"
|
||||
unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset(
|
||||
cube.get_asset(), object_path
|
||||
)
|
||||
|
||||
# Remove the default material of the cube object
|
||||
unreal_object.get_editor_property('static_materials').pop()
|
||||
|
||||
unreal_object.add_material(
|
||||
material.get_editor_property('material_interface'))
|
||||
|
||||
pre_create_data["members"].append(object_path)
|
||||
|
||||
unreal.EditorAssetLibrary.save_asset(object_path)
|
||||
|
||||
super(CreateLook, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
UILabelDef("Select the asset from which to create the look.")
|
||||
]
|
||||
|
|
@ -1,276 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
UNREAL_VERSION,
|
||||
create_folder,
|
||||
get_subsequences,
|
||||
)
|
||||
from ayon_core.hosts.unreal.api.plugin import (
|
||||
UnrealAssetCreator
|
||||
)
|
||||
from ayon_core.lib import (
|
||||
UILabelDef,
|
||||
UISeparatorDef,
|
||||
BoolDef,
|
||||
NumberDef
|
||||
)
|
||||
|
||||
|
||||
class CreateRender(UnrealAssetCreator):
|
||||
"""Create instance for sequence for rendering"""
|
||||
|
||||
identifier = "io.ayon.creators.unreal.render"
|
||||
label = "Render"
|
||||
product_type = "render"
|
||||
icon = "eye"
|
||||
|
||||
def create_instance(
|
||||
self, instance_data, product_name, pre_create_data,
|
||||
selected_asset_path, master_seq, master_lvl, seq_data
|
||||
):
|
||||
instance_data["members"] = [selected_asset_path]
|
||||
instance_data["sequence"] = selected_asset_path
|
||||
instance_data["master_sequence"] = master_seq
|
||||
instance_data["master_level"] = master_lvl
|
||||
instance_data["output"] = seq_data.get('output')
|
||||
instance_data["frameStart"] = seq_data.get('frame_range')[0]
|
||||
instance_data["frameEnd"] = seq_data.get('frame_range')[1]
|
||||
|
||||
super(CreateRender, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
def create_with_new_sequence(
|
||||
self, product_name, instance_data, pre_create_data
|
||||
):
|
||||
# If the option to create a new level sequence is selected,
|
||||
# create a new level sequence and a master level.
|
||||
|
||||
root = "/Game/Ayon/Sequences"
|
||||
|
||||
# Create a new folder for the sequence in root
|
||||
sequence_dir_name = create_folder(root, product_name)
|
||||
sequence_dir = f"{root}/{sequence_dir_name}"
|
||||
|
||||
unreal.log_warning(f"sequence_dir: {sequence_dir}")
|
||||
|
||||
# Create the level sequence
|
||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
seq = asset_tools.create_asset(
|
||||
asset_name=product_name,
|
||||
package_path=sequence_dir,
|
||||
asset_class=unreal.LevelSequence,
|
||||
factory=unreal.LevelSequenceFactoryNew())
|
||||
|
||||
seq.set_playback_start(pre_create_data.get("start_frame"))
|
||||
seq.set_playback_end(pre_create_data.get("end_frame"))
|
||||
|
||||
pre_create_data["members"] = [seq.get_path_name()]
|
||||
|
||||
unreal.EditorAssetLibrary.save_asset(seq.get_path_name())
|
||||
|
||||
# Create the master level
|
||||
if UNREAL_VERSION.major >= 5:
|
||||
curr_level = unreal.LevelEditorSubsystem().get_current_level()
|
||||
else:
|
||||
world = unreal.EditorLevelLibrary.get_editor_world()
|
||||
levels = unreal.EditorLevelUtils.get_levels(world)
|
||||
curr_level = levels[0] if len(levels) else None
|
||||
if not curr_level:
|
||||
raise RuntimeError("No level loaded.")
|
||||
curr_level_path = curr_level.get_outer().get_path_name()
|
||||
|
||||
# If the level path does not start with "/Game/", the current
|
||||
# level is a temporary, unsaved level.
|
||||
if curr_level_path.startswith("/Game/"):
|
||||
if UNREAL_VERSION.major >= 5:
|
||||
unreal.LevelEditorSubsystem().save_current_level()
|
||||
else:
|
||||
unreal.EditorLevelLibrary.save_current_level()
|
||||
|
||||
ml_path = f"{sequence_dir}/{product_name}_MasterLevel"
|
||||
|
||||
if UNREAL_VERSION.major >= 5:
|
||||
unreal.LevelEditorSubsystem().new_level(ml_path)
|
||||
else:
|
||||
unreal.EditorLevelLibrary.new_level(ml_path)
|
||||
|
||||
seq_data = {
|
||||
"sequence": seq,
|
||||
"output": f"{seq.get_name()}",
|
||||
"frame_range": (
|
||||
seq.get_playback_start(),
|
||||
seq.get_playback_end())}
|
||||
|
||||
self.create_instance(
|
||||
instance_data, product_name, pre_create_data,
|
||||
seq.get_path_name(), seq.get_path_name(), ml_path, seq_data)
|
||||
|
||||
def create_from_existing_sequence(
|
||||
self, product_name, instance_data, pre_create_data
|
||||
):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
selection = [
|
||||
a.get_path_name() for a in sel_objects
|
||||
if a.get_class().get_name() == "LevelSequence"]
|
||||
|
||||
if len(selection) == 0:
|
||||
raise RuntimeError("Please select at least one Level Sequence.")
|
||||
|
||||
seq_data = None
|
||||
|
||||
for sel in selection:
|
||||
selected_asset = ar.get_asset_by_object_path(sel).get_asset()
|
||||
selected_asset_path = selected_asset.get_path_name()
|
||||
|
||||
# Check if the selected asset is a level sequence asset.
|
||||
if selected_asset.get_class().get_name() != "LevelSequence":
|
||||
unreal.log_warning(
|
||||
f"Skipping {selected_asset.get_name()}. It isn't a Level "
|
||||
"Sequence.")
|
||||
|
||||
if pre_create_data.get("use_hierarchy"):
|
||||
# The asset name is the the third element of the path which
|
||||
# contains the map.
|
||||
# To take the asset name, we remove from the path the prefix
|
||||
# "/Game/OpenPype/" and then we split the path by "/".
|
||||
sel_path = selected_asset_path
|
||||
asset_name = sel_path.replace(
|
||||
"/Game/Ayon/", "").split("/")[0]
|
||||
|
||||
search_path = f"/Game/Ayon/{asset_name}"
|
||||
else:
|
||||
search_path = Path(selected_asset_path).parent.as_posix()
|
||||
|
||||
# Get the master sequence and the master level.
|
||||
# There should be only one sequence and one level in the directory.
|
||||
try:
|
||||
ar_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[search_path],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(ar_filter)
|
||||
master_seq = sequences[0].get_asset().get_path_name()
|
||||
master_seq_obj = sequences[0].get_asset()
|
||||
ar_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[search_path],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(ar_filter)
|
||||
master_lvl = levels[0].get_asset().get_path_name()
|
||||
except IndexError:
|
||||
raise RuntimeError(
|
||||
"Could not find the hierarchy for the selected sequence.")
|
||||
|
||||
# If the selected asset is the master sequence, we get its data
|
||||
# and then we create the instance for the master sequence.
|
||||
# Otherwise, we cycle from the master sequence to find the selected
|
||||
# sequence and we get its data. This data will be used to create
|
||||
# the instance for the selected sequence. In particular,
|
||||
# we get the frame range of the selected sequence and its final
|
||||
# output path.
|
||||
master_seq_data = {
|
||||
"sequence": master_seq_obj,
|
||||
"output": f"{master_seq_obj.get_name()}",
|
||||
"frame_range": (
|
||||
master_seq_obj.get_playback_start(),
|
||||
master_seq_obj.get_playback_end())}
|
||||
|
||||
if (selected_asset_path == master_seq or
|
||||
pre_create_data.get("use_hierarchy")):
|
||||
seq_data = master_seq_data
|
||||
else:
|
||||
seq_data_list = [master_seq_data]
|
||||
|
||||
for seq in seq_data_list:
|
||||
subscenes = get_subsequences(seq.get('sequence'))
|
||||
|
||||
for sub_seq in subscenes:
|
||||
sub_seq_obj = sub_seq.get_sequence()
|
||||
curr_data = {
|
||||
"sequence": sub_seq_obj,
|
||||
"output": (f"{seq.get('output')}/"
|
||||
f"{sub_seq_obj.get_name()}"),
|
||||
"frame_range": (
|
||||
sub_seq.get_start_frame(),
|
||||
sub_seq.get_end_frame() - 1)}
|
||||
|
||||
# If the selected asset is the current sub-sequence,
|
||||
# we get its data and we break the loop.
|
||||
# Otherwise, we add the current sub-sequence data to
|
||||
# the list of sequences to check.
|
||||
if sub_seq_obj.get_path_name() == selected_asset_path:
|
||||
seq_data = curr_data
|
||||
break
|
||||
|
||||
seq_data_list.append(curr_data)
|
||||
|
||||
# If we found the selected asset, we break the loop.
|
||||
if seq_data is not None:
|
||||
break
|
||||
|
||||
# If we didn't find the selected asset, we don't create the
|
||||
# instance.
|
||||
if not seq_data:
|
||||
unreal.log_warning(
|
||||
f"Skipping {selected_asset.get_name()}. It isn't a "
|
||||
"sub-sequence of the master sequence.")
|
||||
continue
|
||||
|
||||
self.create_instance(
|
||||
instance_data, product_name, pre_create_data,
|
||||
selected_asset_path, master_seq, master_lvl, seq_data)
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
if pre_create_data.get("create_seq"):
|
||||
self.create_with_new_sequence(
|
||||
product_name, instance_data, pre_create_data)
|
||||
else:
|
||||
self.create_from_existing_sequence(
|
||||
product_name, instance_data, pre_create_data)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
UILabelDef(
|
||||
"Select a Level Sequence to render or create a new one."
|
||||
),
|
||||
BoolDef(
|
||||
"create_seq",
|
||||
label="Create a new Level Sequence",
|
||||
default=False
|
||||
),
|
||||
UILabelDef(
|
||||
"WARNING: If you create a new Level Sequence, the current\n"
|
||||
"level will be saved and a new Master Level will be created."
|
||||
),
|
||||
NumberDef(
|
||||
"start_frame",
|
||||
label="Start Frame",
|
||||
default=0,
|
||||
minimum=-999999,
|
||||
maximum=999999
|
||||
),
|
||||
NumberDef(
|
||||
"end_frame",
|
||||
label="Start Frame",
|
||||
default=150,
|
||||
minimum=-999999,
|
||||
maximum=999999
|
||||
),
|
||||
UISeparatorDef(),
|
||||
UILabelDef(
|
||||
"The following settings are valid only if you are not\n"
|
||||
"creating a new sequence."
|
||||
),
|
||||
BoolDef(
|
||||
"use_hierarchy",
|
||||
label="Use Hierarchy",
|
||||
default=False
|
||||
),
|
||||
]
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ayon_core.hosts.unreal.api.plugin import (
|
||||
UnrealAssetCreator,
|
||||
)
|
||||
|
||||
|
||||
class CreateStaticMeshFBX(UnrealAssetCreator):
|
||||
"""Create Static Meshes as FBX geometry."""
|
||||
|
||||
identifier = "io.ayon.creators.unreal.staticmeshfbx"
|
||||
label = "Static Mesh (FBX)"
|
||||
product_type = "unrealStaticMesh"
|
||||
icon = "cube"
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
|
||||
from ayon_core.pipeline import CreatorError
|
||||
from ayon_core.hosts.unreal.api.plugin import (
|
||||
UnrealAssetCreator,
|
||||
)
|
||||
|
||||
|
||||
class CreateUAsset(UnrealAssetCreator):
|
||||
"""Create UAsset."""
|
||||
|
||||
identifier = "io.ayon.creators.unreal.uasset"
|
||||
label = "UAsset"
|
||||
product_type = "uasset"
|
||||
icon = "cube"
|
||||
|
||||
extension = ".uasset"
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
if pre_create_data.get("use_selection"):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
selection = [a.get_path_name() for a in sel_objects]
|
||||
|
||||
if len(selection) != 1:
|
||||
raise CreatorError("Please select only one object.")
|
||||
|
||||
obj = selection[0]
|
||||
|
||||
asset = ar.get_asset_by_object_path(obj).get_asset()
|
||||
sys_path = unreal.SystemLibrary.get_system_path(asset)
|
||||
|
||||
if not sys_path:
|
||||
raise CreatorError(
|
||||
f"{Path(obj).name} is not on the disk. Likely it needs to"
|
||||
"be saved first.")
|
||||
|
||||
if Path(sys_path).suffix != self.extension:
|
||||
raise CreatorError(
|
||||
f"{Path(sys_path).name} is not a {self.label}.")
|
||||
|
||||
super(CreateUAsset, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
|
||||
class CreateUMap(CreateUAsset):
|
||||
"""Create Level."""
|
||||
|
||||
identifier = "io.ayon.creators.unreal.umap"
|
||||
label = "Level"
|
||||
product_type = "uasset"
|
||||
extension = ".umap"
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
instance_data["families"] = ["umap"]
|
||||
|
||||
super(CreateUMap, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import unreal
|
||||
|
||||
from ayon_core.hosts.unreal.api.tools_ui import qt_app_context
|
||||
from ayon_core.hosts.unreal.api.pipeline import delete_asset_if_unused
|
||||
from ayon_core.pipeline import InventoryAction
|
||||
|
||||
|
||||
class DeleteUnusedAssets(InventoryAction):
|
||||
"""Delete all the assets that are not used in any level.
|
||||
"""
|
||||
|
||||
label = "Delete Unused Assets"
|
||||
icon = "trash"
|
||||
color = "red"
|
||||
order = 1
|
||||
|
||||
dialog = None
|
||||
|
||||
def _delete_unused_assets(self, containers):
|
||||
allowed_families = ["model", "rig"]
|
||||
|
||||
for container in containers:
|
||||
container_dir = container.get("namespace")
|
||||
if container.get("family") not in allowed_families:
|
||||
unreal.log_warning(
|
||||
f"Container {container_dir} is not supported.")
|
||||
continue
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
container_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
delete_asset_if_unused(container, asset_content)
|
||||
|
||||
def _show_confirmation_dialog(self, containers):
|
||||
from qtpy import QtCore
|
||||
from ayon_core.tools.utils import SimplePopup
|
||||
from ayon_core.style import load_stylesheet
|
||||
|
||||
dialog = SimplePopup()
|
||||
dialog.setWindowFlags(
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
dialog.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
dialog.setWindowTitle("Delete all unused assets")
|
||||
dialog.set_message(
|
||||
"You are about to delete all the assets in the project that \n"
|
||||
"are not used in any level. Are you sure you want to continue?"
|
||||
)
|
||||
dialog.set_button_text("Delete")
|
||||
|
||||
dialog.on_clicked.connect(
|
||||
lambda: self._delete_unused_assets(containers)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
dialog.setStyleSheet(load_stylesheet())
|
||||
|
||||
self.dialog = dialog
|
||||
|
||||
def process(self, containers):
|
||||
with qt_app_context():
|
||||
self._show_confirmation_dialog(containers)
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import unreal
|
||||
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
ls,
|
||||
replace_static_mesh_actors,
|
||||
replace_skeletal_mesh_actors,
|
||||
replace_geometry_cache_actors,
|
||||
)
|
||||
from ayon_core.pipeline import InventoryAction
|
||||
|
||||
|
||||
def update_assets(containers, selected):
|
||||
allowed_families = ["model", "rig"]
|
||||
|
||||
# Get all the containers in the Unreal Project
|
||||
all_containers = ls()
|
||||
|
||||
for container in containers:
|
||||
container_dir = container.get("namespace")
|
||||
if container.get("family") not in allowed_families:
|
||||
unreal.log_warning(
|
||||
f"Container {container_dir} is not supported.")
|
||||
continue
|
||||
|
||||
# Get all containers with same asset_name but different objectName.
|
||||
# These are the containers that need to be updated in the level.
|
||||
sa_containers = [
|
||||
i
|
||||
for i in all_containers
|
||||
if (
|
||||
i.get("asset_name") == container.get("asset_name") and
|
||||
i.get("objectName") != container.get("objectName")
|
||||
)
|
||||
]
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
container_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
# Update all actors in level
|
||||
for sa_cont in sa_containers:
|
||||
sa_dir = sa_cont.get("namespace")
|
||||
old_content = unreal.EditorAssetLibrary.list_assets(
|
||||
sa_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
if container.get("family") == "rig":
|
||||
replace_skeletal_mesh_actors(
|
||||
old_content, asset_content, selected)
|
||||
replace_static_mesh_actors(
|
||||
old_content, asset_content, selected)
|
||||
elif container.get("family") == "model":
|
||||
if container.get("loader") == "PointCacheAlembicLoader":
|
||||
replace_geometry_cache_actors(
|
||||
old_content, asset_content, selected)
|
||||
else:
|
||||
replace_static_mesh_actors(
|
||||
old_content, asset_content, selected)
|
||||
|
||||
unreal.EditorLevelLibrary.save_current_level()
|
||||
|
||||
|
||||
class UpdateAllActors(InventoryAction):
|
||||
"""Update all the Actors in the current level to the version of the asset
|
||||
selected in the scene manager.
|
||||
"""
|
||||
|
||||
label = "Replace all Actors in level to this version"
|
||||
icon = "arrow-up"
|
||||
|
||||
def process(self, containers):
|
||||
update_assets(containers, False)
|
||||
|
||||
|
||||
class UpdateSelectedActors(InventoryAction):
|
||||
"""Update only the selected Actors in the current level to the version
|
||||
of the asset selected in the scene manager.
|
||||
"""
|
||||
|
||||
label = "Replace selected Actors in level to this version"
|
||||
icon = "arrow-up"
|
||||
|
||||
def process(self, containers):
|
||||
update_assets(containers, True)
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load Alembic Animation."""
|
||||
import os
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class AnimationAlembicLoader(plugin.Loader):
|
||||
"""Load Unreal SkeletalMesh from Alembic"""
|
||||
|
||||
product_types = {"animation"}
|
||||
label = "Import Alembic Animation"
|
||||
representations = {"abc"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
def get_task(self, filename, asset_dir, asset_name, replace):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.AbcImportSettings()
|
||||
sm_settings = unreal.AbcStaticMeshSettings()
|
||||
conversion_settings = unreal.AbcConversionSettings(
|
||||
preset=unreal.AbcConversionPreset.CUSTOM,
|
||||
flip_u=False, flip_v=False,
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, 1.0, -1.0])
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
options.set_editor_property(
|
||||
'import_type', unreal.AlembicImportType.SKELETAL)
|
||||
|
||||
options.static_mesh_settings = sm_settings
|
||||
options.conversion_settings = conversion_settings
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
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): Product 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
|
||||
"""
|
||||
|
||||
# Create directory for asset and ayon container
|
||||
root = unreal_pipeline.AYON_ASSET_DIR
|
||||
folder_name = context["folder"]["name"]
|
||||
folder_path = context["folder"]["path"]
|
||||
product_type = context["product"]["productType"]
|
||||
suffix = "_CON"
|
||||
if folder_name:
|
||||
asset_name = "{}_{}".format(folder_name, name)
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
version = context["version"]["version"]
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version:03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
task = self.get_task(path, asset_dir, asset_name, False)
|
||||
|
||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
asset_tools.import_asset_tasks([task])
|
||||
|
||||
# Create Asset Container
|
||||
unreal_pipeline.create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"folder_path": folder_path,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["id"],
|
||||
"parent": context["representation"]["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these should be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
unreal_pipeline.imprint(
|
||||
f"{asset_dir}/{container_name}", data)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
folder_name = container["asset_name"]
|
||||
repre_entity = context["representation"]
|
||||
source_path = get_representation_path(repre_entity)
|
||||
destination_path = container["namespace"]
|
||||
|
||||
task = self.get_task(
|
||||
source_path, destination_path, folder_name, True
|
||||
)
|
||||
|
||||
# do import fbx and replace existing data
|
||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
asset_tools.import_asset_tasks([task])
|
||||
|
||||
container_path = f"{container['namespace']}/{container['objectName']}"
|
||||
|
||||
# update metadata
|
||||
unreal_pipeline.imprint(
|
||||
container_path,
|
||||
{
|
||||
"representation": repre_entity["id"],
|
||||
"parent": repre_entity["versionId"],
|
||||
})
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
destination_path, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,337 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load FBX with animations."""
|
||||
import os
|
||||
import json
|
||||
|
||||
import unreal
|
||||
from unreal import EditorAssetLibrary
|
||||
from unreal import MovieSceneSkeletalAnimationTrack
|
||||
from unreal import MovieSceneSkeletalAnimationSection
|
||||
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline
|
||||
|
||||
|
||||
class AnimationFBXLoader(plugin.Loader):
|
||||
"""Load Unreal SkeletalMesh from FBX."""
|
||||
|
||||
product_types = {"animation"}
|
||||
label = "Import FBX Animation"
|
||||
representations = {"fbx"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
def _process(self, path, asset_dir, asset_name, instance_name):
|
||||
automated = False
|
||||
actor = None
|
||||
|
||||
task = unreal.AssetImportTask()
|
||||
task.options = unreal.FbxImportUI()
|
||||
|
||||
if instance_name:
|
||||
automated = True
|
||||
# Old method to get the actor
|
||||
# actor_name = 'PersistentLevel.' + instance_name
|
||||
# actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
for a in actors:
|
||||
if a.get_class().get_name() != "SkeletalMeshActor":
|
||||
continue
|
||||
if a.get_actor_label() == instance_name:
|
||||
actor = a
|
||||
break
|
||||
if not actor:
|
||||
raise Exception(f"Could not find actor {instance_name}")
|
||||
skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton
|
||||
task.options.set_editor_property('skeleton', skeleton)
|
||||
|
||||
if not actor:
|
||||
return None
|
||||
|
||||
folder_entity = get_current_folder_entity(fields=["attrib.fps"])
|
||||
|
||||
task.set_editor_property('filename', path)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', False)
|
||||
task.set_editor_property('automated', automated)
|
||||
task.set_editor_property('save', False)
|
||||
|
||||
# set import options here
|
||||
task.options.set_editor_property(
|
||||
'automated_import_should_detect_type', False)
|
||||
task.options.set_editor_property(
|
||||
'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)
|
||||
task.options.set_editor_property(
|
||||
'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)
|
||||
task.options.set_editor_property('import_mesh', False)
|
||||
task.options.set_editor_property('import_animations', True)
|
||||
task.options.set_editor_property('override_full_name', True)
|
||||
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'animation_length',
|
||||
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
|
||||
)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_meshes_in_bone_hierarchy', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'use_default_sample_rate', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'custom_sample_rate', folder_entity.get("attrib", {}).get("fps"))
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_custom_attribute', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_bone_tracks', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'remove_redundant_keys', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'convert_scene', True)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
animation = None
|
||||
|
||||
for a in asset_content:
|
||||
imported_asset_data = EditorAssetLibrary.find_asset_data(a)
|
||||
imported_asset = unreal.AssetRegistryHelpers.get_asset(
|
||||
imported_asset_data)
|
||||
if imported_asset.__class__ == unreal.AnimSequence:
|
||||
animation = imported_asset
|
||||
break
|
||||
|
||||
if animation:
|
||||
animation.set_editor_property('enable_root_motion', True)
|
||||
actor.skeletal_mesh_component.set_editor_property(
|
||||
'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)
|
||||
actor.skeletal_mesh_component.animation_data.set_editor_property(
|
||||
'anim_to_play', animation)
|
||||
|
||||
return animation
|
||||
|
||||
def load(self, context, name, namespace, options=None):
|
||||
"""
|
||||
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): Product 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
|
||||
"""
|
||||
# Create directory for asset and Ayon container
|
||||
root = "/Game/Ayon"
|
||||
folder_path = context["folder"]["path"]
|
||||
hierarchy = folder_path.lstrip("/").split("/")
|
||||
folder_name = hierarchy.pop(-1)
|
||||
product_type = context["product"]["productType"]
|
||||
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/Animations/{folder_name}/{name}", suffix="")
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[f"{root}/{hierarchy[0]}"],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(_filter)
|
||||
master_level = levels[0].get_asset().get_path_name()
|
||||
|
||||
hierarchy_dir = root
|
||||
for h in hierarchy:
|
||||
hierarchy_dir = f"{hierarchy_dir}/{h}"
|
||||
hierarchy_dir = f"{hierarchy_dir}/{folder_name}"
|
||||
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[f"{hierarchy_dir}/"],
|
||||
recursive_paths=True)
|
||||
levels = ar.get_assets(_filter)
|
||||
level = levels[0].get_asset().get_path_name()
|
||||
|
||||
unreal.EditorLevelLibrary.save_all_dirty_levels()
|
||||
unreal.EditorLevelLibrary.load_level(level)
|
||||
|
||||
container_name += suffix
|
||||
|
||||
EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
libpath = path.replace(".fbx", ".json")
|
||||
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
instance_name = data.get("instance_name")
|
||||
|
||||
animation = self._process(path, asset_dir, asset_name, instance_name)
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
hierarchy_dir, recursive=True, include_folder=False)
|
||||
|
||||
# Get the sequence for the layout, excluding the camera one.
|
||||
sequences = [a for a in asset_content
|
||||
if (EditorAssetLibrary.find_asset_data(a).get_class() ==
|
||||
unreal.LevelSequence.static_class() and
|
||||
"_camera" not in a.split("/")[-1])]
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
for s in sequences:
|
||||
sequence = ar.get_asset_by_object_path(s).get_asset()
|
||||
possessables = [
|
||||
p for p in sequence.get_possessables()
|
||||
if p.get_display_name() == instance_name]
|
||||
|
||||
for p in possessables:
|
||||
tracks = [
|
||||
t for t in p.get_tracks()
|
||||
if (t.get_class() ==
|
||||
MovieSceneSkeletalAnimationTrack.static_class())]
|
||||
|
||||
for t in tracks:
|
||||
sections = [
|
||||
s for s in t.get_sections()
|
||||
if (s.get_class() ==
|
||||
MovieSceneSkeletalAnimationSection.static_class())]
|
||||
|
||||
for s in sections:
|
||||
s.params.set_editor_property('animation', animation)
|
||||
|
||||
# Create Asset Container
|
||||
unreal_pipeline.create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["id"],
|
||||
"parent": context["representation"]["versionId"],
|
||||
"folder_path": folder_path,
|
||||
"product_type": product_type,
|
||||
# TODO these shold be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type
|
||||
}
|
||||
unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
imported_content = EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False)
|
||||
|
||||
for a in imported_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
unreal.EditorLevelLibrary.save_current_level()
|
||||
unreal.EditorLevelLibrary.load_level(master_level)
|
||||
|
||||
def update(self, container, context):
|
||||
repre_entity = context["representation"]
|
||||
folder_name = container["asset_name"]
|
||||
source_path = get_representation_path(repre_entity)
|
||||
folder_entity = get_current_folder_entity(fields=["attrib.fps"])
|
||||
destination_path = container["namespace"]
|
||||
|
||||
task = unreal.AssetImportTask()
|
||||
task.options = unreal.FbxImportUI()
|
||||
|
||||
task.set_editor_property('filename', source_path)
|
||||
task.set_editor_property('destination_path', destination_path)
|
||||
# strip suffix
|
||||
task.set_editor_property('destination_name', folder_name)
|
||||
task.set_editor_property('replace_existing', True)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
# set import options here
|
||||
task.options.set_editor_property(
|
||||
'automated_import_should_detect_type', False)
|
||||
task.options.set_editor_property(
|
||||
'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)
|
||||
task.options.set_editor_property(
|
||||
'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)
|
||||
task.options.set_editor_property('import_mesh', False)
|
||||
task.options.set_editor_property('import_animations', True)
|
||||
task.options.set_editor_property('override_full_name', True)
|
||||
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'animation_length',
|
||||
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
|
||||
)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_meshes_in_bone_hierarchy', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'use_default_sample_rate', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'custom_sample_rate', folder_entity.get("attrib", {}).get("fps"))
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_custom_attribute', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_bone_tracks', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'remove_redundant_keys', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'convert_scene', True)
|
||||
|
||||
skeletal_mesh = EditorAssetLibrary.load_asset(
|
||||
container.get('namespace') + "/" + container.get('asset_name'))
|
||||
skeleton = skeletal_mesh.get_editor_property('skeleton')
|
||||
task.options.set_editor_property('skeleton', skeleton)
|
||||
|
||||
# do import fbx and replace existing data
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
container_path = f'{container["namespace"]}/{container["objectName"]}'
|
||||
# update metadata
|
||||
unreal_pipeline.imprint(
|
||||
container_path,
|
||||
{
|
||||
"representation": repre_entity["id"],
|
||||
"parent": repre_entity["versionId"],
|
||||
})
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
destination_path, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,591 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load camera from FBX."""
|
||||
from pathlib import Path
|
||||
|
||||
import ayon_api
|
||||
|
||||
import unreal
|
||||
from unreal import (
|
||||
EditorAssetLibrary,
|
||||
EditorLevelLibrary,
|
||||
EditorLevelUtils,
|
||||
LevelSequenceEditorBlueprintLibrary as LevelSequenceLib,
|
||||
)
|
||||
from ayon_core.pipeline import (
|
||||
AYON_CONTAINER_ID,
|
||||
get_current_project_name,
|
||||
get_representation_path,
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
generate_sequence,
|
||||
set_sequence_hierarchy,
|
||||
create_container,
|
||||
imprint,
|
||||
)
|
||||
|
||||
|
||||
class CameraLoader(plugin.Loader):
|
||||
"""Load Unreal StaticMesh from FBX"""
|
||||
|
||||
product_types = {"camera"}
|
||||
label = "Load Camera"
|
||||
representations = {"fbx"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
def _import_camera(
|
||||
self, world, sequence, bindings, import_fbx_settings, import_filename
|
||||
):
|
||||
ue_version = unreal.SystemLibrary.get_engine_version().split('.')
|
||||
ue_major = int(ue_version[0])
|
||||
ue_minor = int(ue_version[1])
|
||||
|
||||
if ue_major == 4 and ue_minor <= 26:
|
||||
unreal.SequencerTools.import_fbx(
|
||||
world,
|
||||
sequence,
|
||||
bindings,
|
||||
import_fbx_settings,
|
||||
import_filename
|
||||
)
|
||||
elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5:
|
||||
unreal.SequencerTools.import_level_sequence_fbx(
|
||||
world,
|
||||
sequence,
|
||||
bindings,
|
||||
import_fbx_settings,
|
||||
import_filename
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unreal version {ue_major} not supported")
|
||||
|
||||
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): Product 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
|
||||
"""
|
||||
|
||||
# Create directory for asset and Ayon container
|
||||
folder_entity = context["folder"]
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
folder_path = folder_entity["path"]
|
||||
hierarchy_parts = folder_path.split("/")
|
||||
# Remove empty string
|
||||
hierarchy_parts.pop(0)
|
||||
# Pop folder name
|
||||
folder_name = hierarchy_parts.pop(-1)
|
||||
|
||||
root = "/Game/Ayon"
|
||||
hierarchy_dir = root
|
||||
hierarchy_dir_list = []
|
||||
for h in hierarchy_parts:
|
||||
hierarchy_dir = f"{hierarchy_dir}/{h}"
|
||||
hierarchy_dir_list.append(hierarchy_dir)
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else name
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
|
||||
# Create a unique name for the camera directory
|
||||
unique_number = 1
|
||||
if EditorAssetLibrary.does_directory_exist(
|
||||
f"{hierarchy_dir}/{folder_name}"
|
||||
):
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
f"{root}/{folder_name}", recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
# Get highest number to make a unique name
|
||||
folders = [a for a in asset_content
|
||||
if a[-1] == "/" and f"{name}_" in a]
|
||||
# Get number from folder name. Splits the string by "_" and
|
||||
# removes the last element (which is a "/").
|
||||
f_numbers = [int(f.split("_")[-1][:-1]) for f in folders]
|
||||
f_numbers.sort()
|
||||
unique_number = f_numbers[-1] + 1 if f_numbers else 1
|
||||
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{hierarchy_dir}/{folder_name}/{name}_{unique_number:02d}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
# Create map for the shot, and create hierarchy of map. If the maps
|
||||
# already exist, we will use them.
|
||||
h_dir = hierarchy_dir_list[0]
|
||||
h_asset = hierarchy_dir[0]
|
||||
master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map"
|
||||
if not EditorAssetLibrary.does_asset_exist(master_level):
|
||||
EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map")
|
||||
|
||||
level = (
|
||||
f"{asset_dir}/{folder_name}_map_camera.{folder_name}_map_camera"
|
||||
)
|
||||
if not EditorAssetLibrary.does_asset_exist(level):
|
||||
EditorLevelLibrary.new_level(
|
||||
f"{asset_dir}/{folder_name}_map_camera"
|
||||
)
|
||||
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
EditorLevelUtils.add_level_to_world(
|
||||
EditorLevelLibrary.get_editor_world(),
|
||||
level,
|
||||
unreal.LevelStreamingDynamic
|
||||
)
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(level)
|
||||
|
||||
# Get all the sequences in the hierarchy. It will create them, if
|
||||
# they don't exist.
|
||||
frame_ranges = []
|
||||
sequences = []
|
||||
for (h_dir, h) in zip(hierarchy_dir_list, hierarchy_parts):
|
||||
root_content = EditorAssetLibrary.list_assets(
|
||||
h_dir, recursive=False, include_folder=False)
|
||||
|
||||
existing_sequences = [
|
||||
EditorAssetLibrary.find_asset_data(asset)
|
||||
for asset in root_content
|
||||
if EditorAssetLibrary.find_asset_data(
|
||||
asset).get_class().get_name() == 'LevelSequence'
|
||||
]
|
||||
|
||||
if existing_sequences:
|
||||
for seq in existing_sequences:
|
||||
sequences.append(seq.get_asset())
|
||||
frame_ranges.append((
|
||||
seq.get_asset().get_playback_start(),
|
||||
seq.get_asset().get_playback_end()))
|
||||
else:
|
||||
sequence, frame_range = generate_sequence(h, h_dir)
|
||||
|
||||
sequences.append(sequence)
|
||||
frame_ranges.append(frame_range)
|
||||
|
||||
EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
cam_seq = tools.create_asset(
|
||||
asset_name=f"{folder_name}_camera",
|
||||
package_path=asset_dir,
|
||||
asset_class=unreal.LevelSequence,
|
||||
factory=unreal.LevelSequenceFactoryNew()
|
||||
)
|
||||
|
||||
# Add sequences data to hierarchy
|
||||
for i in range(len(sequences) - 1):
|
||||
set_sequence_hierarchy(
|
||||
sequences[i], sequences[i + 1],
|
||||
frame_ranges[i][1],
|
||||
frame_ranges[i + 1][0], frame_ranges[i + 1][1],
|
||||
[level])
|
||||
|
||||
clip_in = folder_attributes.get("clipIn")
|
||||
clip_out = folder_attributes.get("clipOut")
|
||||
|
||||
cam_seq.set_display_rate(
|
||||
unreal.FrameRate(folder_attributes.get("fps"), 1.0))
|
||||
cam_seq.set_playback_start(clip_in)
|
||||
cam_seq.set_playback_end(clip_out + 1)
|
||||
set_sequence_hierarchy(
|
||||
sequences[-1], cam_seq,
|
||||
frame_ranges[-1][1],
|
||||
clip_in, clip_out,
|
||||
[level])
|
||||
|
||||
settings = unreal.MovieSceneUserImportFBXSettings()
|
||||
settings.set_editor_property('reduce_keys', False)
|
||||
|
||||
if cam_seq:
|
||||
path = self.filepath_from_context(context)
|
||||
self._import_camera(
|
||||
EditorLevelLibrary.get_editor_world(),
|
||||
cam_seq,
|
||||
cam_seq.get_bindings(),
|
||||
settings,
|
||||
path
|
||||
)
|
||||
|
||||
# Set range of all sections
|
||||
# Changing the range of the section is not enough. We need to change
|
||||
# the frame of all the keys in the section.
|
||||
for possessable in cam_seq.get_possessables():
|
||||
for tracks in possessable.get_tracks():
|
||||
for section in tracks.get_sections():
|
||||
section.set_range(clip_in, clip_out + 1)
|
||||
for channel in section.get_all_channels():
|
||||
for key in channel.get_keys():
|
||||
old_time = key.get_time().get_editor_property(
|
||||
'frame_number')
|
||||
old_time_value = old_time.get_editor_property(
|
||||
'value')
|
||||
new_time = old_time_value + (
|
||||
clip_in - folder_attributes.get('frameStart')
|
||||
)
|
||||
key.set_time(unreal.FrameNumber(value=new_time))
|
||||
|
||||
# Create Asset Container
|
||||
create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
product_type = context["product"]["productType"]
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"folder_path": folder_path,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["id"],
|
||||
"parent": context["representation"]["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these should be probably removed
|
||||
"asset": folder_name,
|
||||
"family": product_type,
|
||||
}
|
||||
imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
|
||||
# Save all assets in the hierarchy
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
hierarchy_dir_list[0], recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
curr_level_sequence = LevelSequenceLib.get_current_level_sequence()
|
||||
curr_time = LevelSequenceLib.get_current_time()
|
||||
is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport()
|
||||
|
||||
editor_subsystem = unreal.UnrealEditorSubsystem()
|
||||
vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info()
|
||||
|
||||
asset_dir = container.get('namespace')
|
||||
|
||||
EditorLevelLibrary.save_current_level()
|
||||
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(_filter)
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=True)
|
||||
maps = ar.get_assets(_filter)
|
||||
|
||||
# There should be only one map in the list
|
||||
EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name())
|
||||
|
||||
level_sequence = sequences[0].get_asset()
|
||||
|
||||
display_rate = level_sequence.get_display_rate()
|
||||
playback_start = level_sequence.get_playback_start()
|
||||
playback_end = level_sequence.get_playback_end()
|
||||
|
||||
sequence_name = f"{container.get('asset')}_camera"
|
||||
|
||||
# Get the actors in the level sequence.
|
||||
objs = unreal.SequencerTools.get_bound_objects(
|
||||
unreal.EditorLevelLibrary.get_editor_world(),
|
||||
level_sequence,
|
||||
level_sequence.get_bindings(),
|
||||
unreal.SequencerScriptingRange(
|
||||
has_start_value=True,
|
||||
has_end_value=True,
|
||||
inclusive_start=level_sequence.get_playback_start(),
|
||||
exclusive_end=level_sequence.get_playback_end()
|
||||
)
|
||||
)
|
||||
|
||||
# Delete actors from the map
|
||||
for o in objs:
|
||||
if o.bound_objects[0].get_class().get_name() == "CineCameraActor":
|
||||
actor_path = o.bound_objects[0].get_path_name().split(":")[-1]
|
||||
actor = EditorLevelLibrary.get_actor_reference(actor_path)
|
||||
EditorLevelLibrary.destroy_actor(actor)
|
||||
|
||||
# Remove the Level Sequence from the parent.
|
||||
# We need to traverse the hierarchy from the master sequence to find
|
||||
# the level sequence.
|
||||
root = "/Game/Ayon"
|
||||
namespace = container.get('namespace').replace(f"{root}/", "")
|
||||
ms_asset = namespace.split('/')[0]
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[f"{root}/{ms_asset}"],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(_filter)
|
||||
master_sequence = sequences[0].get_asset()
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[f"{root}/{ms_asset}"],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(_filter)
|
||||
master_level = levels[0].get_asset().get_path_name()
|
||||
|
||||
sequences = [master_sequence]
|
||||
|
||||
parent = None
|
||||
sub_scene = None
|
||||
for s in sequences:
|
||||
tracks = s.get_master_tracks()
|
||||
subscene_track = None
|
||||
for t in tracks:
|
||||
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
|
||||
subscene_track = t
|
||||
if subscene_track:
|
||||
sections = subscene_track.get_sections()
|
||||
for ss in sections:
|
||||
if ss.get_sequence().get_name() == sequence_name:
|
||||
parent = s
|
||||
sub_scene = ss
|
||||
break
|
||||
sequences.append(ss.get_sequence())
|
||||
for i, ss in enumerate(sections):
|
||||
ss.set_row_index(i)
|
||||
if parent:
|
||||
break
|
||||
|
||||
assert parent, "Could not find the parent sequence"
|
||||
|
||||
EditorAssetLibrary.delete_asset(level_sequence.get_path_name())
|
||||
|
||||
settings = unreal.MovieSceneUserImportFBXSettings()
|
||||
settings.set_editor_property('reduce_keys', False)
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
new_sequence = tools.create_asset(
|
||||
asset_name=sequence_name,
|
||||
package_path=asset_dir,
|
||||
asset_class=unreal.LevelSequence,
|
||||
factory=unreal.LevelSequenceFactoryNew()
|
||||
)
|
||||
|
||||
new_sequence.set_display_rate(display_rate)
|
||||
new_sequence.set_playback_start(playback_start)
|
||||
new_sequence.set_playback_end(playback_end)
|
||||
|
||||
sub_scene.set_sequence(new_sequence)
|
||||
|
||||
repre_entity = context["representation"]
|
||||
repre_path = get_representation_path(repre_entity)
|
||||
self._import_camera(
|
||||
EditorLevelLibrary.get_editor_world(),
|
||||
new_sequence,
|
||||
new_sequence.get_bindings(),
|
||||
settings,
|
||||
repre_path
|
||||
)
|
||||
|
||||
# Set range of all sections
|
||||
# Changing the range of the section is not enough. We need to change
|
||||
# the frame of all the keys in the section.
|
||||
project_name = get_current_project_name()
|
||||
folder_path = container.get("folder_path")
|
||||
if folder_path is None:
|
||||
folder_path = container.get("asset")
|
||||
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
|
||||
clip_in = folder_attributes["clipIn"]
|
||||
clip_out = folder_attributes["clipOut"]
|
||||
frame_start = folder_attributes["frameStart"]
|
||||
for possessable in new_sequence.get_possessables():
|
||||
for tracks in possessable.get_tracks():
|
||||
for section in tracks.get_sections():
|
||||
section.set_range(clip_in, clip_out + 1)
|
||||
for channel in section.get_all_channels():
|
||||
for key in channel.get_keys():
|
||||
old_time = key.get_time().get_editor_property(
|
||||
'frame_number')
|
||||
old_time_value = old_time.get_editor_property(
|
||||
'value')
|
||||
new_time = old_time_value + (
|
||||
clip_in - frame_start
|
||||
)
|
||||
key.set_time(unreal.FrameNumber(value=new_time))
|
||||
|
||||
data = {
|
||||
"representation": repre_entity["id"],
|
||||
"parent": repre_entity["versionId"],
|
||||
}
|
||||
imprint(f"{asset_dir}/{container.get('container_name')}", data)
|
||||
|
||||
EditorLevelLibrary.save_current_level()
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
f"{root}/{ms_asset}", recursive=True, include_folder=False)
|
||||
|
||||
for a in asset_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
|
||||
if curr_level_sequence:
|
||||
LevelSequenceLib.open_level_sequence(curr_level_sequence)
|
||||
LevelSequenceLib.set_current_time(curr_time)
|
||||
LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock)
|
||||
|
||||
editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot)
|
||||
|
||||
def remove(self, container):
|
||||
asset_dir = container.get('namespace')
|
||||
path = Path(asset_dir)
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(_filter)
|
||||
|
||||
if not sequences:
|
||||
raise Exception("Could not find sequence.")
|
||||
|
||||
world = ar.get_asset_by_object_path(
|
||||
EditorLevelLibrary.get_editor_world().get_path_name())
|
||||
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=True)
|
||||
maps = ar.get_assets(_filter)
|
||||
|
||||
# There should be only one map in the list
|
||||
if not maps:
|
||||
raise Exception("Could not find map.")
|
||||
|
||||
map = maps[0]
|
||||
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(map.get_asset().get_path_name())
|
||||
|
||||
# Remove the camera from the level.
|
||||
actors = EditorLevelLibrary.get_all_level_actors()
|
||||
|
||||
for a in actors:
|
||||
if a.__class__ == unreal.CineCameraActor:
|
||||
EditorLevelLibrary.destroy_actor(a)
|
||||
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(world.get_asset().get_path_name())
|
||||
|
||||
# There should be only one sequence in the path.
|
||||
sequence_name = sequences[0].asset_name
|
||||
|
||||
# Remove the Level Sequence from the parent.
|
||||
# We need to traverse the hierarchy from the master sequence to find
|
||||
# the level sequence.
|
||||
root = "/Game/Ayon"
|
||||
namespace = container.get('namespace').replace(f"{root}/", "")
|
||||
ms_asset = namespace.split('/')[0]
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[f"{root}/{ms_asset}"],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(_filter)
|
||||
master_sequence = sequences[0].get_asset()
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[f"{root}/{ms_asset}"],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(_filter)
|
||||
master_level = levels[0].get_full_name()
|
||||
|
||||
sequences = [master_sequence]
|
||||
|
||||
parent = None
|
||||
for s in sequences:
|
||||
tracks = s.get_master_tracks()
|
||||
subscene_track = None
|
||||
visibility_track = None
|
||||
for t in tracks:
|
||||
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
|
||||
subscene_track = t
|
||||
if (t.get_class() ==
|
||||
unreal.MovieSceneLevelVisibilityTrack.static_class()):
|
||||
visibility_track = t
|
||||
if subscene_track:
|
||||
sections = subscene_track.get_sections()
|
||||
for ss in sections:
|
||||
if ss.get_sequence().get_name() == sequence_name:
|
||||
parent = s
|
||||
subscene_track.remove_section(ss)
|
||||
break
|
||||
sequences.append(ss.get_sequence())
|
||||
# Update subscenes indexes.
|
||||
for i, ss in enumerate(sections):
|
||||
ss.set_row_index(i)
|
||||
|
||||
if visibility_track:
|
||||
sections = visibility_track.get_sections()
|
||||
for ss in sections:
|
||||
if (unreal.Name(f"{container.get('asset')}_map_camera")
|
||||
in ss.get_level_names()):
|
||||
visibility_track.remove_section(ss)
|
||||
# Update visibility sections indexes.
|
||||
i = -1
|
||||
prev_name = []
|
||||
for ss in sections:
|
||||
if prev_name != ss.get_level_names():
|
||||
i += 1
|
||||
ss.set_row_index(i)
|
||||
prev_name = ss.get_level_names()
|
||||
if parent:
|
||||
break
|
||||
|
||||
assert parent, "Could not find the parent sequence"
|
||||
|
||||
# Create a temporary level to delete the layout level.
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorAssetLibrary.make_directory(f"{root}/tmp")
|
||||
tmp_level = f"{root}/tmp/temp_map"
|
||||
if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"):
|
||||
EditorLevelLibrary.new_level(tmp_level)
|
||||
else:
|
||||
EditorLevelLibrary.load_level(tmp_level)
|
||||
|
||||
# Delete the layout directory.
|
||||
EditorAssetLibrary.delete_directory(asset_dir)
|
||||
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
EditorAssetLibrary.delete_directory(f"{root}/tmp")
|
||||
|
||||
# Check if there isn't any more assets in the parent folder, and
|
||||
# delete it if not.
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
path.parent.as_posix(), recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
EditorAssetLibrary.delete_directory(path.parent.as_posix())
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Loader for published alembics."""
|
||||
import os
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
AYON_ASSET_DIR,
|
||||
create_container,
|
||||
imprint,
|
||||
)
|
||||
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class PointCacheAlembicLoader(plugin.Loader):
|
||||
"""Load Point Cache from Alembic"""
|
||||
|
||||
product_types = {"model", "pointcache"}
|
||||
label = "Import Alembic Point Cache"
|
||||
representations = {"abc"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
root = AYON_ASSET_DIR
|
||||
|
||||
@staticmethod
|
||||
def get_task(
|
||||
filename, asset_dir, asset_name, replace,
|
||||
frame_start=None, frame_end=None
|
||||
):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.AbcImportSettings()
|
||||
gc_settings = unreal.AbcGeometryCacheSettings()
|
||||
conversion_settings = unreal.AbcConversionSettings()
|
||||
sampling_settings = unreal.AbcSamplingSettings()
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
options.set_editor_property(
|
||||
'import_type', unreal.AlembicImportType.GEOMETRY_CACHE)
|
||||
|
||||
gc_settings.set_editor_property('flatten_tracks', False)
|
||||
|
||||
conversion_settings.set_editor_property('flip_u', False)
|
||||
conversion_settings.set_editor_property('flip_v', True)
|
||||
conversion_settings.set_editor_property(
|
||||
'scale', unreal.Vector(x=100.0, y=100.0, z=100.0))
|
||||
conversion_settings.set_editor_property(
|
||||
'rotation', unreal.Vector(x=-90.0, y=0.0, z=180.0))
|
||||
|
||||
if frame_start is not None:
|
||||
sampling_settings.set_editor_property('frame_start', frame_start)
|
||||
if frame_end is not None:
|
||||
sampling_settings.set_editor_property('frame_end', frame_end)
|
||||
|
||||
options.geometry_cache_settings = gc_settings
|
||||
options.conversion_settings = conversion_settings
|
||||
options.sampling_settings = sampling_settings
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
def import_and_containerize(
|
||||
self, filepath, asset_dir, asset_name, container_name,
|
||||
frame_start, frame_end
|
||||
):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
task = self.get_task(
|
||||
filepath, asset_dir, asset_name, False, frame_start, frame_end)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
# Create Asset Container
|
||||
create_container(container=container_name, path=asset_dir)
|
||||
|
||||
def imprint(
|
||||
self,
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
representation,
|
||||
frame_start,
|
||||
frame_end,
|
||||
product_type,
|
||||
):
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": representation["id"],
|
||||
"parent": representation["versionId"],
|
||||
"frame_start": frame_start,
|
||||
"frame_end": frame_end,
|
||||
"product_type": product_type,
|
||||
"folder_path": folder_path,
|
||||
# TODO these should be probably removed
|
||||
"family": product_type,
|
||||
"asset": folder_path,
|
||||
}
|
||||
imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""Load and containerise representation into Content Browser.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): Product 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.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
# Create directory for asset and Ayon container
|
||||
folder_entity = context["folder"]
|
||||
folder_path = folder_entity["path"]
|
||||
folder_name = folder_entity["name"]
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
version = context["version"]["version"]
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version:03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
frame_start = folder_attributes.get("frameStart")
|
||||
frame_end = folder_attributes.get("frameEnd")
|
||||
|
||||
# If frame start and end are the same, we increase the end frame by
|
||||
# one, otherwise Unreal will not import it
|
||||
if frame_start == frame_end:
|
||||
frame_end += 1
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
self.import_and_containerize(
|
||||
path, asset_dir, asset_name, container_name,
|
||||
frame_start, frame_end)
|
||||
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
context["representation"],
|
||||
frame_start,
|
||||
frame_end,
|
||||
context["product"]["productType"]
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
# Create directory for folder and Ayon container
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
product_name = context["product"]["name"]
|
||||
product_type = context["product"]["productType"]
|
||||
version = context["version"]["version"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
suffix = "_CON"
|
||||
asset_name = product_name
|
||||
if folder_name:
|
||||
asset_name = f"{folder_name}_{product_name}"
|
||||
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{product_name}_hero"
|
||||
else:
|
||||
name_version = f"{product_name}_v{version:03d}"
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
frame_start = int(container.get("frame_start"))
|
||||
frame_end = int(container.get("frame_end"))
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = get_representation_path(repre_entity)
|
||||
|
||||
self.import_and_containerize(
|
||||
path, asset_dir, asset_name, container_name,
|
||||
frame_start, frame_end)
|
||||
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
repre_entity,
|
||||
frame_start,
|
||||
frame_end,
|
||||
product_type
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,916 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Loader for layouts."""
|
||||
import json
|
||||
import collections
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
from unreal import (
|
||||
EditorAssetLibrary,
|
||||
EditorLevelLibrary,
|
||||
EditorLevelUtils,
|
||||
AssetToolsHelpers,
|
||||
FBXImportType,
|
||||
MovieSceneLevelVisibilityTrack,
|
||||
MovieSceneSubTrack,
|
||||
LevelSequenceEditorBlueprintLibrary as LevelSequenceLib,
|
||||
)
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
discover_loader_plugins,
|
||||
loaders_from_representation,
|
||||
load_container,
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID,
|
||||
get_current_project_name,
|
||||
)
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
from ayon_core.settings import get_current_project_settings
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
generate_sequence,
|
||||
set_sequence_hierarchy,
|
||||
create_container,
|
||||
imprint,
|
||||
ls,
|
||||
)
|
||||
|
||||
|
||||
class LayoutLoader(plugin.Loader):
|
||||
"""Load Layout from a JSON file"""
|
||||
|
||||
product_types = {"layout"}
|
||||
representations = {"json"}
|
||||
|
||||
label = "Load Layout"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
ASSET_ROOT = "/Game/Ayon"
|
||||
|
||||
def _get_asset_containers(self, path):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
path, recursive=True)
|
||||
|
||||
asset_containers = []
|
||||
|
||||
# Get all the asset containers
|
||||
for a in asset_content:
|
||||
obj = ar.get_asset_by_object_path(a)
|
||||
if obj.get_asset().get_class().get_name() == 'AyonAssetContainer':
|
||||
asset_containers.append(obj)
|
||||
|
||||
return asset_containers
|
||||
|
||||
@staticmethod
|
||||
def _get_fbx_loader(loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "SkeletalMeshFBXLoader"
|
||||
elif family == 'model':
|
||||
name = "StaticMeshFBXLoader"
|
||||
elif family == 'camera':
|
||||
name = "CameraLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_abc_loader(loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "SkeletalMeshAlembicLoader"
|
||||
elif family == 'model':
|
||||
name = "StaticMeshAlembicLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
def _transform_from_basis(self, transform, basis):
|
||||
"""Transform a transform from a basis to a new basis."""
|
||||
# Get the basis matrix
|
||||
basis_matrix = unreal.Matrix(
|
||||
basis[0],
|
||||
basis[1],
|
||||
basis[2],
|
||||
basis[3]
|
||||
)
|
||||
transform_matrix = unreal.Matrix(
|
||||
transform[0],
|
||||
transform[1],
|
||||
transform[2],
|
||||
transform[3]
|
||||
)
|
||||
|
||||
new_transform = (
|
||||
basis_matrix.get_inverse() * transform_matrix * basis_matrix)
|
||||
|
||||
return new_transform.transform()
|
||||
|
||||
def _process_family(
|
||||
self, assets, class_name, transform, basis, sequence, inst_name=None
|
||||
):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
actors = []
|
||||
bindings = []
|
||||
|
||||
for asset in assets:
|
||||
obj = ar.get_asset_by_object_path(asset).get_asset()
|
||||
if obj.get_class().get_name() == class_name:
|
||||
t = self._transform_from_basis(transform, basis)
|
||||
actor = EditorLevelLibrary.spawn_actor_from_object(
|
||||
obj, t.translation
|
||||
)
|
||||
actor.set_actor_rotation(t.rotation.rotator(), False)
|
||||
actor.set_actor_scale3d(t.scale3d)
|
||||
|
||||
if class_name == 'SkeletalMesh':
|
||||
skm_comp = actor.get_editor_property(
|
||||
'skeletal_mesh_component')
|
||||
skm_comp.set_bounds_scale(10.0)
|
||||
|
||||
actors.append(actor)
|
||||
|
||||
if sequence:
|
||||
binding = None
|
||||
for p in sequence.get_possessables():
|
||||
if p.get_name() == actor.get_name():
|
||||
binding = p
|
||||
break
|
||||
|
||||
if not binding:
|
||||
binding = sequence.add_possessable(actor)
|
||||
|
||||
bindings.append(binding)
|
||||
|
||||
return actors, bindings
|
||||
|
||||
def _import_animation(
|
||||
self, asset_dir, path, instance_name, skeleton, actors_dict,
|
||||
animation_file, bindings_dict, sequence
|
||||
):
|
||||
anim_file = Path(animation_file)
|
||||
anim_file_name = anim_file.with_suffix('')
|
||||
|
||||
anim_path = f"{asset_dir}/animations/{anim_file_name}"
|
||||
|
||||
folder_entity = get_current_folder_entity()
|
||||
# Import animation
|
||||
task = unreal.AssetImportTask()
|
||||
task.options = unreal.FbxImportUI()
|
||||
|
||||
task.set_editor_property(
|
||||
'filename', str(path.with_suffix(f".{animation_file}")))
|
||||
task.set_editor_property('destination_path', anim_path)
|
||||
task.set_editor_property(
|
||||
'destination_name', f"{instance_name}_animation")
|
||||
task.set_editor_property('replace_existing', False)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', False)
|
||||
|
||||
# set import options here
|
||||
task.options.set_editor_property(
|
||||
'automated_import_should_detect_type', False)
|
||||
task.options.set_editor_property(
|
||||
'original_import_type', FBXImportType.FBXIT_SKELETAL_MESH)
|
||||
task.options.set_editor_property(
|
||||
'mesh_type_to_import', FBXImportType.FBXIT_ANIMATION)
|
||||
task.options.set_editor_property('import_mesh', False)
|
||||
task.options.set_editor_property('import_animations', True)
|
||||
task.options.set_editor_property('override_full_name', True)
|
||||
task.options.set_editor_property('skeleton', skeleton)
|
||||
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'animation_length',
|
||||
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
|
||||
)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_meshes_in_bone_hierarchy', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'use_default_sample_rate', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'custom_sample_rate', folder_entity.get("attrib", {}).get("fps"))
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_custom_attribute', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_bone_tracks', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'remove_redundant_keys', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'convert_scene', True)
|
||||
|
||||
AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
anim_path, recursive=False, include_folder=False
|
||||
)
|
||||
|
||||
animation = None
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a)
|
||||
imported_asset = unreal.AssetRegistryHelpers.get_asset(
|
||||
imported_asset_data)
|
||||
if imported_asset.__class__ == unreal.AnimSequence:
|
||||
animation = imported_asset
|
||||
break
|
||||
|
||||
if animation:
|
||||
actor = None
|
||||
if actors_dict.get(instance_name):
|
||||
for a in actors_dict.get(instance_name):
|
||||
if a.get_class().get_name() == 'SkeletalMeshActor':
|
||||
actor = a
|
||||
break
|
||||
|
||||
animation.set_editor_property('enable_root_motion', True)
|
||||
actor.skeletal_mesh_component.set_editor_property(
|
||||
'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)
|
||||
actor.skeletal_mesh_component.animation_data.set_editor_property(
|
||||
'anim_to_play', animation)
|
||||
|
||||
if sequence:
|
||||
# Add animation to the sequencer
|
||||
bindings = bindings_dict.get(instance_name)
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
for binding in bindings:
|
||||
tracks = binding.get_tracks()
|
||||
track = None
|
||||
track = tracks[0] if tracks else binding.add_track(
|
||||
unreal.MovieSceneSkeletalAnimationTrack)
|
||||
|
||||
sections = track.get_sections()
|
||||
section = None
|
||||
if not sections:
|
||||
section = track.add_section()
|
||||
else:
|
||||
section = sections[0]
|
||||
|
||||
sec_params = section.get_editor_property('params')
|
||||
curr_anim = sec_params.get_editor_property('animation')
|
||||
|
||||
if curr_anim:
|
||||
# Checks if the animation path has a container.
|
||||
# If it does, it means that the animation is
|
||||
# already in the sequencer.
|
||||
anim_path = str(Path(
|
||||
curr_anim.get_path_name()).parent
|
||||
).replace('\\', '/')
|
||||
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["AyonAssetContainer"],
|
||||
package_paths=[anim_path],
|
||||
recursive_paths=False)
|
||||
containers = ar.get_assets(_filter)
|
||||
|
||||
if len(containers) > 0:
|
||||
return
|
||||
|
||||
section.set_range(
|
||||
sequence.get_playback_start(),
|
||||
sequence.get_playback_end())
|
||||
sec_params = section.get_editor_property('params')
|
||||
sec_params.set_editor_property('animation', animation)
|
||||
|
||||
def _get_repre_entities_by_version_id(self, data):
|
||||
version_ids = {
|
||||
element.get("version")
|
||||
for element in data
|
||||
if element.get("representation")
|
||||
}
|
||||
version_ids.discard(None)
|
||||
|
||||
output = collections.defaultdict(list)
|
||||
if not version_ids:
|
||||
return output
|
||||
|
||||
project_name = get_current_project_name()
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name,
|
||||
representation_names={"fbx", "abc"},
|
||||
version_ids=version_ids,
|
||||
fields={"id", "versionId", "name"}
|
||||
)
|
||||
for repre_entity in repre_entities:
|
||||
version_id = repre_entity["versionId"]
|
||||
output[version_id].append(repre_entity)
|
||||
return output
|
||||
|
||||
def _process(self, lib_path, asset_dir, sequence, repr_loaded=None):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
with open(lib_path, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
all_loaders = discover_loader_plugins()
|
||||
|
||||
if not repr_loaded:
|
||||
repr_loaded = []
|
||||
|
||||
path = Path(lib_path)
|
||||
|
||||
skeleton_dict = {}
|
||||
actors_dict = {}
|
||||
bindings_dict = {}
|
||||
|
||||
loaded_assets = []
|
||||
|
||||
repre_entities_by_version_id = self._get_repre_entities_by_version_id(
|
||||
data
|
||||
)
|
||||
for element in data:
|
||||
repre_id = None
|
||||
repr_format = None
|
||||
if element.get('representation'):
|
||||
version_id = element.get("version")
|
||||
repre_entities = repre_entities_by_version_id[version_id]
|
||||
if not repre_entities:
|
||||
self.log.error(
|
||||
f"No valid representation found for version"
|
||||
f" {version_id}")
|
||||
continue
|
||||
repre_entity = repre_entities[0]
|
||||
repre_id = repre_entity["id"]
|
||||
repr_format = repre_entity["name"]
|
||||
|
||||
# This is to keep compatibility with old versions of the
|
||||
# json format.
|
||||
elif element.get('reference_fbx'):
|
||||
repre_id = element.get('reference_fbx')
|
||||
repr_format = 'fbx'
|
||||
elif element.get('reference_abc'):
|
||||
repre_id = element.get('reference_abc')
|
||||
repr_format = 'abc'
|
||||
|
||||
# If reference is None, this element is skipped, as it cannot be
|
||||
# imported in Unreal
|
||||
if not repre_id:
|
||||
continue
|
||||
|
||||
instance_name = element.get('instance_name')
|
||||
|
||||
skeleton = None
|
||||
|
||||
if repre_id not in repr_loaded:
|
||||
repr_loaded.append(repre_id)
|
||||
|
||||
product_type = element.get("product_type")
|
||||
if product_type is None:
|
||||
product_type = element.get("family")
|
||||
loaders = loaders_from_representation(
|
||||
all_loaders, repre_id)
|
||||
|
||||
loader = None
|
||||
|
||||
if repr_format == 'fbx':
|
||||
loader = self._get_fbx_loader(loaders, product_type)
|
||||
elif repr_format == 'abc':
|
||||
loader = self._get_abc_loader(loaders, product_type)
|
||||
|
||||
if not loader:
|
||||
self.log.error(
|
||||
f"No valid loader found for {repre_id}")
|
||||
continue
|
||||
|
||||
options = {
|
||||
# "asset_dir": asset_dir
|
||||
}
|
||||
|
||||
assets = load_container(
|
||||
loader,
|
||||
repre_id,
|
||||
namespace=instance_name,
|
||||
options=options
|
||||
)
|
||||
|
||||
container = None
|
||||
|
||||
for asset in assets:
|
||||
obj = ar.get_asset_by_object_path(asset).get_asset()
|
||||
if obj.get_class().get_name() == 'AyonAssetContainer':
|
||||
container = obj
|
||||
if obj.get_class().get_name() == 'Skeleton':
|
||||
skeleton = obj
|
||||
|
||||
loaded_assets.append(container.get_path_name())
|
||||
|
||||
instances = [
|
||||
item for item in data
|
||||
if ((item.get('version') and
|
||||
item.get('version') == element.get('version')) or
|
||||
item.get('reference_fbx') == repre_id or
|
||||
item.get('reference_abc') == repre_id)]
|
||||
|
||||
for instance in instances:
|
||||
# transform = instance.get('transform')
|
||||
transform = instance.get('transform_matrix')
|
||||
basis = instance.get('basis')
|
||||
inst = instance.get('instance_name')
|
||||
|
||||
actors = []
|
||||
|
||||
if product_type == 'model':
|
||||
actors, _ = self._process_family(
|
||||
assets, 'StaticMesh', transform, basis,
|
||||
sequence, inst
|
||||
)
|
||||
elif product_type == 'rig':
|
||||
actors, bindings = self._process_family(
|
||||
assets, 'SkeletalMesh', transform, basis,
|
||||
sequence, inst
|
||||
)
|
||||
actors_dict[inst] = actors
|
||||
bindings_dict[inst] = bindings
|
||||
|
||||
if skeleton:
|
||||
skeleton_dict[repre_id] = skeleton
|
||||
else:
|
||||
skeleton = skeleton_dict.get(repre_id)
|
||||
|
||||
animation_file = element.get('animation')
|
||||
|
||||
if animation_file and skeleton:
|
||||
self._import_animation(
|
||||
asset_dir, path, instance_name, skeleton, actors_dict,
|
||||
animation_file, bindings_dict, sequence)
|
||||
|
||||
return loaded_assets
|
||||
|
||||
@staticmethod
|
||||
def _remove_family(assets, components, class_name, prop_name):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
objects = []
|
||||
for a in assets:
|
||||
obj = ar.get_asset_by_object_path(a)
|
||||
if obj.get_asset().get_class().get_name() == class_name:
|
||||
objects.append(obj)
|
||||
for obj in objects:
|
||||
for comp in components:
|
||||
if comp.get_editor_property(prop_name) == obj.get_asset():
|
||||
comp.get_owner().destroy_actor()
|
||||
|
||||
def _remove_actors(self, path):
|
||||
asset_containers = self._get_asset_containers(path)
|
||||
|
||||
# Get all the static and skeletal meshes components in the level
|
||||
components = EditorLevelLibrary.get_all_level_actors_components()
|
||||
static_meshes_comp = [
|
||||
c for c in components
|
||||
if c.get_class().get_name() == 'StaticMeshComponent']
|
||||
skel_meshes_comp = [
|
||||
c for c in components
|
||||
if c.get_class().get_name() == 'SkeletalMeshComponent']
|
||||
|
||||
# For all the asset containers, get the static and skeletal meshes.
|
||||
# Then, check the components in the level and destroy the matching
|
||||
# actors.
|
||||
for asset_container in asset_containers:
|
||||
package_path = asset_container.get_editor_property('package_path')
|
||||
family = EditorAssetLibrary.get_metadata_tag(
|
||||
asset_container.get_asset(), "family")
|
||||
assets = EditorAssetLibrary.list_assets(
|
||||
str(package_path), recursive=False)
|
||||
if family == 'model':
|
||||
self._remove_family(
|
||||
assets, static_meshes_comp, 'StaticMesh', 'static_mesh')
|
||||
elif family == 'rig':
|
||||
self._remove_family(
|
||||
assets, skel_meshes_comp, 'SkeletalMesh', 'skeletal_mesh')
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""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): Product 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.
|
||||
options (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
|
||||
"""
|
||||
data = get_current_project_settings()
|
||||
create_sequences = data["unreal"]["level_sequences_for_layouts"]
|
||||
|
||||
# Create directory for asset and Ayon container
|
||||
folder_entity = context["folder"]
|
||||
folder_path = folder_entity["path"]
|
||||
hierarchy = folder_path.lstrip("/").split("/")
|
||||
# Remove folder name
|
||||
folder_name = hierarchy.pop(-1)
|
||||
root = self.ASSET_ROOT
|
||||
hierarchy_dir = root
|
||||
hierarchy_dir_list = []
|
||||
for h in hierarchy:
|
||||
hierarchy_dir = f"{hierarchy_dir}/{h}"
|
||||
hierarchy_dir_list.append(hierarchy_dir)
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else name
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
"{}/{}/{}".format(hierarchy_dir, folder_name, name),
|
||||
suffix=""
|
||||
)
|
||||
|
||||
container_name += suffix
|
||||
|
||||
EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
master_level = None
|
||||
shot = None
|
||||
sequences = []
|
||||
|
||||
level = f"{asset_dir}/{folder_name}_map.{folder_name}_map"
|
||||
EditorLevelLibrary.new_level(f"{asset_dir}/{folder_name}_map")
|
||||
|
||||
if create_sequences:
|
||||
# Create map for the shot, and create hierarchy of map. If the
|
||||
# maps already exist, we will use them.
|
||||
if hierarchy:
|
||||
h_dir = hierarchy_dir_list[0]
|
||||
h_asset = hierarchy[0]
|
||||
master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map"
|
||||
if not EditorAssetLibrary.does_asset_exist(master_level):
|
||||
EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map")
|
||||
|
||||
if master_level:
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
EditorLevelUtils.add_level_to_world(
|
||||
EditorLevelLibrary.get_editor_world(),
|
||||
level,
|
||||
unreal.LevelStreamingDynamic
|
||||
)
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(level)
|
||||
|
||||
# Get all the sequences in the hierarchy. It will create them, if
|
||||
# they don't exist.
|
||||
frame_ranges = []
|
||||
for (h_dir, h) in zip(hierarchy_dir_list, hierarchy):
|
||||
root_content = EditorAssetLibrary.list_assets(
|
||||
h_dir, recursive=False, include_folder=False)
|
||||
|
||||
existing_sequences = [
|
||||
EditorAssetLibrary.find_asset_data(asset)
|
||||
for asset in root_content
|
||||
if EditorAssetLibrary.find_asset_data(
|
||||
asset).get_class().get_name() == 'LevelSequence'
|
||||
]
|
||||
|
||||
if not existing_sequences:
|
||||
sequence, frame_range = generate_sequence(h, h_dir)
|
||||
|
||||
sequences.append(sequence)
|
||||
frame_ranges.append(frame_range)
|
||||
else:
|
||||
for e in existing_sequences:
|
||||
sequences.append(e.get_asset())
|
||||
frame_ranges.append((
|
||||
e.get_asset().get_playback_start(),
|
||||
e.get_asset().get_playback_end()))
|
||||
|
||||
shot = tools.create_asset(
|
||||
asset_name=folder_name,
|
||||
package_path=asset_dir,
|
||||
asset_class=unreal.LevelSequence,
|
||||
factory=unreal.LevelSequenceFactoryNew()
|
||||
)
|
||||
|
||||
# sequences and frame_ranges have the same length
|
||||
for i in range(0, len(sequences) - 1):
|
||||
set_sequence_hierarchy(
|
||||
sequences[i], sequences[i + 1],
|
||||
frame_ranges[i][1],
|
||||
frame_ranges[i + 1][0], frame_ranges[i + 1][1],
|
||||
[level])
|
||||
|
||||
project_name = get_current_project_name()
|
||||
folder_attributes = (
|
||||
ayon_api.get_folder_by_path(project_name, folder_path)["attrib"]
|
||||
)
|
||||
shot.set_display_rate(
|
||||
unreal.FrameRate(folder_attributes.get("fps"), 1.0))
|
||||
shot.set_playback_start(0)
|
||||
shot.set_playback_end(
|
||||
folder_attributes.get('clipOut')
|
||||
- folder_attributes.get('clipIn')
|
||||
+ 1
|
||||
)
|
||||
if sequences:
|
||||
set_sequence_hierarchy(
|
||||
sequences[-1],
|
||||
shot,
|
||||
frame_ranges[-1][1],
|
||||
folder_attributes.get('clipIn'),
|
||||
folder_attributes.get('clipOut'),
|
||||
[level])
|
||||
|
||||
EditorLevelLibrary.load_level(level)
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
loaded_assets = self._process(path, asset_dir, shot)
|
||||
|
||||
for s in sequences:
|
||||
EditorAssetLibrary.save_asset(s.get_path_name())
|
||||
|
||||
EditorLevelLibrary.save_current_level()
|
||||
|
||||
# Create Asset Container
|
||||
create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"asset": folder_name,
|
||||
"folder_path": folder_path,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["id"],
|
||||
"parent": context["representation"]["versionId"],
|
||||
"family": context["product"]["productType"],
|
||||
"loaded_assets": loaded_assets
|
||||
}
|
||||
imprint(
|
||||
"{}/{}".format(asset_dir, container_name), data)
|
||||
|
||||
save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
save_dir, recursive=True, include_folder=False)
|
||||
|
||||
for a in asset_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
if master_level:
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
data = get_current_project_settings()
|
||||
create_sequences = data["unreal"]["level_sequences_for_layouts"]
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
curr_level_sequence = LevelSequenceLib.get_current_level_sequence()
|
||||
curr_time = LevelSequenceLib.get_current_time()
|
||||
is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport()
|
||||
|
||||
editor_subsystem = unreal.UnrealEditorSubsystem()
|
||||
vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info()
|
||||
|
||||
root = "/Game/Ayon"
|
||||
|
||||
asset_dir = container.get('namespace')
|
||||
|
||||
folder_entity = context["folder"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
hierarchy = folder_entity["path"].lstrip("/").split("/")
|
||||
first_parent_name = hierarchy[0]
|
||||
|
||||
sequence = None
|
||||
master_level = None
|
||||
|
||||
if create_sequences:
|
||||
h_dir = f"{root}/{first_parent_name}"
|
||||
h_asset = first_parent_name
|
||||
master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map"
|
||||
|
||||
filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(filter)
|
||||
sequence = sequences[0].get_asset()
|
||||
|
||||
prev_level = None
|
||||
|
||||
if not master_level:
|
||||
curr_level = unreal.LevelEditorSubsystem().get_current_level()
|
||||
curr_level_path = curr_level.get_outer().get_path_name()
|
||||
# If the level path does not start with "/Game/", the current
|
||||
# level is a temporary, unsaved level.
|
||||
if curr_level_path.startswith("/Game/"):
|
||||
prev_level = curr_level_path
|
||||
|
||||
# Get layout level
|
||||
filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(filter)
|
||||
|
||||
layout_level = levels[0].get_asset().get_path_name()
|
||||
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(layout_level)
|
||||
|
||||
# Delete all the actors in the level
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
for actor in actors:
|
||||
unreal.EditorLevelLibrary.destroy_actor(actor)
|
||||
|
||||
if create_sequences:
|
||||
EditorLevelLibrary.save_current_level()
|
||||
|
||||
EditorAssetLibrary.delete_directory(f"{asset_dir}/animations/")
|
||||
|
||||
source_path = get_representation_path(repre_entity)
|
||||
|
||||
loaded_assets = self._process(source_path, asset_dir, sequence)
|
||||
|
||||
data = {
|
||||
"representation": repre_entity["id"],
|
||||
"parent": repre_entity["versionId"],
|
||||
"loaded_assets": loaded_assets,
|
||||
}
|
||||
imprint(
|
||||
"{}/{}".format(asset_dir, container.get('container_name')), data)
|
||||
|
||||
EditorLevelLibrary.save_current_level()
|
||||
|
||||
save_dir = f"{root}/{first_parent_name}" if create_sequences else asset_dir
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
save_dir, recursive=True, include_folder=False)
|
||||
|
||||
for a in asset_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
if master_level:
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
elif prev_level:
|
||||
EditorLevelLibrary.load_level(prev_level)
|
||||
|
||||
if curr_level_sequence:
|
||||
LevelSequenceLib.open_level_sequence(curr_level_sequence)
|
||||
LevelSequenceLib.set_current_time(curr_time)
|
||||
LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock)
|
||||
|
||||
editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot)
|
||||
|
||||
def remove(self, container):
|
||||
"""
|
||||
Delete the layout. First, check if the assets loaded with the layout
|
||||
are used by other layouts. If not, delete the assets.
|
||||
"""
|
||||
data = get_current_project_settings()
|
||||
create_sequences = data["unreal"]["level_sequences_for_layouts"]
|
||||
|
||||
root = "/Game/Ayon"
|
||||
path = Path(container.get("namespace"))
|
||||
|
||||
containers = ls()
|
||||
layout_containers = [
|
||||
c for c in containers
|
||||
if (c.get('asset_name') != container.get('asset_name') and
|
||||
c.get('family') == "layout")]
|
||||
|
||||
# Check if the assets have been loaded by other layouts, and deletes
|
||||
# them if they haven't.
|
||||
for asset in eval(container.get('loaded_assets')):
|
||||
layouts = [
|
||||
lc for lc in layout_containers
|
||||
if asset in lc.get('loaded_assets')]
|
||||
|
||||
if not layouts:
|
||||
EditorAssetLibrary.delete_directory(str(Path(asset).parent))
|
||||
|
||||
# Delete the parent folder if there aren't any more
|
||||
# layouts in it.
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
str(Path(asset).parent.parent), recursive=False,
|
||||
include_folder=True
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
EditorAssetLibrary.delete_directory(
|
||||
str(Path(asset).parent.parent))
|
||||
|
||||
master_sequence = None
|
||||
master_level = None
|
||||
sequences = []
|
||||
|
||||
if create_sequences:
|
||||
# Remove the Level Sequence from the parent.
|
||||
# We need to traverse the hierarchy from the master sequence to
|
||||
# find the level sequence.
|
||||
namespace = container.get('namespace').replace(f"{root}/", "")
|
||||
ms_asset = namespace.split('/')[0]
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[f"{root}/{ms_asset}"],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(_filter)
|
||||
master_sequence = sequences[0].get_asset()
|
||||
_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[f"{root}/{ms_asset}"],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(_filter)
|
||||
master_level = levels[0].get_asset().get_path_name()
|
||||
|
||||
sequences = [master_sequence]
|
||||
|
||||
parent = None
|
||||
for s in sequences:
|
||||
tracks = s.get_master_tracks()
|
||||
subscene_track = None
|
||||
visibility_track = None
|
||||
for t in tracks:
|
||||
if t.get_class() == MovieSceneSubTrack.static_class():
|
||||
subscene_track = t
|
||||
if (t.get_class() ==
|
||||
MovieSceneLevelVisibilityTrack.static_class()):
|
||||
visibility_track = t
|
||||
if subscene_track:
|
||||
sections = subscene_track.get_sections()
|
||||
for ss in sections:
|
||||
if (ss.get_sequence().get_name() ==
|
||||
container.get('asset')):
|
||||
parent = s
|
||||
subscene_track.remove_section(ss)
|
||||
break
|
||||
sequences.append(ss.get_sequence())
|
||||
# Update subscenes indexes.
|
||||
i = 0
|
||||
for ss in sections:
|
||||
ss.set_row_index(i)
|
||||
i += 1
|
||||
|
||||
if visibility_track:
|
||||
sections = visibility_track.get_sections()
|
||||
for ss in sections:
|
||||
if (unreal.Name(f"{container.get('asset')}_map")
|
||||
in ss.get_level_names()):
|
||||
visibility_track.remove_section(ss)
|
||||
# Update visibility sections indexes.
|
||||
i = -1
|
||||
prev_name = []
|
||||
for ss in sections:
|
||||
if prev_name != ss.get_level_names():
|
||||
i += 1
|
||||
ss.set_row_index(i)
|
||||
prev_name = ss.get_level_names()
|
||||
if parent:
|
||||
break
|
||||
|
||||
assert parent, "Could not find the parent sequence"
|
||||
|
||||
# Create a temporary level to delete the layout level.
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorAssetLibrary.make_directory(f"{root}/tmp")
|
||||
tmp_level = f"{root}/tmp/temp_map"
|
||||
if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"):
|
||||
EditorLevelLibrary.new_level(tmp_level)
|
||||
else:
|
||||
EditorLevelLibrary.load_level(tmp_level)
|
||||
|
||||
# Delete the layout directory.
|
||||
EditorAssetLibrary.delete_directory(str(path))
|
||||
|
||||
if create_sequences:
|
||||
EditorLevelLibrary.load_level(master_level)
|
||||
EditorAssetLibrary.delete_directory(f"{root}/tmp")
|
||||
|
||||
# Delete the parent folder if there aren't any more layouts in it.
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
str(path.parent), recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
EditorAssetLibrary.delete_directory(str(path.parent))
|
||||
|
|
@ -1,451 +0,0 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
from unreal import EditorLevelLibrary
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
discover_loader_plugins,
|
||||
loaders_from_representation,
|
||||
load_container,
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID,
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api import pipeline as upipeline
|
||||
|
||||
|
||||
class ExistingLayoutLoader(plugin.Loader):
|
||||
"""
|
||||
Load Layout for an existing scene, and match the existing assets.
|
||||
"""
|
||||
|
||||
product_types = {"layout"}
|
||||
representations = {"json"}
|
||||
|
||||
label = "Load Layout on Existing Scene"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
ASSET_ROOT = "/Game/Ayon"
|
||||
|
||||
delete_unmatched_assets = True
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings):
|
||||
super(ExistingLayoutLoader, cls).apply_settings(
|
||||
project_settings
|
||||
)
|
||||
cls.delete_unmatched_assets = (
|
||||
project_settings["unreal"]["delete_unmatched_assets"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_container(
|
||||
asset_name,
|
||||
asset_dir,
|
||||
folder_path,
|
||||
representation,
|
||||
version_id,
|
||||
product_type
|
||||
):
|
||||
container_name = f"{asset_name}_CON"
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_asset_exist(
|
||||
f"{asset_dir}/{container_name}"
|
||||
):
|
||||
container = upipeline.create_container(container_name, asset_dir)
|
||||
else:
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
obj = ar.get_asset_by_object_path(
|
||||
f"{asset_dir}/{container_name}.{container_name}")
|
||||
container = obj.get_asset()
|
||||
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"folder_path": folder_path,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
# "loader": str(self.__class__.__name__),
|
||||
"representation": representation,
|
||||
"parent": version_id,
|
||||
"product_type": product_type,
|
||||
# TODO these shold be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
|
||||
upipeline.imprint(
|
||||
"{}/{}".format(asset_dir, container_name), data)
|
||||
|
||||
return container.get_path_name()
|
||||
|
||||
@staticmethod
|
||||
def _get_current_level():
|
||||
ue_version = unreal.SystemLibrary.get_engine_version().split('.')
|
||||
ue_major = ue_version[0]
|
||||
|
||||
if ue_major == '4':
|
||||
return EditorLevelLibrary.get_editor_world()
|
||||
elif ue_major == '5':
|
||||
return unreal.LevelEditorSubsystem().get_current_level()
|
||||
|
||||
raise NotImplementedError(
|
||||
f"Unreal version {ue_major} not supported")
|
||||
|
||||
def _transform_from_basis(self, transform, basis):
|
||||
"""Transform a transform from a basis to a new basis."""
|
||||
# Get the basis matrix
|
||||
basis_matrix = unreal.Matrix(
|
||||
basis[0],
|
||||
basis[1],
|
||||
basis[2],
|
||||
basis[3]
|
||||
)
|
||||
transform_matrix = unreal.Matrix(
|
||||
transform[0],
|
||||
transform[1],
|
||||
transform[2],
|
||||
transform[3]
|
||||
)
|
||||
|
||||
new_transform = (
|
||||
basis_matrix.get_inverse() * transform_matrix * basis_matrix)
|
||||
|
||||
return new_transform.transform()
|
||||
|
||||
def _spawn_actor(self, obj, lasset):
|
||||
actor = EditorLevelLibrary.spawn_actor_from_object(
|
||||
obj, unreal.Vector(0.0, 0.0, 0.0)
|
||||
)
|
||||
|
||||
actor.set_actor_label(lasset.get('instance_name'))
|
||||
|
||||
transform = lasset.get('transform_matrix')
|
||||
basis = lasset.get('basis')
|
||||
|
||||
computed_transform = self._transform_from_basis(transform, basis)
|
||||
|
||||
actor.set_actor_transform(computed_transform, False, True)
|
||||
|
||||
@staticmethod
|
||||
def _get_fbx_loader(loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "SkeletalMeshFBXLoader"
|
||||
elif family == 'model' or family == 'staticMesh':
|
||||
name = "StaticMeshFBXLoader"
|
||||
elif family == 'camera':
|
||||
name = "CameraLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_abc_loader(loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "SkeletalMeshAlembicLoader"
|
||||
elif family == 'model':
|
||||
name = "StaticMeshAlembicLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
def _load_asset(self, repr_data, representation, instance_name, family):
|
||||
repr_format = repr_data.get('name')
|
||||
|
||||
all_loaders = discover_loader_plugins()
|
||||
loaders = loaders_from_representation(
|
||||
all_loaders, representation)
|
||||
|
||||
loader = None
|
||||
|
||||
if repr_format == 'fbx':
|
||||
loader = self._get_fbx_loader(loaders, family)
|
||||
elif repr_format == 'abc':
|
||||
loader = self._get_abc_loader(loaders, family)
|
||||
|
||||
if not loader:
|
||||
self.log.error(f"No valid loader found for {representation}")
|
||||
return []
|
||||
|
||||
# This option is necessary to avoid importing the assets with a
|
||||
# different conversion compared to the other assets. For ABC files,
|
||||
# it is in fact impossible to access the conversion settings. So,
|
||||
# we must assume that the Maya conversion settings have been applied.
|
||||
options = {
|
||||
"default_conversion": True
|
||||
}
|
||||
|
||||
assets = load_container(
|
||||
loader,
|
||||
representation,
|
||||
namespace=instance_name,
|
||||
options=options
|
||||
)
|
||||
|
||||
return assets
|
||||
|
||||
def _get_valid_repre_entities(self, project_name, version_ids):
|
||||
valid_formats = ['fbx', 'abc']
|
||||
|
||||
repre_entities = list(ayon_api.get_representations(
|
||||
project_name,
|
||||
representation_names=valid_formats,
|
||||
version_ids=version_ids
|
||||
))
|
||||
repre_entities_by_version_id = {}
|
||||
for repre_entity in repre_entities:
|
||||
version_id = repre_entity["versionId"]
|
||||
repre_entities_by_version_id[version_id] = repre_entity
|
||||
return repre_entities_by_version_id
|
||||
|
||||
def _process(self, lib_path, project_name):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
actors = EditorLevelLibrary.get_all_level_actors()
|
||||
|
||||
with open(lib_path, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
elements = []
|
||||
repre_ids = set()
|
||||
# Get all the representations in the JSON from the database.
|
||||
for element in data:
|
||||
repre_id = element.get('representation')
|
||||
if repre_id:
|
||||
repre_ids.add(repre_id)
|
||||
elements.append(element)
|
||||
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name, representation_ids=repre_ids
|
||||
)
|
||||
repre_entities_by_id = {
|
||||
repre_entity["id"]: repre_entity
|
||||
for repre_entity in repre_entities
|
||||
}
|
||||
layout_data = []
|
||||
version_ids = set()
|
||||
for element in elements:
|
||||
repre_id = element.get("representation")
|
||||
repre_entity = repre_entities_by_id.get(repre_id)
|
||||
if not repre_entity:
|
||||
raise AssertionError("Representation not found")
|
||||
if not (
|
||||
repre_entity.get("attrib")
|
||||
or repre_entity["attrib"].get("path")
|
||||
):
|
||||
raise AssertionError("Representation does not have path")
|
||||
if not repre_entity.get('context'):
|
||||
raise AssertionError("Representation does not have context")
|
||||
|
||||
layout_data.append((repre_entity, element))
|
||||
version_ids.add(repre_entity["versionId"])
|
||||
|
||||
repre_parents_by_id = ayon_api.get_representation_parents(
|
||||
project_name, repre_entities_by_id.keys()
|
||||
)
|
||||
|
||||
# Prequery valid repre documents for all elements at once
|
||||
valid_repre_entities_by_version_id = self._get_valid_repre_entities(
|
||||
project_name, version_ids)
|
||||
containers = []
|
||||
actors_matched = []
|
||||
|
||||
for (repre_entity, lasset) in layout_data:
|
||||
# For every actor in the scene, check if it has a representation in
|
||||
# those we got from the JSON. If so, create a container for it.
|
||||
# Otherwise, remove it from the scene.
|
||||
found = False
|
||||
repre_id = repre_entity["id"]
|
||||
repre_parents = repre_parents_by_id[repre_id]
|
||||
folder_path = repre_parents.folder["path"]
|
||||
folder_name = repre_parents.folder["name"]
|
||||
product_name = repre_parents.product["name"]
|
||||
product_type = repre_parents.product["productType"]
|
||||
|
||||
for actor in actors:
|
||||
if not actor.get_class().get_name() == 'StaticMeshActor':
|
||||
continue
|
||||
if actor in actors_matched:
|
||||
continue
|
||||
|
||||
# Get the original path of the file from which the asset has
|
||||
# been imported.
|
||||
smc = actor.get_editor_property('static_mesh_component')
|
||||
mesh = smc.get_editor_property('static_mesh')
|
||||
import_data = mesh.get_editor_property('asset_import_data')
|
||||
filename = import_data.get_first_filename()
|
||||
path = Path(filename)
|
||||
|
||||
if (not path.name or
|
||||
path.name not in repre_entity["attrib"]["path"]):
|
||||
continue
|
||||
|
||||
actor.set_actor_label(lasset.get('instance_name'))
|
||||
|
||||
mesh_path = Path(mesh.get_path_name()).parent.as_posix()
|
||||
|
||||
# Create the container for the asset.
|
||||
container = self._create_container(
|
||||
f"{folder_name}_{product_name}",
|
||||
mesh_path,
|
||||
folder_path,
|
||||
repre_entity["id"],
|
||||
repre_entity["versionId"],
|
||||
product_type
|
||||
)
|
||||
containers.append(container)
|
||||
|
||||
# Set the transform for the actor.
|
||||
transform = lasset.get('transform_matrix')
|
||||
basis = lasset.get('basis')
|
||||
|
||||
computed_transform = self._transform_from_basis(
|
||||
transform, basis)
|
||||
actor.set_actor_transform(computed_transform, False, True)
|
||||
|
||||
actors_matched.append(actor)
|
||||
found = True
|
||||
break
|
||||
|
||||
# If an actor has not been found for this representation,
|
||||
# we check if it has been loaded already by checking all the
|
||||
# loaded containers. If so, we add it to the scene. Otherwise,
|
||||
# we load it.
|
||||
if found:
|
||||
continue
|
||||
|
||||
all_containers = upipeline.ls()
|
||||
|
||||
loaded = False
|
||||
|
||||
for container in all_containers:
|
||||
repre_id = container.get('representation')
|
||||
|
||||
if not repre_id == repre_entity["id"]:
|
||||
continue
|
||||
|
||||
asset_dir = container.get('namespace')
|
||||
|
||||
arfilter = unreal.ARFilter(
|
||||
class_names=["StaticMesh"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=False)
|
||||
assets = ar.get_assets(arfilter)
|
||||
|
||||
for asset in assets:
|
||||
obj = asset.get_asset()
|
||||
self._spawn_actor(obj, lasset)
|
||||
|
||||
loaded = True
|
||||
break
|
||||
|
||||
# If the asset has not been loaded yet, we load it.
|
||||
if loaded:
|
||||
continue
|
||||
|
||||
version_id = lasset.get('version')
|
||||
assets = self._load_asset(
|
||||
valid_repre_entities_by_version_id.get(version_id),
|
||||
lasset.get('representation'),
|
||||
lasset.get('instance_name'),
|
||||
lasset.get('family')
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
obj = ar.get_asset_by_object_path(asset).get_asset()
|
||||
if not obj.get_class().get_name() == 'StaticMesh':
|
||||
continue
|
||||
self._spawn_actor(obj, lasset)
|
||||
|
||||
break
|
||||
|
||||
# Check if an actor was not matched to a representation.
|
||||
# If so, remove it from the scene.
|
||||
for actor in actors:
|
||||
if not actor.get_class().get_name() == 'StaticMeshActor':
|
||||
continue
|
||||
if actor not in actors_matched:
|
||||
self.log.warning(f"Actor {actor.get_name()} not matched.")
|
||||
if self.delete_unmatched_assets:
|
||||
EditorLevelLibrary.destroy_actor(actor)
|
||||
|
||||
return containers
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
print("Loading Layout and Match Assets")
|
||||
|
||||
folder_name = context["folder"]["name"]
|
||||
folder_path = context["folder"]["path"]
|
||||
product_type = context["product"]["productType"]
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else name
|
||||
container_name = f"{folder_name}_{name}_CON"
|
||||
|
||||
curr_level = self._get_current_level()
|
||||
|
||||
if not curr_level:
|
||||
raise AssertionError("Current level not saved")
|
||||
|
||||
project_name = context["project"]["name"]
|
||||
path = self.filepath_from_context(context)
|
||||
containers = self._process(path, project_name)
|
||||
|
||||
curr_level_path = Path(
|
||||
curr_level.get_outer().get_path_name()).parent.as_posix()
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_asset_exist(
|
||||
f"{curr_level_path}/{container_name}"
|
||||
):
|
||||
upipeline.create_container(
|
||||
container=container_name, path=curr_level_path)
|
||||
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"folder_path": folder_path,
|
||||
"namespace": curr_level_path,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["id"],
|
||||
"parent": context["representation"]["versionId"],
|
||||
"product_type": product_type,
|
||||
"loaded_assets": containers,
|
||||
# TODO these shold be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
upipeline.imprint(f"{curr_level_path}/{container_name}", data)
|
||||
|
||||
def update(self, container, context):
|
||||
asset_dir = container.get('namespace')
|
||||
|
||||
project_name = context["project"]["name"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
source_path = get_representation_path(repre_entity)
|
||||
containers = self._process(source_path, project_name)
|
||||
|
||||
data = {
|
||||
"representation": repre_entity["id"],
|
||||
"loaded_assets": containers,
|
||||
"parent": repre_entity["versionId"],
|
||||
}
|
||||
upipeline.imprint(
|
||||
"{}/{}".format(asset_dir, container.get('container_name')), data)
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load Skeletal Mesh alembics."""
|
||||
import os
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
AYON_ASSET_DIR,
|
||||
create_container,
|
||||
imprint,
|
||||
)
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class SkeletalMeshAlembicLoader(plugin.Loader):
|
||||
"""Load Unreal SkeletalMesh from Alembic"""
|
||||
|
||||
product_types = {"pointcache", "skeletalMesh"}
|
||||
label = "Import Alembic Skeletal Mesh"
|
||||
representations = {"abc"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
root = AYON_ASSET_DIR
|
||||
|
||||
@staticmethod
|
||||
def get_task(filename, asset_dir, asset_name, replace, default_conversion):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.AbcImportSettings()
|
||||
conversion_settings = unreal.AbcConversionSettings(
|
||||
preset=unreal.AbcConversionPreset.CUSTOM,
|
||||
flip_u=False, flip_v=False,
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, 1.0, 1.0])
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
options.set_editor_property(
|
||||
'import_type', unreal.AlembicImportType.SKELETAL)
|
||||
|
||||
if not default_conversion:
|
||||
conversion_settings = unreal.AbcConversionSettings(
|
||||
preset=unreal.AbcConversionPreset.CUSTOM,
|
||||
flip_u=False, flip_v=False,
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, 1.0, 1.0])
|
||||
options.conversion_settings = conversion_settings
|
||||
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
def import_and_containerize(
|
||||
self, filepath, asset_dir, asset_name, container_name,
|
||||
default_conversion=False
|
||||
):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
task = self.get_task(
|
||||
filepath, asset_dir, asset_name, False, default_conversion)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
# Create Asset Container
|
||||
create_container(container=container_name, path=asset_dir)
|
||||
|
||||
def imprint(
|
||||
self,
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
representation,
|
||||
product_type
|
||||
):
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"folder_path": folder_path,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": representation["id"],
|
||||
"parent": representation["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these should be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""Load and containerise representation into Content Browser.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): Product 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.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
# Create directory for asset and ayon container
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
version = context["version"]["version"]
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version:03d}"
|
||||
|
||||
default_conversion = False
|
||||
if options.get("default_conversion"):
|
||||
default_conversion = options.get("default_conversion")
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
self.import_and_containerize(path, asset_dir, asset_name,
|
||||
container_name, default_conversion)
|
||||
|
||||
product_type = context["product"]["productType"]
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
context["representation"],
|
||||
product_type
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
product_name = context["product"]["name"]
|
||||
product_type = context["product"]["productType"]
|
||||
version = context["version"]["version"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
# Create directory for folder and Ayon container
|
||||
suffix = "_CON"
|
||||
asset_name = product_name
|
||||
if folder_name:
|
||||
asset_name = f"{folder_name}_{product_name}"
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{product_name}_hero"
|
||||
else:
|
||||
name_version = f"{product_name}_v{version:03d}"
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = get_representation_path(repre_entity)
|
||||
|
||||
self.import_and_containerize(path, asset_dir, asset_name,
|
||||
container_name)
|
||||
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
repre_entity,
|
||||
product_type,
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load Skeletal Meshes form FBX."""
|
||||
import os
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
AYON_ASSET_DIR,
|
||||
create_container,
|
||||
imprint,
|
||||
)
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class SkeletalMeshFBXLoader(plugin.Loader):
|
||||
"""Load Unreal SkeletalMesh from FBX."""
|
||||
|
||||
product_types = {"rig", "skeletalMesh"}
|
||||
label = "Import FBX Skeletal Mesh"
|
||||
representations = {"fbx"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
root = AYON_ASSET_DIR
|
||||
|
||||
@staticmethod
|
||||
def get_task(filename, asset_dir, asset_name, replace):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.FbxImportUI()
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
options.set_editor_property(
|
||||
'automated_import_should_detect_type', False)
|
||||
options.set_editor_property('import_as_skeletal', True)
|
||||
options.set_editor_property('import_animations', False)
|
||||
options.set_editor_property('import_mesh', True)
|
||||
options.set_editor_property('import_materials', False)
|
||||
options.set_editor_property('import_textures', False)
|
||||
options.set_editor_property('skeleton', None)
|
||||
options.set_editor_property('create_physics_asset', False)
|
||||
|
||||
options.set_editor_property(
|
||||
'mesh_type_to_import',
|
||||
unreal.FBXImportType.FBXIT_SKELETAL_MESH)
|
||||
|
||||
options.skeletal_mesh_import_data.set_editor_property(
|
||||
'import_content_type',
|
||||
unreal.FBXImportContentType.FBXICT_ALL)
|
||||
|
||||
options.skeletal_mesh_import_data.set_editor_property(
|
||||
'normal_import_method',
|
||||
unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS)
|
||||
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
def import_and_containerize(
|
||||
self, filepath, asset_dir, asset_name, container_name
|
||||
):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
task = self.get_task(
|
||||
filepath, asset_dir, asset_name, False)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
# Create Asset Container
|
||||
create_container(container=container_name, path=asset_dir)
|
||||
|
||||
def imprint(
|
||||
self,
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
representation,
|
||||
product_type
|
||||
):
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"folder_path": folder_path,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": representation["id"],
|
||||
"parent": representation["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these should be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""Load and containerise representation into Content Browser.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): Product 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.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
# Create directory for asset and Ayon container
|
||||
folder_name = context["folder"]["name"]
|
||||
product_type = context["product"]["productType"]
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
version_entity = context["version"]
|
||||
# Check if version is hero version and use different name
|
||||
version = version_entity["version"]
|
||||
if version < 0:
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version:03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix=""
|
||||
)
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
self.import_and_containerize(
|
||||
path, asset_dir, asset_name, container_name)
|
||||
|
||||
self.imprint(
|
||||
folder_name,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
context["representation"],
|
||||
product_type
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
product_name = context["product"]["name"]
|
||||
product_type = context["product"]["productType"]
|
||||
version = context["version"]["version"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
# Create directory for asset and Ayon container
|
||||
suffix = "_CON"
|
||||
asset_name = product_name
|
||||
if folder_name:
|
||||
asset_name = f"{folder_name}_{product_name}"
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{product_name}_hero"
|
||||
else:
|
||||
name_version = f"{product_name}_v{version:03d}"
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = get_representation_path(repre_entity)
|
||||
|
||||
self.import_and_containerize(
|
||||
path, asset_dir, asset_name, container_name)
|
||||
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
repre_entity,
|
||||
product_type
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Loader for Static Mesh alembics."""
|
||||
import os
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
AYON_ASSET_DIR,
|
||||
create_container,
|
||||
imprint,
|
||||
)
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class StaticMeshAlembicLoader(plugin.Loader):
|
||||
"""Load Unreal StaticMesh from Alembic"""
|
||||
|
||||
product_types = {"model", "staticMesh"}
|
||||
label = "Import Alembic Static Mesh"
|
||||
representations = {"abc"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
root = AYON_ASSET_DIR
|
||||
|
||||
@staticmethod
|
||||
def get_task(filename, asset_dir, asset_name, replace, default_conversion):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.AbcImportSettings()
|
||||
sm_settings = unreal.AbcStaticMeshSettings()
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
# set import options here
|
||||
# Unreal 4.24 ignores the settings. It works with Unreal 4.26
|
||||
options.set_editor_property(
|
||||
'import_type', unreal.AlembicImportType.STATIC_MESH)
|
||||
|
||||
sm_settings.set_editor_property('merge_meshes', True)
|
||||
|
||||
if not default_conversion:
|
||||
conversion_settings = unreal.AbcConversionSettings(
|
||||
preset=unreal.AbcConversionPreset.CUSTOM,
|
||||
flip_u=False, flip_v=False,
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, 1.0, 1.0])
|
||||
options.conversion_settings = conversion_settings
|
||||
|
||||
options.static_mesh_settings = sm_settings
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
def import_and_containerize(
|
||||
self, filepath, asset_dir, asset_name, container_name,
|
||||
default_conversion=False
|
||||
):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
task = self.get_task(
|
||||
filepath, asset_dir, asset_name, False, default_conversion)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
# Create Asset Container
|
||||
create_container(container=container_name, path=asset_dir)
|
||||
|
||||
def imprint(
|
||||
self,
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
representation,
|
||||
product_type,
|
||||
):
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"folder_path": folder_path,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": representation["id"],
|
||||
"parent": representation["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these should be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type
|
||||
}
|
||||
imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""Load and containerise representation into Content Browser.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): Product 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.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
# Create directory for asset and Ayon container
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["path"]
|
||||
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
version = context["version"]["version"]
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version:03d}"
|
||||
|
||||
default_conversion = False
|
||||
if options.get("default_conversion"):
|
||||
default_conversion = options.get("default_conversion")
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
self.import_and_containerize(path, asset_dir, asset_name,
|
||||
container_name, default_conversion)
|
||||
|
||||
product_type = context["product"]["productType"]
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
context["representation"],
|
||||
product_type
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
product_name = context["product"]["name"]
|
||||
product_type = context["product"]["productType"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
# Create directory for asset and Ayon container
|
||||
suffix = "_CON"
|
||||
asset_name = product_name
|
||||
if folder_name:
|
||||
asset_name = f"{folder_name}_{product_name}"
|
||||
version = context["version"]["version"]
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{product_name}_hero"
|
||||
else:
|
||||
name_version = f"{product_name}_v{version:03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = get_representation_path(repre_entity)
|
||||
|
||||
self.import_and_containerize(path, asset_dir, asset_name,
|
||||
container_name)
|
||||
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
repre_entity,
|
||||
product_type
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load Static meshes form FBX."""
|
||||
import os
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api.pipeline import (
|
||||
AYON_ASSET_DIR,
|
||||
create_container,
|
||||
imprint,
|
||||
)
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class StaticMeshFBXLoader(plugin.Loader):
|
||||
"""Load Unreal StaticMesh from FBX."""
|
||||
|
||||
product_types = {"model", "staticMesh"}
|
||||
label = "Import FBX Static Mesh"
|
||||
representations = {"fbx"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
root = AYON_ASSET_DIR
|
||||
|
||||
@staticmethod
|
||||
def get_task(filename, asset_dir, asset_name, replace):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.FbxImportUI()
|
||||
import_data = unreal.FbxStaticMeshImportData()
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
# set import options here
|
||||
options.set_editor_property(
|
||||
'automated_import_should_detect_type', False)
|
||||
options.set_editor_property('import_animations', False)
|
||||
|
||||
import_data.set_editor_property('combine_meshes', True)
|
||||
import_data.set_editor_property('remove_degenerates', False)
|
||||
|
||||
options.static_mesh_import_data = import_data
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
def import_and_containerize(
|
||||
self, filepath, asset_dir, asset_name, container_name
|
||||
):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
task = self.get_task(
|
||||
filepath, asset_dir, asset_name, False)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
# Create Asset Container
|
||||
create_container(container=container_name, path=asset_dir)
|
||||
|
||||
def imprint(
|
||||
self,
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
repre_entity,
|
||||
product_type
|
||||
):
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"namespace": asset_dir,
|
||||
"folder_path": folder_path,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": repre_entity["id"],
|
||||
"parent": repre_entity["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these shold be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""Load and containerise representation into Content Browser.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): Product 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.
|
||||
options (dict): Those would be data to be imprinted.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
# Create directory for asset and Ayon container
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
version = context["version"]["version"]
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version:03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix=""
|
||||
)
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
self.import_and_containerize(
|
||||
path, asset_dir, asset_name, container_name)
|
||||
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
context["representation"],
|
||||
context["product"]["productType"]
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
product_name = context["product"]["name"]
|
||||
product_type = context["product"]["productType"]
|
||||
version = context["version"]["version"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
# Create directory for asset and Ayon container
|
||||
suffix = "_CON"
|
||||
asset_name = product_name
|
||||
if folder_name:
|
||||
asset_name = f"{folder_name}_{product_name}"
|
||||
# Check if version is hero version and use different name
|
||||
if version < 0:
|
||||
name_version = f"{product_name}_hero"
|
||||
else:
|
||||
name_version = f"{product_name}_v{version:03d}"
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{self.root}/{folder_name}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
path = get_representation_path(repre_entity)
|
||||
|
||||
self.import_and_containerize(
|
||||
path, asset_dir, asset_name, container_name)
|
||||
|
||||
self.imprint(
|
||||
folder_path,
|
||||
asset_dir,
|
||||
container_name,
|
||||
asset_name,
|
||||
repre_entity,
|
||||
product_type,
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load UAsset."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class UAssetLoader(plugin.Loader):
|
||||
"""Load UAsset."""
|
||||
|
||||
product_types = {"uasset"}
|
||||
label = "Load UAsset"
|
||||
representations = {"uasset"}
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
extension = "uasset"
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""Load and containerise representation into Content Browser.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): Product 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.
|
||||
options (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
|
||||
"""
|
||||
|
||||
# Create directory for asset and Ayon container
|
||||
root = unreal_pipeline.AYON_ASSET_DIR
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{folder_name}/{name}", suffix=""
|
||||
)
|
||||
|
||||
unique_number = 1
|
||||
while unreal.EditorAssetLibrary.does_directory_exist(
|
||||
f"{asset_dir}_{unique_number:02}"
|
||||
):
|
||||
unique_number += 1
|
||||
|
||||
asset_dir = f"{asset_dir}_{unique_number:02}"
|
||||
container_name = f"{container_name}_{unique_number:02}{suffix}"
|
||||
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
destination_path = asset_dir.replace(
|
||||
"/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1)
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
shutil.copy(
|
||||
path,
|
||||
f"{destination_path}/{name}_{unique_number:02}.{self.extension}")
|
||||
|
||||
# Create Asset Container
|
||||
unreal_pipeline.create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
product_type = context["product"]["productType"]
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"namespace": asset_dir,
|
||||
"folder_path": folder_path,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["id"],
|
||||
"parent": context["representation"]["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these should be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
asset_dir = container["namespace"]
|
||||
|
||||
product_name = context["product"]["name"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
unique_number = container["container_name"].split("_")[-2]
|
||||
|
||||
destination_path = asset_dir.replace(
|
||||
"/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
for asset in asset_content:
|
||||
obj = ar.get_asset_by_object_path(asset).get_asset()
|
||||
if obj.get_class().get_name() != "AyonAssetContainer":
|
||||
unreal.EditorAssetLibrary.delete_asset(asset)
|
||||
|
||||
update_filepath = get_representation_path(repre_entity)
|
||||
|
||||
shutil.copy(
|
||||
update_filepath,
|
||||
f"{destination_path}/{product_name}_{unique_number}.{self.extension}"
|
||||
)
|
||||
|
||||
container_path = f'{container["namespace"]}/{container["objectName"]}'
|
||||
# update metadata
|
||||
unreal_pipeline.imprint(
|
||||
container_path,
|
||||
{
|
||||
"representation": repre_entity["id"],
|
||||
"parent": repre_entity["versionId"],
|
||||
}
|
||||
)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = Path(path).parent.as_posix()
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
||||
|
||||
class UMapLoader(UAssetLoader):
|
||||
"""Load Level."""
|
||||
|
||||
product_types = {"uasset"}
|
||||
label = "Load Level"
|
||||
representations = {"umap"}
|
||||
|
||||
extension = "umap"
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Loader for Yeti Cache."""
|
||||
import os
|
||||
import json
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
get_representation_path,
|
||||
AYON_CONTAINER_ID
|
||||
)
|
||||
from ayon_core.hosts.unreal.api import plugin
|
||||
from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class YetiLoader(plugin.Loader):
|
||||
"""Load Yeti Cache"""
|
||||
|
||||
product_types = {"yeticacheUE"}
|
||||
label = "Import Yeti"
|
||||
representations = {"abc"}
|
||||
icon = "pagelines"
|
||||
color = "orange"
|
||||
|
||||
@staticmethod
|
||||
def get_task(filename, asset_dir, asset_name, replace):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.AbcImportSettings()
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def is_groom_module_active():
|
||||
"""
|
||||
Check if Groom plugin is active.
|
||||
|
||||
This is a workaround, because the Unreal python API don't have
|
||||
any method to check if plugin is active.
|
||||
"""
|
||||
prj_file = unreal.Paths.get_project_file_path()
|
||||
|
||||
with open(prj_file, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
plugins = data.get("Plugins")
|
||||
|
||||
if not plugins:
|
||||
return False
|
||||
|
||||
plugin_names = [p.get("Name") for p in plugins]
|
||||
|
||||
return "HairStrands" in plugin_names
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""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): Product 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
|
||||
|
||||
"""
|
||||
# Check if Groom plugin is active
|
||||
if not self.is_groom_module_active():
|
||||
raise RuntimeError("Groom plugin is not activated.")
|
||||
|
||||
# Create directory for asset and Ayon container
|
||||
root = unreal_pipeline.AYON_ASSET_DIR
|
||||
folder_path = context["folder"]["path"]
|
||||
folder_name = context["folder"]["name"]
|
||||
suffix = "_CON"
|
||||
asset_name = f"{folder_name}_{name}" if folder_name else f"{name}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{folder_name}/{name}", suffix="")
|
||||
|
||||
unique_number = 1
|
||||
while unreal.EditorAssetLibrary.does_directory_exist(
|
||||
f"{asset_dir}_{unique_number:02}"
|
||||
):
|
||||
unique_number += 1
|
||||
|
||||
asset_dir = f"{asset_dir}_{unique_number:02}"
|
||||
container_name = f"{container_name}_{unique_number:02}{suffix}"
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
task = self.get_task(path, asset_dir, asset_name, False)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501
|
||||
|
||||
# Create Asset Container
|
||||
unreal_pipeline.create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
product_type = context["product"]["productType"]
|
||||
data = {
|
||||
"schema": "ayon:container-2.0",
|
||||
"id": AYON_CONTAINER_ID,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"folder_path": folder_path,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["id"],
|
||||
"parent": context["representation"]["versionId"],
|
||||
"product_type": product_type,
|
||||
# TODO these shold be probably removed
|
||||
"asset": folder_path,
|
||||
"family": product_type,
|
||||
}
|
||||
unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, context):
|
||||
repre_entity = context["representation"]
|
||||
name = container["asset_name"]
|
||||
source_path = get_representation_path(repre_entity)
|
||||
destination_path = container["namespace"]
|
||||
|
||||
task = self.get_task(source_path, destination_path, name, True)
|
||||
|
||||
# do import fbx and replace existing data
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
container_path = f'{container["namespace"]}/{container["objectName"]}'
|
||||
# update metadata
|
||||
unreal_pipeline.imprint(
|
||||
container_path,
|
||||
{
|
||||
"representation": repre_entity["id"],
|
||||
"parent": repre_entity["versionId"],
|
||||
})
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
destination_path, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect current project path."""
|
||||
import unreal # noqa
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectUnrealCurrentFile(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file into context."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
label = "Unreal Current File"
|
||||
hosts = ['unreal']
|
||||
|
||||
def process(self, context):
|
||||
"""Inject the current working file."""
|
||||
current_file = unreal.Paths.get_project_file_path()
|
||||
context.data['currentFile'] = current_file
|
||||
|
||||
assert current_file != '', "Current file is empty. " \
|
||||
"Save the file before continuing."
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import unreal
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectInstanceMembers(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Collect members of instance.
|
||||
|
||||
This collector will collect the assets for the families that support to
|
||||
have them included as External Data, and will add them to the instance
|
||||
as members.
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
hosts = ["unreal"]
|
||||
families = ["camera", "look", "unrealStaticMesh", "uasset"]
|
||||
label = "Collect Instance Members"
|
||||
|
||||
def process(self, instance):
|
||||
"""Collect members of instance."""
|
||||
self.log.info("Collecting instance members")
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
inst_path = instance.data.get('instance_path')
|
||||
inst_name = inst_path.split('/')[-1]
|
||||
|
||||
pub_instance = ar.get_asset_by_object_path(
|
||||
f"{inst_path}.{inst_name}").get_asset()
|
||||
|
||||
if not pub_instance:
|
||||
self.log.error(f"{inst_path}.{inst_name}")
|
||||
raise RuntimeError(f"Instance {instance} not found.")
|
||||
|
||||
if not pub_instance.get_editor_property("add_external_assets"):
|
||||
# No external assets in the instance
|
||||
return
|
||||
|
||||
assets = pub_instance.get_editor_property('asset_data_external')
|
||||
|
||||
members = [asset.get_path_name() for asset in assets]
|
||||
|
||||
self.log.debug(f"Members: {members}")
|
||||
|
||||
instance.data["members"] = members
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
from ayon_core.pipeline import Anatomy
|
||||
from ayon_core.hosts.unreal.api import pipeline
|
||||
|
||||
|
||||
class CollectRenderInstances(pyblish.api.InstancePlugin):
|
||||
""" This collector will try to find all the rendered frames.
|
||||
|
||||
"""
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["unreal"]
|
||||
families = ["render"]
|
||||
label = "Collect Render Instances"
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("Preparing Rendering Instances")
|
||||
|
||||
context = instance.context
|
||||
|
||||
data = instance.data
|
||||
data['remove'] = True
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
sequence = ar.get_asset_by_object_path(
|
||||
data.get('sequence')).get_asset()
|
||||
|
||||
sequences = [{
|
||||
"sequence": sequence,
|
||||
"output": data.get('output'),
|
||||
"frame_range": (
|
||||
data.get('frameStart'), data.get('frameEnd'))
|
||||
}]
|
||||
|
||||
for s in sequences:
|
||||
self.log.debug(f"Processing: {s.get('sequence').get_name()}")
|
||||
subscenes = pipeline.get_subsequences(s.get('sequence'))
|
||||
|
||||
if subscenes:
|
||||
for ss in subscenes:
|
||||
sequences.append({
|
||||
"sequence": ss.get_sequence(),
|
||||
"output": (f"{s.get('output')}/"
|
||||
f"{ss.get_sequence().get_name()}"),
|
||||
"frame_range": (
|
||||
ss.get_start_frame(), ss.get_end_frame() - 1)
|
||||
})
|
||||
else:
|
||||
# Avoid creating instances for camera sequences
|
||||
if "_camera" not in s.get('sequence').get_name():
|
||||
seq = s.get('sequence')
|
||||
seq_name = seq.get_name()
|
||||
|
||||
product_type = "render"
|
||||
new_product_name = f"{data.get('productName')}_{seq_name}"
|
||||
new_instance = context.create_instance(
|
||||
new_product_name
|
||||
)
|
||||
new_instance[:] = seq_name
|
||||
|
||||
new_data = new_instance.data
|
||||
|
||||
new_data["folderPath"] = f"/{s.get('output')}"
|
||||
new_data["setMembers"] = seq_name
|
||||
new_data["productName"] = new_product_name
|
||||
new_data["productType"] = product_type
|
||||
new_data["family"] = product_type
|
||||
new_data["families"] = [product_type, "review"]
|
||||
new_data["parent"] = data.get("parent")
|
||||
new_data["level"] = data.get("level")
|
||||
new_data["output"] = s.get('output')
|
||||
new_data["fps"] = seq.get_display_rate().numerator
|
||||
new_data["frameStart"] = int(s.get('frame_range')[0])
|
||||
new_data["frameEnd"] = int(s.get('frame_range')[1])
|
||||
new_data["sequence"] = seq.get_path_name()
|
||||
new_data["master_sequence"] = data["master_sequence"]
|
||||
new_data["master_level"] = data["master_level"]
|
||||
|
||||
self.log.debug(f"new instance data: {new_data}")
|
||||
|
||||
try:
|
||||
project = get_current_project_name()
|
||||
anatomy = Anatomy(project)
|
||||
root = anatomy.roots['renders']
|
||||
except Exception as e:
|
||||
raise Exception((
|
||||
"Could not find render root "
|
||||
"in anatomy settings.")) from e
|
||||
|
||||
render_dir = f"{root}/{project}/{s.get('output')}"
|
||||
render_path = Path(render_dir)
|
||||
|
||||
frames = []
|
||||
|
||||
for x in render_path.iterdir():
|
||||
if x.is_file() and x.suffix == '.png':
|
||||
frames.append(str(x.name))
|
||||
|
||||
if "representations" not in new_instance.data:
|
||||
new_instance.data["representations"] = []
|
||||
|
||||
repr = {
|
||||
'frameStart': instance.data["frameStart"],
|
||||
'frameEnd': instance.data["frameEnd"],
|
||||
'name': 'png',
|
||||
'ext': 'png',
|
||||
'files': frames,
|
||||
'stagingDir': render_dir,
|
||||
'tags': ['review']
|
||||
}
|
||||
new_instance.data["representations"].append(repr)
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Extract camera from Unreal."""
|
||||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.unreal.api.pipeline import UNREAL_VERSION
|
||||
|
||||
|
||||
class ExtractCamera(publish.Extractor):
|
||||
"""Extract a camera."""
|
||||
|
||||
label = "Extract Camera"
|
||||
hosts = ["unreal"]
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
# Define extract output file path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
fbx_filename = "{}.fbx".format(instance.name)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
# Check if the loaded level is the same of the instance
|
||||
if UNREAL_VERSION.major == 5:
|
||||
world = unreal.UnrealEditorSubsystem().get_editor_world()
|
||||
else:
|
||||
world = unreal.EditorLevelLibrary.get_editor_world()
|
||||
current_level = world.get_path_name()
|
||||
assert current_level == instance.data.get("level"), \
|
||||
"Wrong level loaded"
|
||||
|
||||
for member in instance.data.get('members'):
|
||||
data = ar.get_asset_by_object_path(member)
|
||||
if UNREAL_VERSION.major == 5:
|
||||
is_level_sequence = (
|
||||
data.asset_class_path.asset_name == "LevelSequence")
|
||||
else:
|
||||
is_level_sequence = (data.asset_class == "LevelSequence")
|
||||
|
||||
if is_level_sequence:
|
||||
sequence = data.get_asset()
|
||||
if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor >= 1:
|
||||
params = unreal.SequencerExportFBXParams(
|
||||
world=world,
|
||||
root_sequence=sequence,
|
||||
sequence=sequence,
|
||||
bindings=sequence.get_bindings(),
|
||||
master_tracks=sequence.get_master_tracks(),
|
||||
fbx_file_name=os.path.join(staging_dir, fbx_filename)
|
||||
)
|
||||
unreal.SequencerTools.export_level_sequence_fbx(params)
|
||||
elif UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor == 26:
|
||||
unreal.SequencerTools.export_fbx(
|
||||
world,
|
||||
sequence,
|
||||
sequence.get_bindings(),
|
||||
unreal.FbxExportOption(),
|
||||
os.path.join(staging_dir, fbx_filename)
|
||||
)
|
||||
else:
|
||||
# Unreal 5.0 or 4.27
|
||||
unreal.SequencerTools.export_level_sequence_fbx(
|
||||
world,
|
||||
sequence,
|
||||
sequence.get_bindings(),
|
||||
unreal.FbxExportOption(),
|
||||
os.path.join(staging_dir, fbx_filename)
|
||||
)
|
||||
|
||||
if not os.path.isfile(os.path.join(staging_dir, fbx_filename)):
|
||||
raise RuntimeError("Failed to extract camera")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
fbx_representation = {
|
||||
'name': 'fbx',
|
||||
'ext': 'fbx',
|
||||
'files': fbx_filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import json
|
||||
import math
|
||||
|
||||
import unreal
|
||||
from unreal import EditorLevelLibrary as ell
|
||||
from unreal import EditorAssetLibrary as eal
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractLayout(publish.Extractor):
|
||||
"""Extract a layout."""
|
||||
|
||||
label = "Extract Layout"
|
||||
hosts = ["unreal"]
|
||||
families = ["layout"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
# Check if the loaded level is the same of the instance
|
||||
current_level = ell.get_editor_world().get_path_name()
|
||||
assert current_level == instance.data.get("level"), \
|
||||
"Wrong level loaded"
|
||||
|
||||
json_data = []
|
||||
project_name = instance.context.data["projectName"]
|
||||
|
||||
for member in instance[:]:
|
||||
actor = ell.get_actor_reference(member)
|
||||
mesh = None
|
||||
|
||||
# Check type the type of mesh
|
||||
if actor.get_class().get_name() == 'SkeletalMeshActor':
|
||||
mesh = actor.skeletal_mesh_component.skeletal_mesh
|
||||
elif actor.get_class().get_name() == 'StaticMeshActor':
|
||||
mesh = actor.static_mesh_component.static_mesh
|
||||
|
||||
if mesh:
|
||||
# Search the reference to the Asset Container for the object
|
||||
path = unreal.Paths.get_path(mesh.get_path_name())
|
||||
filter = unreal.ARFilter(
|
||||
class_names=["AyonAssetContainer"], package_paths=[path])
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
try:
|
||||
asset_container = ar.get_assets(filter)[0].get_asset()
|
||||
except IndexError:
|
||||
self.log.error("AssetContainer not found.")
|
||||
return
|
||||
|
||||
parent_id = eal.get_metadata_tag(asset_container, "parent")
|
||||
family = eal.get_metadata_tag(asset_container, "family")
|
||||
|
||||
self.log.info("Parent: {}".format(parent_id))
|
||||
blend = ayon_api.get_representation_by_name(
|
||||
project_name, "blend", parent_id, fields={"id"}
|
||||
)
|
||||
blend_id = blend["id"]
|
||||
|
||||
json_element = {}
|
||||
json_element["reference"] = str(blend_id)
|
||||
json_element["family"] = family
|
||||
json_element["product_type"] = family
|
||||
json_element["instance_name"] = actor.get_name()
|
||||
json_element["asset_name"] = mesh.get_name()
|
||||
import_data = mesh.get_editor_property("asset_import_data")
|
||||
json_element["file_path"] = import_data.get_first_filename()
|
||||
transform = actor.get_actor_transform()
|
||||
|
||||
json_element["transform"] = {
|
||||
"translation": {
|
||||
"x": -transform.translation.x,
|
||||
"y": transform.translation.y,
|
||||
"z": transform.translation.z
|
||||
},
|
||||
"rotation": {
|
||||
"x": math.radians(transform.rotation.euler().x),
|
||||
"y": math.radians(transform.rotation.euler().y),
|
||||
"z": math.radians(180.0 - transform.rotation.euler().z)
|
||||
},
|
||||
"scale": {
|
||||
"x": transform.scale3d.x,
|
||||
"y": transform.scale3d.y,
|
||||
"z": transform.scale3d.z
|
||||
}
|
||||
}
|
||||
json_data.append(json_element)
|
||||
|
||||
json_filename = "{}.json".format(instance.name)
|
||||
json_path = os.path.join(staging_dir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_data, fp=file, indent=2)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
json_representation = {
|
||||
'name': 'json',
|
||||
'ext': 'json',
|
||||
'files': json_filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import os
|
||||
|
||||
import unreal
|
||||
from unreal import MaterialEditingLibrary as mat_lib
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractLook(publish.Extractor):
|
||||
"""Extract look."""
|
||||
|
||||
label = "Extract Look"
|
||||
hosts = ["unreal"]
|
||||
families = ["look"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
resources_dir = instance.data["resourcesDir"]
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
transfers = []
|
||||
|
||||
json_data = []
|
||||
|
||||
for member in instance:
|
||||
asset = ar.get_asset_by_object_path(member)
|
||||
obj = asset.get_asset()
|
||||
|
||||
name = asset.get_editor_property('asset_name')
|
||||
|
||||
json_element = {'material': str(name)}
|
||||
|
||||
material_obj = obj.get_editor_property('static_materials')[0]
|
||||
material = material_obj.material_interface
|
||||
|
||||
base_color = mat_lib.get_material_property_input_node(
|
||||
material, unreal.MaterialProperty.MP_BASE_COLOR)
|
||||
|
||||
base_color_name = base_color.get_editor_property('parameter_name')
|
||||
|
||||
texture = mat_lib.get_material_default_texture_parameter_value(
|
||||
material, base_color_name)
|
||||
|
||||
if texture:
|
||||
# Export Texture
|
||||
tga_filename = f"{instance.name}_{name}_texture.tga"
|
||||
|
||||
tga_exporter = unreal.TextureExporterTGA()
|
||||
|
||||
tga_export_task = unreal.AssetExportTask()
|
||||
|
||||
tga_export_task.set_editor_property('exporter', tga_exporter)
|
||||
tga_export_task.set_editor_property('automated', True)
|
||||
tga_export_task.set_editor_property('object', texture)
|
||||
tga_export_task.set_editor_property(
|
||||
'filename', f"{staging_dir}/{tga_filename}")
|
||||
tga_export_task.set_editor_property('prompt', False)
|
||||
tga_export_task.set_editor_property('selected', False)
|
||||
|
||||
unreal.Exporter.run_asset_export_task(tga_export_task)
|
||||
|
||||
json_element['tga_filename'] = tga_filename
|
||||
|
||||
transfers.append((
|
||||
f"{staging_dir}/{tga_filename}",
|
||||
f"{resources_dir}/{tga_filename}"))
|
||||
|
||||
fbx_filename = f"{instance.name}_{name}.fbx"
|
||||
|
||||
fbx_exporter = unreal.StaticMeshExporterFBX()
|
||||
fbx_exporter.set_editor_property('text', False)
|
||||
|
||||
options = unreal.FbxExportOption()
|
||||
options.set_editor_property('ascii', False)
|
||||
options.set_editor_property('collision', False)
|
||||
|
||||
task = unreal.AssetExportTask()
|
||||
task.set_editor_property('exporter', fbx_exporter)
|
||||
task.set_editor_property('options', options)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('object', object)
|
||||
task.set_editor_property(
|
||||
'filename', f"{staging_dir}/{fbx_filename}")
|
||||
task.set_editor_property('prompt', False)
|
||||
task.set_editor_property('selected', False)
|
||||
|
||||
unreal.Exporter.run_asset_export_task(task)
|
||||
|
||||
json_element['fbx_filename'] = fbx_filename
|
||||
|
||||
transfers.append((
|
||||
f"{staging_dir}/{fbx_filename}",
|
||||
f"{resources_dir}/{fbx_filename}"))
|
||||
|
||||
json_data.append(json_element)
|
||||
|
||||
json_filename = f"{instance.name}.json"
|
||||
json_path = os.path.join(staging_dir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_data, fp=file, indent=2)
|
||||
|
||||
if "transfers" not in instance.data:
|
||||
instance.data["transfers"] = []
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
json_representation = {
|
||||
'name': 'json',
|
||||
'ext': 'json',
|
||||
'files': json_filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
|
||||
instance.data["representations"].append(json_representation)
|
||||
instance.data["transfers"].extend(transfers)
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import unreal
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractUAsset(publish.Extractor):
|
||||
"""Extract a UAsset."""
|
||||
|
||||
label = "Extract UAsset"
|
||||
hosts = ["unreal"]
|
||||
families = ["uasset", "umap"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
extension = (
|
||||
"umap" if "umap" in instance.data.get("families") else "uasset")
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
self.log.debug("Performing extraction..")
|
||||
staging_dir = self.staging_dir(instance)
|
||||
|
||||
members = instance.data.get("members", [])
|
||||
|
||||
if not members:
|
||||
raise RuntimeError("No members found in instance.")
|
||||
|
||||
# UAsset publishing supports only one member
|
||||
obj = members[0]
|
||||
|
||||
asset = ar.get_asset_by_object_path(obj).get_asset()
|
||||
sys_path = unreal.SystemLibrary.get_system_path(asset)
|
||||
filename = Path(sys_path).name
|
||||
|
||||
shutil.copy(sys_path, staging_dir)
|
||||
|
||||
self.log.info(f"instance.data: {instance.data}")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": extension,
|
||||
"ext": extension,
|
||||
"files": filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import unreal
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateNoDependencies(pyblish.api.InstancePlugin):
|
||||
"""Ensure that the uasset has no dependencies
|
||||
|
||||
The uasset is checked for dependencies. If there are any, the instance
|
||||
cannot be published.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Check no dependencies"
|
||||
families = ["uasset"]
|
||||
hosts = ["unreal"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
all_dependencies = []
|
||||
|
||||
for obj in instance[:]:
|
||||
asset = ar.get_asset_by_object_path(obj)
|
||||
dependencies = ar.get_dependencies(
|
||||
asset.package_name,
|
||||
unreal.AssetRegistryDependencyOptions(
|
||||
include_soft_package_references=False,
|
||||
include_hard_package_references=True,
|
||||
include_searchable_names=False,
|
||||
include_soft_management_references=False,
|
||||
include_hard_management_references=False
|
||||
))
|
||||
if dependencies:
|
||||
for dep in dependencies:
|
||||
if str(dep).startswith("/Game/"):
|
||||
all_dependencies.append(str(dep))
|
||||
|
||||
if all_dependencies:
|
||||
raise RuntimeError(
|
||||
f"Dependencies found: {all_dependencies}")
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import clique
|
||||
import os
|
||||
import re
|
||||
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline.publish import PublishValidationError
|
||||
|
||||
|
||||
class ValidateSequenceFrames(pyblish.api.InstancePlugin):
|
||||
"""Ensure the sequence of frames is complete
|
||||
|
||||
The files found in the folder are checked against the frameStart and
|
||||
frameEnd of the instance. If the first or last file is not
|
||||
corresponding with the first or last frame it is flagged as invalid.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Sequence Frames"
|
||||
families = ["render"]
|
||||
hosts = ["unreal"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
representations = instance.data.get("representations")
|
||||
folder_attributes = (
|
||||
instance.data
|
||||
.get("folderEntity", {})
|
||||
.get("attrib", {})
|
||||
)
|
||||
for repr in representations:
|
||||
repr_files = repr["files"]
|
||||
if isinstance(repr_files, str):
|
||||
continue
|
||||
|
||||
ext = repr.get("ext")
|
||||
if not ext:
|
||||
_, ext = os.path.splitext(repr_files[0])
|
||||
elif not ext.startswith("."):
|
||||
ext = ".{}".format(ext)
|
||||
pattern = r"\D?(?P<index>(?P<padding>0*)\d+){}$".format(
|
||||
re.escape(ext))
|
||||
patterns = [pattern]
|
||||
|
||||
collections, remainder = clique.assemble(
|
||||
repr["files"], minimum_items=1, patterns=patterns)
|
||||
|
||||
if remainder:
|
||||
raise PublishValidationError(
|
||||
"Some files have been found outside a sequence. "
|
||||
f"Invalid files: {remainder}")
|
||||
if not collections:
|
||||
raise PublishValidationError(
|
||||
"We have been unable to find a sequence in the "
|
||||
"files. Please ensure the files are named "
|
||||
"appropriately. "
|
||||
f"Files: {repr_files}")
|
||||
if len(collections) > 1:
|
||||
raise PublishValidationError(
|
||||
"Multiple collections detected. There should be a single "
|
||||
"collection per representation. "
|
||||
f"Collections identified: {collections}")
|
||||
|
||||
collection = collections[0]
|
||||
frames = list(collection.indexes)
|
||||
|
||||
if instance.data.get("slate"):
|
||||
# Slate is not part of the frame range
|
||||
frames = frames[1:]
|
||||
|
||||
current_range = (frames[0], frames[-1])
|
||||
required_range = (folder_attributes["clipIn"],
|
||||
folder_attributes["clipOut"])
|
||||
|
||||
if current_range != required_range:
|
||||
raise PublishValidationError(
|
||||
f"Invalid frame range: {current_range} - "
|
||||
f"expected: {required_range}")
|
||||
|
||||
missing = collection.holes().indexes
|
||||
if missing:
|
||||
raise PublishValidationError(
|
||||
"Missing frames have been detected. "
|
||||
f"Missing frames: {missing}")
|
||||
|
|
@ -1,434 +0,0 @@
|
|||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from distutils import dir_util
|
||||
from distutils.dir_util import copy_tree
|
||||
from pathlib import Path
|
||||
from typing import List, Union
|
||||
|
||||
from qtpy import QtCore
|
||||
|
||||
import ayon_core.hosts.unreal.lib as ue_lib
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
|
||||
def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)):
|
||||
match = re.search(r"\[[1-9]+/[0-9]+]", line)
|
||||
if match is not None:
|
||||
split: list[str] = match.group().split("/")
|
||||
curr: float = float(split[0][1:])
|
||||
total: float = float(split[1][:-1])
|
||||
progress_signal.emit(int((curr / total) * 100.0))
|
||||
|
||||
|
||||
def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)):
|
||||
match = re.search("@progress", line)
|
||||
if match is not None:
|
||||
percent_match = re.search(r"\d{1,3}", line)
|
||||
progress_signal.emit(int(percent_match.group()))
|
||||
|
||||
|
||||
def retrieve_exit_code(line: str):
|
||||
match = re.search(r"ExitCode=\d+", line)
|
||||
if match is not None:
|
||||
split: list[str] = match.group().split("=")
|
||||
return int(split[1])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class UEWorker(QtCore.QObject):
|
||||
finished = QtCore.Signal(str)
|
||||
failed = QtCore.Signal(str, int)
|
||||
progress = QtCore.Signal(int)
|
||||
log = QtCore.Signal(str)
|
||||
|
||||
engine_path: Path = None
|
||||
env = None
|
||||
|
||||
def execute(self):
|
||||
raise NotImplementedError("Please implement this method!")
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.execute()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log.emit(str(e))
|
||||
self.log.emit(traceback.format_exc())
|
||||
self.failed.emit(str(e), 1)
|
||||
raise e
|
||||
|
||||
|
||||
class UEProjectGenerationWorker(UEWorker):
|
||||
stage_begin = QtCore.Signal(str)
|
||||
|
||||
ue_version: str = None
|
||||
project_name: str = None
|
||||
project_dir: Path = None
|
||||
dev_mode = False
|
||||
|
||||
def setup(self, ue_version: str,
|
||||
project_name: str,
|
||||
unreal_project_name,
|
||||
engine_path: Path,
|
||||
project_dir: Path,
|
||||
dev_mode: bool = False,
|
||||
env: dict = None):
|
||||
"""Set the worker with necessary parameters.
|
||||
|
||||
Args:
|
||||
ue_version (str): Unreal Engine version.
|
||||
project_name (str): Name of the project in AYON.
|
||||
unreal_project_name (str): Name of the project in Unreal.
|
||||
engine_path (Path): Path to the Unreal Engine.
|
||||
project_dir (Path): Path to the project directory.
|
||||
dev_mode (bool, optional): Whether to run the project in dev mode.
|
||||
Defaults to False.
|
||||
env (dict, optional): Environment variables. Defaults to None.
|
||||
|
||||
"""
|
||||
|
||||
self.ue_version = ue_version
|
||||
self.project_dir = project_dir
|
||||
self.env = env or os.environ
|
||||
|
||||
preset = get_project_settings(project_name)["unreal"]["project_setup"]
|
||||
|
||||
if dev_mode or preset["dev_mode"]:
|
||||
self.dev_mode = True
|
||||
|
||||
self.project_name = unreal_project_name
|
||||
self.engine_path = engine_path
|
||||
|
||||
def execute(self):
|
||||
# engine_path should be the location of UE_X.X folder
|
||||
|
||||
ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path,
|
||||
self.ue_version)
|
||||
cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version)
|
||||
project_file = self.project_dir / f"{self.project_name}.uproject"
|
||||
|
||||
print("--- Generating a new project ...")
|
||||
# 1st stage
|
||||
stage_count = 2
|
||||
if self.dev_mode:
|
||||
stage_count = 4
|
||||
|
||||
self.stage_begin.emit(
|
||||
("Generating a new UE project ... 1 out of "
|
||||
f"{stage_count}"))
|
||||
|
||||
# Need to copy the commandlet project to a temporary folder where
|
||||
# users don't need admin rights to write to.
|
||||
cmdlet_tmp = tempfile.TemporaryDirectory()
|
||||
cmdlet_filename = cmdlet_project.name
|
||||
cmdlet_dir = cmdlet_project.parent.as_posix()
|
||||
cmdlet_tmp_name = Path(cmdlet_tmp.name)
|
||||
cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename)
|
||||
copy_tree(
|
||||
cmdlet_dir,
|
||||
cmdlet_tmp_name.as_posix())
|
||||
|
||||
commandlet_cmd = [
|
||||
f"{ue_editor_exe.as_posix()}",
|
||||
f"{cmdlet_tmp_file.as_posix()}",
|
||||
"-run=AyonGenerateProject",
|
||||
f"{project_file.resolve().as_posix()}",
|
||||
]
|
||||
|
||||
if self.dev_mode:
|
||||
commandlet_cmd.append("-GenerateCode")
|
||||
|
||||
gen_process = subprocess.Popen(commandlet_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
for line in gen_process.stdout:
|
||||
decoded_line = line.decode(errors="replace")
|
||||
print(decoded_line, end="")
|
||||
self.log.emit(decoded_line)
|
||||
gen_process.stdout.close()
|
||||
return_code = gen_process.wait()
|
||||
|
||||
cmdlet_tmp.cleanup()
|
||||
|
||||
if return_code and return_code != 0:
|
||||
msg = (
|
||||
f"Failed to generate {self.project_name} "
|
||||
f"project! Exited with return code {return_code}"
|
||||
)
|
||||
self.failed.emit(msg, return_code)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
print("--- Project has been generated successfully.")
|
||||
self.stage_begin.emit(
|
||||
(f"Writing the Engine ID of the build UE ... 1"
|
||||
f" out of {stage_count}"))
|
||||
|
||||
if not project_file.is_file():
|
||||
msg = ("Failed to write the Engine ID into .uproject file! Can "
|
||||
"not read!")
|
||||
self.failed.emit(msg)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
with open(project_file.as_posix(), mode="r+") as pf:
|
||||
pf_json = json.load(pf)
|
||||
pf_json["EngineAssociation"] = ue_lib.get_build_id(
|
||||
self.engine_path,
|
||||
self.ue_version
|
||||
)
|
||||
print(pf_json["EngineAssociation"])
|
||||
pf.seek(0)
|
||||
json.dump(pf_json, pf, indent=4)
|
||||
pf.truncate()
|
||||
print("--- Engine ID has been written into the project file")
|
||||
|
||||
self.progress.emit(90)
|
||||
if self.dev_mode:
|
||||
# 2nd stage
|
||||
self.stage_begin.emit(
|
||||
(f"Generating project files ... 2 out of "
|
||||
f"{stage_count}"))
|
||||
|
||||
self.progress.emit(0)
|
||||
ubt_path = ue_lib.get_path_to_ubt(self.engine_path,
|
||||
self.ue_version)
|
||||
|
||||
arch = "Win64"
|
||||
if platform.system().lower() == "windows":
|
||||
arch = "Win64"
|
||||
elif platform.system().lower() == "linux":
|
||||
arch = "Linux"
|
||||
elif platform.system().lower() == "darwin":
|
||||
# we need to test this out
|
||||
arch = "Mac"
|
||||
|
||||
gen_prj_files_cmd = [ubt_path.as_posix(),
|
||||
"-projectfiles",
|
||||
f"-project={project_file}",
|
||||
"-progress"]
|
||||
gen_proc = subprocess.Popen(gen_prj_files_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
for line in gen_proc.stdout:
|
||||
decoded_line: str = line.decode(errors="replace")
|
||||
print(decoded_line, end="")
|
||||
self.log.emit(decoded_line)
|
||||
parse_prj_progress(decoded_line, self.progress)
|
||||
|
||||
gen_proc.stdout.close()
|
||||
return_code = gen_proc.wait()
|
||||
|
||||
if return_code and return_code != 0:
|
||||
msg = ("Failed to generate project files! "
|
||||
f"Exited with return code {return_code}")
|
||||
self.failed.emit(msg, return_code)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
self.stage_begin.emit(
|
||||
f"Building the project ... 3 out of {stage_count}")
|
||||
self.progress.emit(0)
|
||||
# 3rd stage
|
||||
build_prj_cmd = [ubt_path.as_posix(),
|
||||
f"-ModuleWithSuffix={self.project_name},3555",
|
||||
arch,
|
||||
"Development",
|
||||
"-TargetType=Editor",
|
||||
f"-Project={project_file}",
|
||||
f"{project_file}",
|
||||
"-IgnoreJunk"]
|
||||
|
||||
build_prj_proc = subprocess.Popen(build_prj_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
for line in build_prj_proc.stdout:
|
||||
decoded_line: str = line.decode(errors="replace")
|
||||
print(decoded_line, end="")
|
||||
self.log.emit(decoded_line)
|
||||
parse_comp_progress(decoded_line, self.progress)
|
||||
|
||||
build_prj_proc.stdout.close()
|
||||
return_code = build_prj_proc.wait()
|
||||
|
||||
if return_code and return_code != 0:
|
||||
msg = ("Failed to build project! "
|
||||
f"Exited with return code {return_code}")
|
||||
self.failed.emit(msg, return_code)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# ensure we have PySide2/6 installed in engine
|
||||
|
||||
self.progress.emit(0)
|
||||
self.stage_begin.emit(
|
||||
(f"Checking Qt bindings installation... {stage_count} "
|
||||
f" out of {stage_count}"))
|
||||
python_path = None
|
||||
if platform.system().lower() == "windows":
|
||||
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
|
||||
"Python3/Win64/python.exe")
|
||||
|
||||
if platform.system().lower() == "linux":
|
||||
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
|
||||
"Python3/Linux/bin/python3")
|
||||
|
||||
if platform.system().lower() == "darwin":
|
||||
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
|
||||
"Python3/Mac/bin/python3")
|
||||
|
||||
if not python_path:
|
||||
msg = "Unsupported platform"
|
||||
self.failed.emit(msg, 1)
|
||||
raise NotImplementedError(msg)
|
||||
if not python_path.exists():
|
||||
msg = f"Unreal Python not found at {python_path}"
|
||||
self.failed.emit(msg, 1)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
pyside_version = "PySide2"
|
||||
ue_version = self.ue_version.split(".")
|
||||
if int(ue_version[0]) == 5 and int(ue_version[1]) >= 4:
|
||||
# Use PySide6 6.6.3 because 6.7.0 had a bug
|
||||
# - 'QPushButton' can't be added to 'QBoxLayout'
|
||||
pyside_version = "PySide6==6.6.3"
|
||||
|
||||
site_packages_prefix = python_path.parent.as_posix()
|
||||
|
||||
pyside_cmd = [
|
||||
python_path.as_posix(),
|
||||
"-m", "pip",
|
||||
"install",
|
||||
"--ignore-installed",
|
||||
pyside_version,
|
||||
|
||||
]
|
||||
|
||||
if platform.system().lower() == "windows":
|
||||
pyside_cmd += ["--target", site_packages_prefix]
|
||||
|
||||
print(f"--- Installing {pyside_version} ...")
|
||||
print(" ".join(pyside_cmd))
|
||||
|
||||
pyside_install = subprocess.Popen(pyside_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
for line in pyside_install.stdout:
|
||||
decoded_line: str = line.decode(errors="replace")
|
||||
print(decoded_line, end="")
|
||||
self.log.emit(decoded_line)
|
||||
|
||||
pyside_install.stdout.close()
|
||||
return_code = pyside_install.wait()
|
||||
|
||||
if return_code and return_code != 0:
|
||||
msg = (f"Failed to create the project! {return_code} "
|
||||
f"The installation of {pyside_version} has failed!: {pyside_install}")
|
||||
self.failed.emit(msg, return_code)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
self.progress.emit(100)
|
||||
self.finished.emit("Project successfully built!")
|
||||
|
||||
|
||||
class UEPluginInstallWorker(UEWorker):
|
||||
installing = QtCore.Signal(str)
|
||||
|
||||
def setup(self, engine_path: Path, env: dict = None, ):
|
||||
self.engine_path = engine_path
|
||||
self.env = env or os.environ
|
||||
|
||||
def _build_and_move_plugin(self, plugin_build_path: Path):
|
||||
uat_path: Path = ue_lib.get_path_to_uat(self.engine_path)
|
||||
src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", ""))
|
||||
|
||||
if not os.path.isdir(src_plugin_dir):
|
||||
msg = "Path to the integration plugin is null!"
|
||||
self.failed.emit(msg, 1)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
if not uat_path.is_file():
|
||||
msg = "Building failed! Path to UAT is invalid!"
|
||||
self.failed.emit(msg, 1)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
temp_dir: Path = src_plugin_dir.parent / "Temp"
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
uplugin_path: Path = src_plugin_dir / "Ayon.uplugin"
|
||||
|
||||
# in order to successfully build the plugin,
|
||||
# It must be built outside the Engine directory and then moved
|
||||
build_plugin_cmd: List[str] = [f"{uat_path.as_posix()}",
|
||||
"BuildPlugin",
|
||||
f"-Plugin={uplugin_path.as_posix()}",
|
||||
f"-Package={temp_dir.as_posix()}"]
|
||||
|
||||
build_proc = subprocess.Popen(build_plugin_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
return_code: Union[None, int] = None
|
||||
for line in build_proc.stdout:
|
||||
decoded_line: str = line.decode(errors="replace")
|
||||
print(decoded_line, end="")
|
||||
self.log.emit(decoded_line)
|
||||
if return_code is None:
|
||||
return_code = retrieve_exit_code(decoded_line)
|
||||
parse_comp_progress(decoded_line, self.progress)
|
||||
|
||||
build_proc.stdout.close()
|
||||
build_proc.wait()
|
||||
|
||||
if return_code and return_code != 0:
|
||||
msg = ("Failed to build plugin"
|
||||
f" project! Exited with return code {return_code}")
|
||||
dir_util.remove_tree(temp_dir.as_posix())
|
||||
self.failed.emit(msg, return_code)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# Copy the contents of the 'Temp' dir into the
|
||||
# 'Ayon' directory in the engine
|
||||
dir_util.copy_tree(temp_dir.as_posix(),
|
||||
plugin_build_path.as_posix())
|
||||
|
||||
# We need to also copy the config folder.
|
||||
# The UAT doesn't include the Config folder in the build
|
||||
plugin_install_config_path: Path = plugin_build_path / "Config"
|
||||
src_plugin_config_path = src_plugin_dir / "Config"
|
||||
|
||||
dir_util.copy_tree(src_plugin_config_path.as_posix(),
|
||||
plugin_install_config_path.as_posix())
|
||||
|
||||
dir_util.remove_tree(temp_dir.as_posix())
|
||||
|
||||
def execute(self):
|
||||
src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", ""))
|
||||
|
||||
if not os.path.isdir(src_plugin_dir):
|
||||
msg = "Path to the integration plugin is null!"
|
||||
self.failed.emit(msg, 1)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# Create a path to the plugin in the engine
|
||||
op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \
|
||||
"/Ayon"
|
||||
|
||||
if not op_plugin_path.is_dir():
|
||||
self.installing.emit("Installing and building the plugin ...")
|
||||
op_plugin_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
engine_plugin_config_path = op_plugin_path / "Config"
|
||||
engine_plugin_config_path.mkdir(exist_ok=True)
|
||||
|
||||
dir_util._path_created = {}
|
||||
|
||||
if not (op_plugin_path / "Binaries").is_dir() \
|
||||
or not (op_plugin_path / "Intermediate").is_dir():
|
||||
self.installing.emit("Building the plugin ...")
|
||||
print("--- Building the plugin...")
|
||||
|
||||
self._build_and_move_plugin(op_plugin_path)
|
||||
|
||||
self.finished.emit("Plugin successfully installed")
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from .splash_screen import SplashScreen
|
||||
|
||||
__all__ = (
|
||||
"SplashScreen",
|
||||
)
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
from ayon_core import style, resources
|
||||
|
||||
|
||||
class SplashScreen(QtWidgets.QDialog):
|
||||
"""Splash screen for executing a process on another thread. It is able
|
||||
to inform about the progress of the process and log given information.
|
||||
"""
|
||||
|
||||
splash_icon = None
|
||||
top_label = None
|
||||
show_log_btn: QtWidgets.QLabel = None
|
||||
progress_bar = None
|
||||
log_text: QtWidgets.QLabel = None
|
||||
scroll_area: QtWidgets.QScrollArea = None
|
||||
close_btn: QtWidgets.QPushButton = None
|
||||
scroll_bar: QtWidgets.QScrollBar = None
|
||||
|
||||
is_log_visible = False
|
||||
is_scroll_auto = True
|
||||
|
||||
thread_return_code = None
|
||||
q_thread: QtCore.QThread = None
|
||||
|
||||
def __init__(self,
|
||||
window_title: str,
|
||||
splash_icon=None,
|
||||
window_icon=None):
|
||||
"""
|
||||
Args:
|
||||
window_title (str): String which sets the window title
|
||||
splash_icon (str | bytes | None): A resource (pic) which is used
|
||||
for the splash icon
|
||||
window_icon (str | bytes | None: A resource (pic) which is used for
|
||||
the window's icon
|
||||
"""
|
||||
super(SplashScreen, self).__init__()
|
||||
|
||||
if splash_icon is None:
|
||||
splash_icon = resources.get_ayon_icon_filepath()
|
||||
|
||||
if window_icon is None:
|
||||
window_icon = resources.get_ayon_icon_filepath()
|
||||
|
||||
self.splash_icon = splash_icon
|
||||
self.setWindowIcon(QtGui.QIcon(window_icon))
|
||||
self.setWindowTitle(window_title)
|
||||
self.init_ui()
|
||||
|
||||
def was_proc_successful(self) -> bool:
|
||||
return self.thread_return_code == 0
|
||||
|
||||
def start_thread(self, q_thread: QtCore.QThread):
|
||||
"""Saves the reference to this thread and starts it.
|
||||
|
||||
Args:
|
||||
q_thread (QtCore.QThread): A QThread containing a given worker
|
||||
(QtCore.QObject)
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if not q_thread:
|
||||
raise RuntimeError("Failed to run a worker thread! "
|
||||
"The thread is null!")
|
||||
|
||||
self.q_thread = q_thread
|
||||
self.q_thread.start()
|
||||
|
||||
@QtCore.Slot()
|
||||
def quit_and_close(self):
|
||||
"""Quits the thread and closes the splash screen. Note that this means
|
||||
the thread has exited with the return code 0!
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.thread_return_code = 0
|
||||
self.q_thread.quit()
|
||||
|
||||
if not self.q_thread.wait(5000):
|
||||
raise RuntimeError("Failed to quit the QThread! "
|
||||
"The deadline has been reached! The thread "
|
||||
"has not finished it's execution!.")
|
||||
self.close()
|
||||
|
||||
|
||||
@QtCore.Slot()
|
||||
def toggle_log(self):
|
||||
if self.is_log_visible:
|
||||
self.scroll_area.hide()
|
||||
width = self.width()
|
||||
self.adjustSize()
|
||||
self.resize(width, self.height())
|
||||
else:
|
||||
self.scroll_area.show()
|
||||
self.scroll_bar.setValue(self.scroll_bar.maximum())
|
||||
self.resize(self.width(), 300)
|
||||
|
||||
self.is_log_visible = not self.is_log_visible
|
||||
|
||||
def show_ui(self):
|
||||
"""Shows the splash screen. BEWARE THAT THIS FUNCTION IS BLOCKING
|
||||
(The execution of code can not proceed further beyond this function
|
||||
until the splash screen is closed!)
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.show()
|
||||
self.exec_()
|
||||
|
||||
def init_ui(self):
|
||||
self.resize(450, 100)
|
||||
self.setMinimumWidth(250)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
# Top Section
|
||||
self.top_label = QtWidgets.QLabel(self)
|
||||
self.top_label.setText("Starting process ...")
|
||||
self.top_label.setWordWrap(True)
|
||||
|
||||
icon = QtWidgets.QLabel(self)
|
||||
icon.setPixmap(QtGui.QPixmap(self.splash_icon))
|
||||
icon.setFixedHeight(45)
|
||||
icon.setFixedWidth(45)
|
||||
icon.setScaledContents(True)
|
||||
|
||||
self.close_btn = QtWidgets.QPushButton(self)
|
||||
self.close_btn.setText("Quit")
|
||||
self.close_btn.clicked.connect(self.close)
|
||||
self.close_btn.setFixedWidth(80)
|
||||
self.close_btn.hide()
|
||||
|
||||
self.show_log_btn = QtWidgets.QPushButton(self)
|
||||
self.show_log_btn.setText("Show log")
|
||||
self.show_log_btn.setFixedWidth(80)
|
||||
self.show_log_btn.clicked.connect(self.toggle_log)
|
||||
|
||||
button_layout = QtWidgets.QVBoxLayout()
|
||||
button_layout.addWidget(self.show_log_btn)
|
||||
button_layout.addWidget(self.close_btn)
|
||||
|
||||
# Progress Bar
|
||||
self.progress_bar = QtWidgets.QProgressBar()
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
# Log Content
|
||||
self.scroll_area = QtWidgets.QScrollArea(self)
|
||||
self.scroll_area.hide()
|
||||
log_widget = QtWidgets.QWidget(self.scroll_area)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(
|
||||
QtCore.Qt.ScrollBarAlwaysOn
|
||||
)
|
||||
self.scroll_area.setVerticalScrollBarPolicy(
|
||||
QtCore.Qt.ScrollBarAlwaysOn
|
||||
)
|
||||
self.scroll_area.setWidget(log_widget)
|
||||
|
||||
self.scroll_bar = self.scroll_area.verticalScrollBar()
|
||||
self.scroll_bar.sliderMoved.connect(self.on_scroll)
|
||||
|
||||
self.log_text = QtWidgets.QLabel(self)
|
||||
self.log_text.setText('')
|
||||
self.log_text.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
log_layout = QtWidgets.QVBoxLayout(log_widget)
|
||||
log_layout.addWidget(self.log_text)
|
||||
|
||||
top_layout = QtWidgets.QHBoxLayout()
|
||||
top_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
top_layout.addWidget(icon)
|
||||
top_layout.addSpacing(10)
|
||||
top_layout.addWidget(self.top_label)
|
||||
top_layout.addSpacing(10)
|
||||
top_layout.addLayout(button_layout)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addLayout(top_layout)
|
||||
main_layout.addSpacing(10)
|
||||
main_layout.addWidget(self.progress_bar)
|
||||
main_layout.addSpacing(10)
|
||||
main_layout.addWidget(self.scroll_area)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.CustomizeWindowHint
|
||||
| QtCore.Qt.WindowTitleHint
|
||||
| QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(self)
|
||||
center = desktop_rect.center()
|
||||
self.move(
|
||||
center.x() - (self.width() * 0.5),
|
||||
center.y() - (self.height() * 0.5)
|
||||
)
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def update_progress(self, value: int):
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def update_top_label_text(self, text: str):
|
||||
self.top_label.setText(text)
|
||||
|
||||
@QtCore.Slot(str, str)
|
||||
def append_log(self, text: str, end: str = ''):
|
||||
"""A slot used for receiving log info and appending it to scroll area's
|
||||
content.
|
||||
Args:
|
||||
text (str): A log text that will append to the current one in the
|
||||
scroll area.
|
||||
end (str): end string which can be appended to the end of the given
|
||||
line (for ex. a line break).
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.log_text.setText(self.log_text.text() + text + end)
|
||||
if self.is_scroll_auto:
|
||||
self.scroll_bar.setValue(self.scroll_bar.maximum())
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def on_scroll(self, position: int):
|
||||
"""
|
||||
A slot for the vertical scroll bar's movement. This ensures the
|
||||
auto-scrolling feature of the scroll area when the scroll bar is at its
|
||||
maximum value.
|
||||
|
||||
Args:
|
||||
position (int): Position value of the scroll bar.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if self.scroll_bar.maximum() == position:
|
||||
self.is_scroll_auto = True
|
||||
return
|
||||
|
||||
self.is_scroll_auto = False
|
||||
|
||||
@QtCore.Slot(str, int)
|
||||
def fail(self, text: str, return_code: int = 1):
|
||||
"""
|
||||
A slot used for signals which can emit when a worker (process) has
|
||||
failed. at this moment the splash screen doesn't close by itself.
|
||||
it has to be closed by the user.
|
||||
|
||||
Args:
|
||||
text (str): A text which can be set to the top label.
|
||||
|
||||
Returns:
|
||||
return_code (int): Return code of the thread's code
|
||||
"""
|
||||
self.top_label.setText(text)
|
||||
self.close_btn.show()
|
||||
self.thread_return_code = return_code
|
||||
self.q_thread.exit(return_code)
|
||||
self.q_thread.wait()
|
||||
|
|
@ -1,19 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa E402
|
||||
"""AYON lib functions."""
|
||||
# add vendor to sys path based on Python version
|
||||
import sys
|
||||
import os
|
||||
import site
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
|
||||
# Add Python version specific vendor folder
|
||||
python_version_dir = os.path.join(
|
||||
AYON_CORE_ROOT, "vendor", "python", "python_{}".format(sys.version[0])
|
||||
)
|
||||
# Prepend path in sys paths
|
||||
sys.path.insert(0, python_version_dir)
|
||||
site.addsitedir(python_version_dir)
|
||||
|
||||
from .local_settings import (
|
||||
IniSettingRegistry,
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ class HiddenDef(AbstractAttrDef):
|
|||
def __init__(self, key, default=None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
kwargs["hidden"] = True
|
||||
super(UnknownDef, self).__init__(key, **kwargs)
|
||||
super(HiddenDef, self).__init__(key, **kwargs)
|
||||
|
||||
def convert_value(self, value):
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.10"
|
||||
|
|
@ -7,6 +7,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
|
|||
class LauncherAction(AYONAddon, ITrayAction):
|
||||
label = "Launcher"
|
||||
name = "launcher_tool"
|
||||
version = "1.0.0"
|
||||
|
||||
def initialize(self, settings):
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from ayon_core.addon import AYONAddon, ITrayAddon
|
|||
|
||||
class LoaderAddon(AYONAddon, ITrayAddon):
|
||||
name = "loader_tool"
|
||||
version = "1.0.0"
|
||||
|
||||
def initialize(self, settings):
|
||||
# Tray attributes
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
|
|||
class PythonInterpreterAction(AYONAddon, ITrayAction):
|
||||
label = "Console"
|
||||
name = "python_interpreter"
|
||||
version = "1.0.0"
|
||||
admin_action = True
|
||||
|
||||
def initialize(self, settings):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
from .version import __version__
|
||||
from .structures import HostMsgAction
|
||||
from .webserver_module import (
|
||||
WebServerAddon
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"HostMsgAction",
|
||||
"WebServerAddon",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,22 +9,18 @@ from qtpy import QtWidgets
|
|||
from ayon_core.addon import ITrayService
|
||||
from ayon_core.tools.stdout_broker.window import ConsoleDialog
|
||||
|
||||
from .structures import HostMsgAction
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Host listener icon type
|
||||
class IconType:
|
||||
IDLE = "idle"
|
||||
RUNNING = "running"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class MsgAction:
|
||||
CONNECTING = "connecting"
|
||||
INITIALIZED = "initialized"
|
||||
ADD = "add"
|
||||
CLOSE = "close"
|
||||
|
||||
|
||||
class HostListener:
|
||||
def __init__(self, webserver, module):
|
||||
self._window_per_id = {}
|
||||
|
|
@ -96,22 +92,22 @@ class HostListener:
|
|||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
host_name, action, text = self._parse_message(msg)
|
||||
|
||||
if action == MsgAction.CONNECTING:
|
||||
if action == HostMsgAction.CONNECTING:
|
||||
self._action_per_id[host_name] = None
|
||||
# must be sent to main thread, or action wont trigger
|
||||
self.module.execute_in_main_thread(
|
||||
lambda: self._host_is_connecting(host_name, text))
|
||||
elif action == MsgAction.CLOSE:
|
||||
elif action == HostMsgAction.CLOSE:
|
||||
# clean close
|
||||
self._close(host_name)
|
||||
await ws.close()
|
||||
elif action == MsgAction.INITIALIZED:
|
||||
elif action == HostMsgAction.INITIALIZED:
|
||||
self.module.execute_in_main_thread(
|
||||
# must be queued as _host_is_connecting might not
|
||||
# be triggered/finished yet
|
||||
lambda: self._set_host_icon(host_name,
|
||||
IconType.RUNNING))
|
||||
elif action == MsgAction.ADD:
|
||||
elif action == HostMsgAction.ADD:
|
||||
self.module.execute_in_main_thread(
|
||||
lambda: self._add_text(host_name, text))
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
|
|
|
|||
6
client/ayon_core/modules/webserver/structures.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Host listener message actions
|
||||
class HostMsgAction:
|
||||
CONNECTING = "connecting"
|
||||
INITIALIZED = "initialized"
|
||||
ADD = "add"
|
||||
CLOSE = "close"
|
||||
1
client/ayon_core/modules/webserver/version.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "1.0.0"
|
||||
|
|
@ -26,9 +26,12 @@ import socket
|
|||
from ayon_core import resources
|
||||
from ayon_core.addon import AYONAddon, ITrayService
|
||||
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class WebServerAddon(AYONAddon, ITrayService):
|
||||
name = "webserver"
|
||||
version = __version__
|
||||
label = "WebServer"
|
||||
|
||||
webserver_url_env = "AYON_WEBSERVER_URL"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ from pyblish.lib import MessageHandler
|
|||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.lib import is_in_tests, initialize_ayon_connection, emit_event
|
||||
from ayon_core.lib import (
|
||||
is_in_tests,
|
||||
initialize_ayon_connection,
|
||||
emit_event,
|
||||
version_up
|
||||
)
|
||||
from ayon_core.addon import load_addons, AddonsManager
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
|
|
@ -21,6 +26,8 @@ from .template_data import get_template_data_with_names
|
|||
from .workfile import (
|
||||
get_workdir,
|
||||
get_custom_workfile_template_by_string_context,
|
||||
get_workfile_template_key_from_context,
|
||||
get_last_workfile
|
||||
)
|
||||
from . import (
|
||||
register_loader_plugin_path,
|
||||
|
|
@ -579,3 +586,48 @@ def get_process_id():
|
|||
if _process_id is None:
|
||||
_process_id = str(uuid.uuid4())
|
||||
return _process_id
|
||||
|
||||
|
||||
def version_up_current_workfile():
|
||||
"""Function to increment and save workfile
|
||||
"""
|
||||
host = registered_host()
|
||||
if not host.has_unsaved_changes():
|
||||
print("No unsaved changes, skipping file save..")
|
||||
return
|
||||
|
||||
project_name = get_current_project_name()
|
||||
folder_path = get_current_folder_path()
|
||||
task_name = get_current_task_name()
|
||||
host_name = get_current_host_name()
|
||||
|
||||
template_key = get_workfile_template_key_from_context(
|
||||
project_name,
|
||||
folder_path,
|
||||
task_name,
|
||||
host_name,
|
||||
)
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
data = get_template_data_with_names(
|
||||
project_name, folder_path, task_name, host_name
|
||||
)
|
||||
data["root"] = anatomy.roots
|
||||
|
||||
work_template = anatomy.get_template_item("work", template_key)
|
||||
|
||||
# Define saving file extension
|
||||
extensions = host.get_workfile_extensions()
|
||||
current_file = host.get_current_workfile()
|
||||
if current_file:
|
||||
extensions = [os.path.splitext(current_file)[-1]]
|
||||
|
||||
work_root = work_template["directory"].format_strict(data)
|
||||
file_template = work_template["file"].template
|
||||
last_workfile_path = get_last_workfile(
|
||||
work_root, file_template, data, extensions, True
|
||||
)
|
||||
new_workfile_path = version_up(last_workfile_path)
|
||||
if os.path.exists(new_workfile_path):
|
||||
new_workfile_path = version_up(new_workfile_path)
|
||||
host.save_workfile(new_workfile_path)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from .creator_plugins import (
|
|||
|
||||
# Changes of instances and context are send as tuple of 2 information
|
||||
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
|
||||
_NOT_SET = object()
|
||||
|
||||
|
||||
class UnavailableSharedData(Exception):
|
||||
|
|
@ -681,7 +682,7 @@ class PublishAttributeValues(AttributeValues):
|
|||
|
||||
@property
|
||||
def parent(self):
|
||||
self.publish_attributes.parent
|
||||
return self.publish_attributes.parent
|
||||
|
||||
|
||||
class PublishAttributes:
|
||||
|
|
@ -1401,6 +1402,11 @@ class CreateContext:
|
|||
self._current_folder_path = None
|
||||
self._current_task_name = None
|
||||
self._current_workfile_path = None
|
||||
self._current_project_settings = None
|
||||
|
||||
self._current_folder_entity = _NOT_SET
|
||||
self._current_task_entity = _NOT_SET
|
||||
self._current_task_type = _NOT_SET
|
||||
|
||||
self._current_project_anatomy = None
|
||||
|
||||
|
|
@ -1571,6 +1577,64 @@ class CreateContext:
|
|||
|
||||
return self._current_task_name
|
||||
|
||||
def get_current_task_type(self):
|
||||
"""Task type which was used as current context on context reset.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Task type.
|
||||
|
||||
"""
|
||||
if self._current_task_type is _NOT_SET:
|
||||
task_type = None
|
||||
task_entity = self.get_current_task_entity()
|
||||
if task_entity:
|
||||
task_type = task_entity["taskType"]
|
||||
self._current_task_type = task_type
|
||||
return self._current_task_type
|
||||
|
||||
def get_current_folder_entity(self):
|
||||
"""Folder entity for current context folder.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Folder entity.
|
||||
|
||||
"""
|
||||
if self._current_folder_entity is not _NOT_SET:
|
||||
return copy.deepcopy(self._current_folder_entity)
|
||||
folder_entity = None
|
||||
folder_path = self.get_current_folder_path()
|
||||
if folder_path:
|
||||
project_name = self.get_current_project_name()
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path
|
||||
)
|
||||
self._current_folder_entity = folder_entity
|
||||
return copy.deepcopy(self._current_folder_entity)
|
||||
|
||||
def get_current_task_entity(self):
|
||||
"""Task entity for current context task.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Task entity.
|
||||
|
||||
"""
|
||||
if self._current_task_entity is not _NOT_SET:
|
||||
return copy.deepcopy(self._current_task_entity)
|
||||
task_entity = None
|
||||
task_name = self.get_current_task_name()
|
||||
if task_name:
|
||||
folder_entity = self.get_current_folder_entity()
|
||||
if folder_entity:
|
||||
project_name = self.get_current_project_name()
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name,
|
||||
folder_id=folder_entity["id"],
|
||||
task_name=task_name
|
||||
)
|
||||
self._current_task_entity = task_entity
|
||||
return copy.deepcopy(self._current_task_entity)
|
||||
|
||||
|
||||
def get_current_workfile_path(self):
|
||||
"""Workfile path which was opened on context reset.
|
||||
|
||||
|
|
@ -1592,6 +1656,12 @@ class CreateContext:
|
|||
self._current_project_name)
|
||||
return self._current_project_anatomy
|
||||
|
||||
def get_current_project_settings(self):
|
||||
if self._current_project_settings is None:
|
||||
self._current_project_settings = get_project_settings(
|
||||
self.get_current_project_name())
|
||||
return self._current_project_settings
|
||||
|
||||
@property
|
||||
def context_has_changed(self):
|
||||
"""Host context has changed.
|
||||
|
|
@ -1718,7 +1788,12 @@ class CreateContext:
|
|||
self._current_task_name = task_name
|
||||
self._current_workfile_path = workfile_path
|
||||
|
||||
self._current_folder_entity = _NOT_SET
|
||||
self._current_task_entity = _NOT_SET
|
||||
self._current_task_type = _NOT_SET
|
||||
|
||||
self._current_project_anatomy = None
|
||||
self._current_project_settings = None
|
||||
|
||||
def reset_plugins(self, discover_publish_plugins=True):
|
||||
"""Reload plugins.
|
||||
|
|
@ -1772,7 +1847,7 @@ class CreateContext:
|
|||
|
||||
def _reset_creator_plugins(self):
|
||||
# Prepare settings
|
||||
project_settings = get_project_settings(self.project_name)
|
||||
project_settings = self.get_current_project_settings()
|
||||
|
||||
# Discover and prepare creators
|
||||
creators = {}
|
||||
|
|
|
|||
|
|
@ -336,17 +336,16 @@ def get_plugin_settings(plugin, project_settings, log, category=None):
|
|||
settings_category = getattr(plugin, "settings_category", None)
|
||||
if settings_category:
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[settings_category]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
)
|
||||
category_settings = project_settings[settings_category]
|
||||
except KeyError:
|
||||
log.warning((
|
||||
"Couldn't find plugin '{}' settings"
|
||||
" under settings category '{}'"
|
||||
).format(plugin.__name__, settings_category))
|
||||
"Couldn't find settings category '{}' in project settings"
|
||||
).format(settings_category))
|
||||
return {}
|
||||
|
||||
try:
|
||||
return category_settings["publish"][plugin.__name__]
|
||||
except KeyError:
|
||||
return {}
|
||||
|
||||
# Use project settings based on a category name
|
||||
|
|
|
|||
|
|
@ -313,7 +313,14 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
|
||||
# Define version
|
||||
version_number = None
|
||||
if self.follow_workfile_version:
|
||||
|
||||
# Allow an instance to force enable or disable the version
|
||||
# following of the current context
|
||||
use_context_version = self.follow_workfile_version
|
||||
if "followWorkfileVersion" in instance.data:
|
||||
use_context_version = instance.data["followWorkfileVersion"]
|
||||
|
||||
if use_context_version:
|
||||
version_number = context.data("version")
|
||||
|
||||
# Even if 'follow_workfile_version' is enabled, it may not be set
|
||||
|
|
@ -391,7 +398,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
anatomy_data.update(folder_data)
|
||||
return
|
||||
|
||||
if instance.data.get("newAssetPublishing"):
|
||||
if (
|
||||
instance.data.get("newHierarchyIntegration")
|
||||
# Backwards compatible (Deprecated since 24/06/06)
|
||||
or instance.data.get("newAssetPublishing")
|
||||
):
|
||||
hierarchy = instance.data["hierarchy"]
|
||||
anatomy_data["hierarchy"] = hierarchy
|
||||
|
||||
|
|
@ -409,7 +420,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
"path": instance.data["folderPath"],
|
||||
# TODO get folder type from hierarchy
|
||||
# Using 'Shot' is current default behavior of editorial
|
||||
# (or 'newAssetPublishing') publishing.
|
||||
# (or 'newHierarchyIntegration') publishing.
|
||||
"type": "Shot",
|
||||
},
|
||||
})
|
||||
|
|
@ -432,15 +443,22 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
if task_data:
|
||||
# Fill task data
|
||||
# - if we're in editorial, make sure the task type is filled
|
||||
if (
|
||||
not instance.data.get("newAssetPublishing")
|
||||
or task_data["type"]
|
||||
):
|
||||
new_hierarchy = (
|
||||
instance.data.get("newHierarchyIntegration")
|
||||
# Backwards compatible (Deprecated since 24/06/06)
|
||||
or instance.data.get("newAssetPublishing")
|
||||
)
|
||||
if not new_hierarchy or task_data["type"]:
|
||||
anatomy_data["task"] = task_data
|
||||
return
|
||||
|
||||
# New hierarchy is not created, so we can only skip rest of the logic
|
||||
if not instance.data.get("newAssetPublishing"):
|
||||
new_hierarchy = (
|
||||
instance.data.get("newHierarchyIntegration")
|
||||
# Backwards compatible (Deprecated since 24/06/06)
|
||||
or instance.data.get("newAssetPublishing")
|
||||
)
|
||||
if not new_hierarchy:
|
||||
return
|
||||
|
||||
# Try to find task data based on hierarchy context and folder path
|
||||
|
|
|
|||
|
|
@ -14,22 +14,20 @@ class CollectFarmTarget(pyblish.api.InstancePlugin):
|
|||
if not instance.data.get("farm"):
|
||||
return
|
||||
|
||||
context = instance.context
|
||||
addons_manager = instance.context.data.get("ayonAddonsManager")
|
||||
|
||||
farm_name = ""
|
||||
addons_manager = context.data.get("ayonAddonsManager")
|
||||
|
||||
for farm_renderer in ["deadline", "royalrender"]:
|
||||
addon = addons_manager.get(farm_renderer, False)
|
||||
|
||||
if not addon:
|
||||
self.log.error("Cannot find AYON addon '{0}'.".format(
|
||||
farm_renderer))
|
||||
elif addon.enabled:
|
||||
farm_renderer_addons = ["deadline", "royalrender"]
|
||||
for farm_renderer in farm_renderer_addons:
|
||||
addon = addons_manager.get(farm_renderer)
|
||||
if addon and addon.enabled:
|
||||
farm_name = farm_renderer
|
||||
|
||||
if farm_name:
|
||||
self.log.debug("Collected render target: {0}".format(farm_name))
|
||||
instance.data["toBeRenderedOn"] = farm_name
|
||||
break
|
||||
else:
|
||||
AssertionError("No AYON renderer addon found")
|
||||
# No enabled farm render addon found, then report all farm
|
||||
# addons that were searched for yet not found
|
||||
for farm_renderer in farm_renderer_addons:
|
||||
self.log.error(f"Cannot find AYON addon '{farm_renderer}'.")
|
||||
raise RuntimeError("No AYON renderer addon found.")
|
||||
|
||||
self.log.debug("Collected render target: {0}".format(farm_name))
|
||||
instance.data["toBeRenderedOn"] = farm_name
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
|
|||
"nuke",
|
||||
"photoshop",
|
||||
"resolve",
|
||||
"tvpaint"
|
||||
"tvpaint",
|
||||
"motionbuilder",
|
||||
"substancepainter"
|
||||
]
|
||||
|
||||
# in some cases of headless publishing (for example webpublisher using PS)
|
||||
|
|
|
|||
|
|
@ -202,43 +202,16 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
added_representations = True
|
||||
|
||||
if added_representations:
|
||||
self._mark_original_repre_for_deletion(repre, profile,
|
||||
added_review)
|
||||
self._mark_original_repre_for_deletion(
|
||||
repre, profile, added_review
|
||||
)
|
||||
|
||||
for repre in tuple(instance.data["representations"]):
|
||||
tags = repre.get("tags") or []
|
||||
if "delete" in tags and "thumbnail" not in tags:
|
||||
instance.data["representations"].remove(repre)
|
||||
|
||||
instance.data["representations"].extend(new_representations)
|
||||
|
||||
def _rename_in_representation(self, new_repre, files_to_convert,
|
||||
output_name, output_extension):
|
||||
"""Replace old extension with new one everywhere in representation.
|
||||
|
||||
Args:
|
||||
new_repre (dict)
|
||||
files_to_convert (list): of filenames from repre["files"],
|
||||
standardized to always list
|
||||
output_name (str): key of output definition from Settings,
|
||||
if "<passthrough>" token used, keep original repre name
|
||||
output_extension (str): extension from output definition
|
||||
"""
|
||||
if output_name != "passthrough":
|
||||
new_repre["name"] = output_name
|
||||
if not output_extension:
|
||||
return
|
||||
|
||||
new_repre["ext"] = output_extension
|
||||
|
||||
renamed_files = []
|
||||
for file_name in files_to_convert:
|
||||
file_name, _ = os.path.splitext(file_name)
|
||||
file_name = '{}.{}'.format(file_name,
|
||||
output_extension)
|
||||
renamed_files.append(file_name)
|
||||
new_repre["files"] = renamed_files
|
||||
|
||||
def _rename_in_representation(self, new_repre, files_to_convert,
|
||||
output_name, output_extension):
|
||||
"""Replace old extension with new one everywhere in representation.
|
||||
|
|
@ -364,7 +337,7 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
|
||||
if not repre.get("colorspaceData"):
|
||||
self.log.debug("Representation '{}' has no colorspace data. "
|
||||
"Skipped.")
|
||||
"Skipped.".format(repre["name"]))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -380,29 +380,28 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
data = {
|
||||
"families": get_instance_families(instance)
|
||||
}
|
||||
attribibutes = {}
|
||||
attributes = {}
|
||||
|
||||
product_group = instance.data.get("productGroup")
|
||||
if product_group:
|
||||
attribibutes["productGroup"] = product_group
|
||||
attributes["productGroup"] = product_group
|
||||
elif existing_product_entity:
|
||||
# Preserve previous product group if new version does not set it
|
||||
product_group = existing_product_entity.get("attrib", {}).get(
|
||||
"productGroup"
|
||||
)
|
||||
if product_group is not None:
|
||||
attribibutes["productGroup"] = product_group
|
||||
attributes["productGroup"] = product_group
|
||||
|
||||
product_id = None
|
||||
if existing_product_entity:
|
||||
product_id = existing_product_entity["id"]
|
||||
|
||||
product_entity = new_product_entity(
|
||||
product_name,
|
||||
product_type,
|
||||
folder_entity["id"],
|
||||
data=data,
|
||||
attribs=attribibutes,
|
||||
attribs=attributes,
|
||||
entity_id=product_id
|
||||
)
|
||||
|
||||
|
|
@ -464,6 +463,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
version_number,
|
||||
product_entity["id"],
|
||||
task_id=task_id,
|
||||
status=instance.data.get("status"),
|
||||
data=version_data,
|
||||
attribs=version_attributes,
|
||||
entity_id=version_id,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ class ValidateFolderEntities(pyblish.api.InstancePlugin):
|
|||
if instance.data.get("folderEntity"):
|
||||
self.log.debug("Instance has set fodler entity in its data.")
|
||||
|
||||
elif instance.data.get("newAssetPublishing"):
|
||||
elif (
|
||||
instance.data.get("newHierarchyIntegration")
|
||||
# Backwards compatible (Deprecated since 24/06/06)
|
||||
or instance.data.get("newAssetPublishing")
|
||||
):
|
||||
# skip if it is editorial
|
||||
self.log.debug("Editorial instance has no need to check...")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import pyblish.api
|
||||
from ayon_core.pipeline.publish import PublishValidationError
|
||||
|
||||
from ayon_core.lib import filter_profiles
|
||||
from ayon_core.pipeline.publish import (
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from ayon_core.pipeline import get_current_host_name
|
||||
|
||||
|
||||
class ValidateVersion(pyblish.api.InstancePlugin):
|
||||
class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
|
||||
"""Validate instance version.
|
||||
|
||||
AYON does not allow overwriting previously published versions.
|
||||
|
|
@ -11,13 +17,39 @@ class ValidateVersion(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.ValidatorOrder
|
||||
|
||||
label = "Validate Version"
|
||||
hosts = ["nuke", "maya", "houdini", "blender",
|
||||
"photoshop", "aftereffects"]
|
||||
|
||||
optional = False
|
||||
active = True
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, settings):
|
||||
# Disable if no profile is found for the current host
|
||||
profiles = (
|
||||
settings
|
||||
["core"]
|
||||
["publish"]
|
||||
["ValidateVersion"]
|
||||
["plugin_state_profiles"]
|
||||
)
|
||||
profile = filter_profiles(
|
||||
profiles, {"host_names": get_current_host_name()}
|
||||
)
|
||||
if not profile:
|
||||
cls.enabled = False
|
||||
return
|
||||
|
||||
# Apply settings from profile
|
||||
for attr_name in {
|
||||
"enabled",
|
||||
"optional",
|
||||
"active",
|
||||
}:
|
||||
setattr(cls, attr_name, profile[attr_name])
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
version = instance.data.get("version")
|
||||
latest_version = instance.data.get("latestVersion")
|
||||
|
||||
|
|
|
|||
BIN
client/ayon_core/resources/app_icons/motionbuilder.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
|
|
@ -1,131 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="2.93333in" height="3.06667in"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<g>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" id="SVGID_1_" x1="-0.0000027" x2="512" y1="256" y2="256">
|
||||
<stop offset="0" style="stop-color:#541f1b"/>
|
||||
<stop offset="1" style="stop-color:#a91b0d"/>
|
||||
</linearGradient>
|
||||
<circle cx="256" cy="256" fill="url(#SVGID_1_)" r="256"/>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" id="SVGID_2_" x1="42.6666641" x2="469.3333435" y1="256.0005188" y2="256.0005188">
|
||||
<stop offset="0" style="stop-color:#a91b0d"/>
|
||||
<stop offset="1" style="stop-color:#541f1b"/>
|
||||
</linearGradient>
|
||||
<path d="M256,469.3338623c-117.6314697,0-213.3333435-95.7023926-213.3333435-213.3333435 c0-117.6314545,95.7018661-213.333313,213.3333435-213.333313c117.6357422,0,213.3333435,95.7018661,213.3333435,213.333313 C469.3333435,373.6314697,373.6357422,469.3338623,256,469.3338623z" fill="url(#SVGID_2_)"/>
|
||||
</g>
|
||||
<g transform="
|
||||
translate(80, 80)
|
||||
scale(0.4)
|
||||
">
|
||||
<path id="glasses"
|
||||
fill="#000"
|
||||
d="M 314.00,503.21
|
||||
C 307.04,504.43 299.79,504.67 294.04,509.39
|
||||
281.95,519.33 287.74,545.64 293.31,558.00
|
||||
305.34,584.70 329.18,602.65 359.00,603.00
|
||||
359.00,603.00 367.00,603.00 367.00,603.00
|
||||
390.85,602.89 413.70,588.04 421.25,565.00
|
||||
424.01,556.59 424.10,550.65 424.00,542.00
|
||||
423.57,505.69 375.59,507.27 350.00,504.83
|
||||
350.00,504.83 335.00,503.91 335.00,503.91
|
||||
335.00,503.91 325.00,503.21 325.00,503.21
|
||||
325.00,503.21 314.00,503.21 314.00,503.21 Z
|
||||
M 549.00,503.42
|
||||
C 549.00,503.42 536.00,504.09 536.00,504.09
|
||||
536.00,504.09 492.00,508.80 492.00,508.80
|
||||
482.54,510.63 471.18,514.25 464.32,521.30
|
||||
457.58,528.23 455.90,537.72 456.00,547.00
|
||||
456.35,577.84 481.12,602.64 512.00,603.00
|
||||
540.73,603.33 565.64,594.85 581.39,569.00
|
||||
587.72,558.59 592.85,544.28 593.00,532.00
|
||||
593.07,525.52 593.79,518.45 589.58,513.02
|
||||
581.71,502.84 560.89,501.98 549.00,503.42 Z" />
|
||||
<path id="head"
|
||||
fill="#000"
|
||||
d="M 196.00,310.00
|
||||
C 157.00,317.34 100.69,333.54 68.00,355.67
|
||||
49.93,367.90 32.97,386.48 45.31,409.00
|
||||
56.44,429.32 84.25,442.43 105.00,450.99
|
||||
105.00,450.99 124.00,458.31 124.00,458.31
|
||||
126.46,459.18 131.76,460.54 133.18,462.51
|
||||
135.43,465.18 132.87,477.62 133.18,482.00
|
||||
133.72,499.63 138.37,519.19 146.27,535.00
|
||||
146.27,535.00 160.00,558.00 160.00,558.00
|
||||
151.04,562.00 138.14,570.76 130.00,576.58
|
||||
106.10,593.66 85.83,612.72 66.73,635.00
|
||||
66.73,635.00 50.58,655.00 50.58,655.00
|
||||
46.85,659.79 43.49,662.96 42.00,669.00
|
||||
42.00,669.00 80.00,697.58 80.00,697.58
|
||||
80.00,697.58 134.00,738.63 134.00,738.63
|
||||
134.00,738.63 159.00,757.63 159.00,757.63
|
||||
159.00,757.63 168.69,766.17 168.69,766.17
|
||||
168.69,766.17 166.41,788.00 166.41,788.00
|
||||
166.41,788.00 159.00,839.00 159.00,839.00
|
||||
159.00,839.00 725.00,839.00 725.00,839.00
|
||||
725.00,839.00 715.00,787.00 715.00,787.00
|
||||
714.23,783.16 710.80,769.90 711.69,767.01
|
||||
712.77,763.46 718.06,760.08 721.00,757.87
|
||||
721.00,757.87 746.00,738.87 746.00,738.87
|
||||
746.00,738.87 805.00,693.88 805.00,693.88
|
||||
805.00,693.88 839.00,668.00 839.00,668.00
|
||||
830.81,653.76 810.68,631.16 799.04,619.00
|
||||
779.93,599.05 746.32,568.97 721.00,558.00
|
||||
736.80,531.67 747.05,511.60 746.88,480.00
|
||||
746.99,476.23 745.11,464.71 746.88,462.51
|
||||
748.19,460.62 752.74,459.42 755.00,458.67
|
||||
755.00,458.67 773.00,451.99 773.00,451.99
|
||||
789.48,445.21 809.73,435.70 823.00,423.83
|
||||
833.14,414.76 839.34,405.89 838.99,392.00
|
||||
838.62,377.75 825.69,365.33 815.00,357.38
|
||||
791.37,339.79 750.60,326.38 722.00,318.42
|
||||
722.00,318.42 698.00,312.65 698.00,312.65
|
||||
694.98,311.97 689.62,311.22 687.31,309.28
|
||||
684.49,306.90 682.00,295.04 680.86,291.00
|
||||
680.86,291.00 667.37,242.00 667.37,242.00
|
||||
655.66,196.99 634.72,129.32 611.40,90.00
|
||||
599.32,69.64 582.92,49.09 559.00,42.75
|
||||
551.96,40.89 546.17,40.92 539.00,41.00
|
||||
521.02,41.21 499.67,47.67 482.00,51.58
|
||||
468.77,54.50 455.47,55.86 442.00,56.82
|
||||
442.00,56.82 432.00,56.04 432.00,56.04
|
||||
400.44,54.66 371.26,41.33 343.00,41.00
|
||||
335.69,40.92 330.19,40.64 323.00,42.48
|
||||
298.44,48.76 281.88,68.37 268.95,89.00
|
||||
244.31,128.34 223.03,195.41 211.63,241.00
|
||||
211.63,241.00 199.63,288.00 199.63,288.00
|
||||
198.01,294.48 194.47,303.56 196.00,310.00 Z
|
||||
M 687.00,478.00
|
||||
C 686.85,494.73 678.57,518.97 665.00,529.32
|
||||
658.84,534.01 657.01,532.67 651.00,535.35
|
||||
644.22,538.36 638.88,543.45 635.40,550.00
|
||||
635.40,550.00 622.40,582.00 622.40,582.00
|
||||
622.40,582.00 611.14,608.00 611.14,608.00
|
||||
592.73,649.33 562.84,703.15 531.00,735.00
|
||||
531.00,735.00 518.00,747.71 518.00,747.71
|
||||
499.32,763.96 471.29,778.70 446.00,779.00
|
||||
399.64,779.54 368.31,757.20 338.87,723.00
|
||||
298.68,676.31 271.17,614.00 248.94,557.00
|
||||
245.41,547.94 240.81,540.99 232.00,536.33
|
||||
224.56,532.38 222.45,534.62 215.00,528.53
|
||||
201.42,517.45 194.00,495.06 194.00,478.00
|
||||
194.00,478.00 226.00,483.92 226.00,483.92
|
||||
226.00,483.92 305.00,494.83 305.00,494.83
|
||||
305.00,494.83 350.00,498.09 350.00,498.09
|
||||
350.00,498.09 391.00,500.00 391.00,500.00
|
||||
391.00,500.00 408.00,501.00 408.00,501.00
|
||||
408.00,501.00 473.00,501.00 473.00,501.00
|
||||
473.00,501.00 485.00,500.04 485.00,500.04
|
||||
485.00,500.04 501.00,500.04 501.00,500.04
|
||||
501.00,500.04 520.00,499.00 520.00,499.00
|
||||
520.00,499.00 531.00,498.04 531.00,498.04
|
||||
559.97,496.78 589.26,493.87 618.00,489.73
|
||||
618.00,489.73 662.00,482.58 662.00,482.58
|
||||
662.00,482.58 687.00,478.00 687.00,478.00 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.8 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" ?><svg enable-background="new 0 0 512 512" id="Layer_1" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g><linearGradient gradientUnits="userSpaceOnUse" id="SVGID_1_" x1="-0.0000027" x2="512" y1="256" y2="256"><stop offset="0" style="stop-color:#00AEEE"/><stop offset="1" style="stop-color:#0095DA"/></linearGradient><circle cx="256" cy="256" fill="url(#SVGID_1_)" r="256"/><linearGradient gradientUnits="userSpaceOnUse" id="SVGID_2_" x1="42.6666641" x2="469.3333435" y1="256.0005188" y2="256.0005188"><stop offset="0" style="stop-color:#0095DA"/><stop offset="1" style="stop-color:#00AEEE"/></linearGradient><path d="M256,469.3338623c-117.6314697,0-213.3333435-95.7023926-213.3333435-213.3333435 c0-117.6314545,95.7018661-213.333313,213.3333435-213.333313c117.6357422,0,213.3333435,95.7018661,213.3333435,213.333313 C469.3333435,373.6314697,373.6357422,469.3338623,256,469.3338623z" fill="url(#SVGID_2_)"/></g><g><path d="M315.8906555,167.4933319h-28.3743896C287.5162659,154.4944,277.020813,144,264.0218811,144 c-13.0010834,0-23.4944153,10.4944-23.4944153,23.4933319h-28.3760071v40.6847992h103.7391968V167.4933319z" opacity="0.3"/><path d="M325.8906555,187.4895935v30.6885376H202.1504059v-30.6885376H164.354126V384h199.2911987V187.4895935 H325.8906555z M309.4405212,336.4693298l-7.0703735,7.0698853l-38.3712158-38.3712158l-38.3717346,38.3712158 l-7.0704041-7.0698853l38.3717346-38.3717346l-38.3717346-38.3717346l7.0704041-7.0698547l38.3717346,38.3711853 l38.3712158-38.3711853l7.0703735,7.0698547l-38.3717346,38.3717346L309.4405212,336.4693298z" opacity="0.3"/></g><g><path d="M307.8906555,159.4933319h-28.3743896C279.5162659,146.4944,269.020813,136,256.0218811,136 c-13.0010834,0-23.4944153,10.4944-23.4944153,23.4933319h-28.3760071v40.6847992h103.7391968V159.4933319z" fill="#FFFFFF"/><path d="M317.8906555,179.4895935v30.6885376H194.1504059v-30.6885376H156.354126V376h199.2911987V179.4895935 H317.8906555z M301.4405212,328.4693298l-7.0703735,7.0698853l-38.3712158-38.3712158l-38.3717346,38.3712158 l-7.0704041-7.0698853l38.3717346-38.3717346l-38.3717346-38.3717346l7.0704041-7.0698547l38.3717346,38.3711853 l38.3712158-38.3711853l7.0703735,7.0698547l-38.3717346,38.3717346L301.4405212,328.4693298z" fill="#FFFFFF"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
|
@ -1,32 +0,0 @@
|
|||
<html>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #333;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
margin-top: 200px;
|
||||
}
|
||||
h1 {
|
||||
font-family: "DejaVu Sans";
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
font-family: "DejaVu Sans";
|
||||
margin: 30px 10px;
|
||||
}
|
||||
em {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Sign in to Ftrack was successful</h1>
|
||||
<h3>
|
||||
You signed in with username <em>{}</em>.
|
||||
</h3>
|
||||
<h3>
|
||||
You can close this window now.
|
||||
</h3>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -14,9 +14,10 @@ from ayon_core.lib import (
|
|||
convert_ffprobe_fps_value,
|
||||
)
|
||||
|
||||
FFMPEG_EXE_COMMAND = subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))
|
||||
FFMPEG = (
|
||||
'{}%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s'
|
||||
).format(subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")))
|
||||
).format(FFMPEG_EXE_COMMAND)
|
||||
|
||||
DRAWTEXT = (
|
||||
"drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':"
|
||||
|
|
@ -482,10 +483,19 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
)
|
||||
print("Launching command: {}".format(command))
|
||||
|
||||
use_shell = True
|
||||
try:
|
||||
test_proc = subprocess.Popen(
|
||||
f"{FFMPEG_EXE_COMMAND} --help", shell=True
|
||||
)
|
||||
test_proc.wait()
|
||||
except BaseException:
|
||||
use_shell = False
|
||||
|
||||
kwargs = {
|
||||
"stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.PIPE,
|
||||
"shell": True,
|
||||
"shell": use_shell,
|
||||
}
|
||||
proc = subprocess.Popen(command, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
Adobe webserver
|
||||
---------------
|
||||
Aiohttp (Asyncio) based websocket server used for communication with host
|
||||
applications, currently only for Adobe (but could be used for any non python
|
||||
DCC which has websocket client).
|
||||
|
||||
This webserver is started in spawned Python process that opens DCC during
|
||||
its launch, waits for connection from DCC and handles communication going
|
||||
forward. Server is closed before Python process is killed.
|
||||
|
||||
(Different from `ayon_core/modules/webserver` as that one is running in Tray,
|
||||
this one is running in spawn Python process.)
|
||||
|
|
@ -14,6 +14,7 @@ from .hierarchy import (
|
|||
)
|
||||
from .thumbnails import ThumbnailsModel
|
||||
from .selection import HierarchyExpectedSelection
|
||||
from .users import UsersModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -32,4 +33,6 @@ __all__ = (
|
|||
"ThumbnailsModel",
|
||||
|
||||
"HierarchyExpectedSelection",
|
||||
|
||||
"UsersModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import ayon_api
|
|||
import six
|
||||
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.lib import CacheItem
|
||||
from ayon_core.lib import CacheItem, NestedCacheItem
|
||||
|
||||
PROJECTS_MODEL_SENDER = "projects.model"
|
||||
|
||||
|
|
@ -17,6 +17,49 @@ class AbstractHierarchyController:
|
|||
pass
|
||||
|
||||
|
||||
class StatusItem:
|
||||
"""Item representing status of project.
|
||||
|
||||
Args:
|
||||
name (str): Status name ("Not ready").
|
||||
color (str): Status color in hex ("#434a56").
|
||||
short (str): Short status name ("NRD").
|
||||
icon (str): Icon name in MaterialIcons ("fiber_new").
|
||||
state (Literal["not_started", "in_progress", "done", "blocked"]):
|
||||
Status state.
|
||||
|
||||
"""
|
||||
def __init__(self, name, color, short, icon, state):
|
||||
self.name = name
|
||||
self.color = color
|
||||
self.short = short
|
||||
self.icon = icon
|
||||
self.state = state
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"color": self.color,
|
||||
"short": self.short,
|
||||
"icon": self.icon,
|
||||
"state": self.state,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
|
||||
@classmethod
|
||||
def from_project_item(cls, status_data):
|
||||
return cls(
|
||||
name=status_data["name"],
|
||||
color=status_data["color"],
|
||||
short=status_data["shortName"],
|
||||
icon=status_data["icon"],
|
||||
state=status_data["state"],
|
||||
)
|
||||
|
||||
|
||||
class ProjectItem:
|
||||
"""Item representing folder entity on a server.
|
||||
|
||||
|
|
@ -40,6 +83,23 @@ class ProjectItem:
|
|||
}
|
||||
self.icon = icon
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, project_entity):
|
||||
"""Creates folder item from entity.
|
||||
|
||||
Args:
|
||||
project_entity (dict[str, Any]): Project entity.
|
||||
|
||||
Returns:
|
||||
ProjectItem: Project item.
|
||||
|
||||
"""
|
||||
return cls(
|
||||
project_entity["name"],
|
||||
project_entity["active"],
|
||||
project_entity["library"],
|
||||
)
|
||||
|
||||
def to_data(self):
|
||||
"""Converts folder item to data.
|
||||
|
||||
|
|
@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects):
|
|||
"""
|
||||
|
||||
return [
|
||||
ProjectItem(project["name"], project["active"], project["library"])
|
||||
ProjectItem.from_entity(project)
|
||||
for project in projects
|
||||
]
|
||||
|
||||
|
|
@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects):
|
|||
class ProjectsModel(object):
|
||||
def __init__(self, controller):
|
||||
self._projects_cache = CacheItem(default_factory=list)
|
||||
self._project_items_by_name = {}
|
||||
self._projects_by_name = {}
|
||||
self._project_statuses_cache = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
self._projects_by_name = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
|
||||
self._is_refreshing = False
|
||||
self._controller = controller
|
||||
|
||||
def reset(self):
|
||||
self._projects_cache.reset()
|
||||
self._project_items_by_name = {}
|
||||
self._projects_by_name = {}
|
||||
self._project_statuses_cache.reset()
|
||||
self._projects_by_name.reset()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh project items.
|
||||
|
||||
This method will requery list of ProjectItem returned by
|
||||
'get_project_items'.
|
||||
|
||||
To reset all cached items use 'reset' method.
|
||||
"""
|
||||
self._refresh_projects_cache()
|
||||
|
||||
def get_project_items(self, sender):
|
||||
|
|
@ -117,12 +188,51 @@ class ProjectsModel(object):
|
|||
return self._projects_cache.get_data()
|
||||
|
||||
def get_project_entity(self, project_name):
|
||||
if project_name not in self._projects_by_name:
|
||||
"""Get project entity.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Project entity or None if project
|
||||
was not found by name.
|
||||
|
||||
"""
|
||||
project_cache = self._projects_by_name[project_name]
|
||||
if not project_cache.is_valid:
|
||||
entity = None
|
||||
if project_name:
|
||||
entity = ayon_api.get_project(project_name)
|
||||
self._projects_by_name[project_name] = entity
|
||||
return self._projects_by_name[project_name]
|
||||
project_cache.update_data(entity)
|
||||
return project_cache.get_data()
|
||||
|
||||
def get_project_status_items(self, project_name, sender):
|
||||
"""Get project status items.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (Union[str, None]): Name of sender who asked for items.
|
||||
|
||||
Returns:
|
||||
list[StatusItem]: Status items for project.
|
||||
|
||||
"""
|
||||
statuses_cache = self._project_statuses_cache[project_name]
|
||||
if not statuses_cache.is_valid:
|
||||
with self._project_statuses_refresh_event_manager(
|
||||
sender, project_name
|
||||
):
|
||||
project_entity = None
|
||||
if project_name:
|
||||
project_entity = self.get_project_entity(project_name)
|
||||
statuses = []
|
||||
if project_entity:
|
||||
statuses = [
|
||||
StatusItem.from_project_item(status)
|
||||
for status in project_entity["statuses"]
|
||||
]
|
||||
statuses_cache.update_data(statuses)
|
||||
return statuses_cache.get_data()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _project_refresh_event_manager(self, sender):
|
||||
|
|
@ -143,6 +253,23 @@ class ProjectsModel(object):
|
|||
)
|
||||
self._is_refreshing = False
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _project_statuses_refresh_event_manager(self, sender, project_name):
|
||||
self._controller.emit_event(
|
||||
"projects.statuses.refresh.started",
|
||||
{"sender": sender, "project_name": project_name},
|
||||
PROJECTS_MODEL_SENDER
|
||||
)
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
self._controller.emit_event(
|
||||
"projects.statuses.refresh.finished",
|
||||
{"sender": sender, "project_name": project_name},
|
||||
PROJECTS_MODEL_SENDER
|
||||
)
|
||||
|
||||
def _refresh_projects_cache(self, sender=None):
|
||||
if self._is_refreshing:
|
||||
return None
|
||||
|
|
|
|||