From 0760c576a5305f43426713f799cbf2167ace8bf9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Oct 2022 15:34:18 +0100 Subject: [PATCH 01/55] Basic (non working) implementation --- openpype/hosts/unreal/__init__.py | 8 +- openpype/hosts/unreal/addon.py | 8 + openpype/hosts/unreal/api/launch_script.py | 81 ++ .../unreal/hooks/pre_workfile_preparation.py | 20 +- .../UE_5.0/Source/OpenPype/OpenPype.Build.cs | 1 + .../Source/OpenPype/Private/OpenPype.cpp | 64 ++ .../UE_5.0/Source/OpenPype/Public/OpenPype.h | 7 + openpype/hosts/unreal/lib.py | 4 +- openpype/hosts/unreal/remote/__init__.py | 1 + .../unreal/remote/communication_server.py | 978 +++++++++++++++++ .../hosts/unreal/remote/remote_execution.py | 639 +++++++++++ openpype/hosts/unreal/remote/rpc/__init__.py | 6 + .../hosts/unreal/remote/rpc/base_server.py | 245 +++++ openpype/hosts/unreal/remote/rpc/client.py | 103 ++ .../hosts/unreal/remote/rpc/exceptions.py | 79 ++ openpype/hosts/unreal/remote/rpc/factory.py | 319 ++++++ .../hosts/unreal/remote/rpc/unreal_server.py | 35 + .../hosts/unreal/remote/rpc/validations.py | 105 ++ openpype/hosts/unreal/remote/unreal.py | 992 ++++++++++++++++++ 19 files changed, 3690 insertions(+), 5 deletions(-) create mode 100644 openpype/hosts/unreal/api/launch_script.py create mode 100644 openpype/hosts/unreal/remote/__init__.py create mode 100644 openpype/hosts/unreal/remote/communication_server.py create mode 100644 openpype/hosts/unreal/remote/remote_execution.py create mode 100644 openpype/hosts/unreal/remote/rpc/__init__.py create mode 100644 openpype/hosts/unreal/remote/rpc/base_server.py create mode 100644 openpype/hosts/unreal/remote/rpc/client.py create mode 100644 openpype/hosts/unreal/remote/rpc/exceptions.py create mode 100644 openpype/hosts/unreal/remote/rpc/factory.py create mode 100644 openpype/hosts/unreal/remote/rpc/unreal_server.py create mode 100644 openpype/hosts/unreal/remote/rpc/validations.py create mode 100644 openpype/hosts/unreal/remote/unreal.py diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index 42dd8f0ac47..1ca46cf3f4a 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -1,6 +1,12 @@ -from .addon import UnrealAddon +from .addon import ( + get_launch_script_path, + UnrealAddon, + UNREAL_ROOT_DIR, +) __all__ = ( + "get_launch_script_path", "UnrealAddon", + "UNREAL_ROOT_DIR" ) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 16736214c52..134b5f427ba 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -5,6 +5,14 @@ UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +def get_launch_script_path(): + return os.path.join( + UNREAL_ROOT_DIR, + "api", + "launch_script.py" + ) + + class UnrealAddon(OpenPypeModule, IHostAddon): name = "unreal" host_name = "unreal" diff --git a/openpype/hosts/unreal/api/launch_script.py b/openpype/hosts/unreal/api/launch_script.py new file mode 100644 index 00000000000..8d3467a2c93 --- /dev/null +++ b/openpype/hosts/unreal/api/launch_script.py @@ -0,0 +1,81 @@ +import os +import sys +import signal +import traceback +import subprocess +import ctypes +import platform +import logging + +logging.basicConfig(level=logging.DEBUG) + +from Qt import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.hosts.unreal.remote.communication_server import ( + CommunicationWrapper +) + + +def safe_excepthook(*args): + traceback.print_exception(*args) + +def main(launch_args): + + # Be sure server won't crash at any moment but just print traceback + sys.excepthook = safe_excepthook + + # Create QtApplication for tools + # - QApplicaiton is also main thread/event loop of the server + qt_app = QtWidgets.QApplication([]) + + # Create Communicator object and trigger launch + # - this must be done before anything is processed + communicator = CommunicationWrapper.create_qt_communicator(qt_app) + communicator.launch(launch_args) + + # subprocess.Popen(launch_args) + + def process_in_main_thread(): + """Execution of `MainThreadItem`.""" + item = communicator.main_thread_listen() + if item: + item.execute() + + timer = QtCore.QTimer() + timer.setInterval(100) + timer.timeout.connect(process_in_main_thread) + timer.start() + + # Register terminal signal handler + def signal_handler(*_args): + print("You pressed Ctrl+C. Process ended.") + communicator.stop() + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + qt_app.setQuitOnLastWindowClosed(False) + qt_app.setStyleSheet(style.load_stylesheet()) + + # Load avalon icon + icon_path = style.app_icon_path() + if icon_path: + icon = QtGui.QIcon(icon_path) + qt_app.setWindowIcon(icon) + + # Set application name to be able show application icon in task bar + if platform.system().lower() == "windows": + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + u"WebsocketServer" + ) + + # Run Qt application event processing + sys.exit(qt_app.exec_()) + +if __name__ == "__main__": + args = list(sys.argv) + if os.path.abspath(__file__) == os.path.normpath(args[0]): + # Pop path to script + args.pop(0) + main(args) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 50b34bd573d..740c0885092 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -8,7 +8,8 @@ PreLaunchHook, ApplicationLaunchFailed, ApplicationNotFound, - get_workfile_template_key + get_workfile_template_key, + get_openpype_execute_args ) import openpype.hosts.unreal.lib as unreal_lib @@ -122,7 +123,7 @@ def execute(self): ue_path = unreal_lib.get_editor_executable_path( Path(detected[engine_version]), engine_version) - self.launch_context.launch_args = [ue_path.as_posix()] + # self.launch_context.launch_args = [ue_path.as_posix()] project_path.mkdir(parents=True, exist_ok=True) project_file = project_path / unreal_project_filename @@ -150,6 +151,21 @@ def execute(self): engine_path=Path(engine_path) ) + # Pop unreal executable + executable_path = ue_path.as_posix() + + new_launch_args = get_openpype_execute_args( + "run", self.launch_script_path(), executable_path, + ) + + # Append as whole list as these areguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + # Append project file to launch arguments self.launch_context.launch_args.append( f"\"{project_file.as_posix()}\"") + + def launch_script_path(self): + from openpype.hosts.unreal import get_launch_script_path + + return get_launch_script_path() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs index fcfd268234f..88eaa74bb4f 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs @@ -26,6 +26,7 @@ public OpenPype(ReadOnlyTargetRules Target) : base(Target) new string[] { "Core", + "WebSockets", // ... add other public dependencies that you statically link with here ... } ); diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp index b3bd9a81b31..2654974a335 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp @@ -5,6 +5,8 @@ #include "LevelEditor.h" #include "Misc/MessageDialog.h" #include "ToolMenus.h" +#include "GenericPlatform/GenericPlatformMisc.h" +#include "WebSocketsModule.h" // Module definition static const FName OpenPypeTabName("OpenPype"); @@ -14,10 +16,20 @@ static const FName OpenPypeTabName("OpenPype"); // This function is triggered when the plugin is staring up void FOpenPypeModule::StartupModule() { + if(!FModuleManager::Get().IsModuleLoaded("WebSockets")) + { + FModuleManager::Get().LoadModule("WebSockets"); + } + FOpenPypeStyle::Initialize(); FOpenPypeStyle::ReloadTextures(); FOpenPypeCommands::Register(); + UE_LOG(LogTemp, Warning, TEXT("OpenPype Plugin Started")); + + CreateSocket(); + ConnectToSocket(); + PluginCommands = MakeShareable(new FUICommandList); PluginCommands->MapAction( @@ -82,4 +94,56 @@ void FOpenPypeModule::MenuDialog() { bridge->RunInPython_Dialog(); } +void FOpenPypeModule::CreateSocket() { + UE_LOG(LogTemp, Warning, TEXT("Starting web socket...")); + + FString url = FWindowsPlatformMisc::GetEnvironmentVariable(*FString("WEBSOCKET_URL")); + + UE_LOG(LogTemp, Warning, TEXT("Websocket URL: %s"), *url); + + const FString ServerURL = url; // Your server URL. You can use ws, wss or wss+insecure. + const FString ServerProtocol = TEXT("ws"); // The WebServer protocol you want to use. + + TMap UpgradeHeaders; + UpgradeHeaders.Add(TEXT("upgrade"), TEXT("websocket")); + + Socket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol, UpgradeHeaders); +} + +void FOpenPypeModule::ConnectToSocket() { + // We bind all available events + Socket->OnConnected().AddLambda([]() -> void { + // This code will run once connected. + UE_LOG(LogTemp, Warning, TEXT("Connected")); + }); + + Socket->OnConnectionError().AddLambda([](const FString & Error) -> void { + // This code will run if the connection failed. Check Error to see what happened. + UE_LOG(LogTemp, Warning, TEXT("Error during connection")); + UE_LOG(LogTemp, Warning, TEXT("%s"), *Error); + }); + + Socket->OnClosed().AddLambda([](int32 StatusCode, const FString& Reason, bool bWasClean) -> void { + // This code will run when the connection to the server has been terminated. + // Because of an error or a call to Socket->Close(). + }); + + Socket->OnMessage().AddLambda([](const FString & Message) -> void { + // This code will run when we receive a string message from the server. + }); + + Socket->OnRawMessage().AddLambda([](const void* Data, SIZE_T Size, SIZE_T BytesRemaining) -> void { + // This code will run when we receive a raw (binary) message from the server. + }); + + Socket->OnMessageSent().AddLambda([](const FString& MessageString) -> void { + // This code is called after we sent a message to the server. + }); + + UE_LOG(LogTemp, Warning, TEXT("Connecting web socket to server...")); + + // And we finally connect to the server. + Socket->Connect(); +} + IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h index 3ee5eaa65f3..aa4cb468ca6 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h @@ -5,6 +5,8 @@ #include "CoreMinimal.h" #include "Modules/ModuleManager.h" +#include "IWebSocket.h" // Socket definition + class FOpenPypeModule : public IModuleInterface { @@ -18,6 +20,11 @@ class FOpenPypeModule : public IModuleInterface void MenuPopup(); void MenuDialog(); + void CreateSocket(); + void ConnectToSocket(); + private: TSharedPtr PluginCommands; + + TSharedPtr Socket; }; diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index d02c6de357f..87a4ac720cd 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -301,8 +301,8 @@ def create_unreal_project(project_name: str, 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"]) + # subprocess.check_call( + # [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path, ue_version) diff --git a/openpype/hosts/unreal/remote/__init__.py b/openpype/hosts/unreal/remote/__init__.py new file mode 100644 index 00000000000..99539baaa49 --- /dev/null +++ b/openpype/hosts/unreal/remote/__init__.py @@ -0,0 +1 @@ +# Copyright Epic Games, Inc. All Rights Reserved. diff --git a/openpype/hosts/unreal/remote/communication_server.py b/openpype/hosts/unreal/remote/communication_server.py new file mode 100644 index 00000000000..203a16ffa6c --- /dev/null +++ b/openpype/hosts/unreal/remote/communication_server.py @@ -0,0 +1,978 @@ +import os +import json +import time +import subprocess +import collections +import asyncio +import logging +import socket +import platform +import filecmp +import tempfile +import threading +import shutil +from queue import Queue +from contextlib import closing +import aiohttp + +from aiohttp import web +from aiohttp_json_rpc import JsonRpc +from aiohttp_json_rpc.protocol import ( + encode_request, encode_error, decode_msg, JsonRpcMsgTyp +) +from aiohttp_json_rpc.exceptions import RpcError + +from openpype.lib import emit_event +# from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +class CommunicationWrapper: + # TODO add logs and exceptions + communicator = None + + log = logging.getLogger("CommunicationWrapper") + + @classmethod + def create_qt_communicator(cls, *args, **kwargs): + """Create communicator for Artist usage.""" + communicator = QtCommunicator(*args, **kwargs) + cls.set_communicator(communicator) + return communicator + + @classmethod + def set_communicator(cls, communicator): + if not cls.communicator: + cls.communicator = communicator + else: + cls.log.warning("Communicator was set multiple times.") + + @classmethod + def client(cls): + if not cls.communicator: + return None + return cls.communicator.client() + + @classmethod + def execute_george(cls, george_script): + """Execute passed goerge script in TVPaint.""" + if not cls.communicator: + return + return cls.communicator.execute_george(george_script) + + +class WebSocketServer: + def __init__(self): + self.client = None + + self.loop = asyncio.new_event_loop() + self.app = web.Application(loop=self.loop) + self.port = self.find_free_port() + self.websocket_thread = WebsocketServerThread( + self, self.port, loop=self.loop + ) + + @property + def server_is_running(self): + return self.websocket_thread.server_is_running + + def add_route(self, *args, **kwargs): + self.app.router.add_route(*args, **kwargs) + + @staticmethod + def find_free_port(): + with closing( + socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ) as sock: + sock.bind(("", 0)) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + port = sock.getsockname()[1] + return port + + def start(self): + self.websocket_thread.start() + + def stop(self): + try: + if self.websocket_thread.is_running: + log.debug("Stopping websocket server") + self.websocket_thread.is_running = False + self.websocket_thread.stop() + except Exception: + log.warning( + "Error has happened during Killing websocket server", + exc_info=True + ) + + +class WebsocketServerThread(threading.Thread): + """ Listener for websocket rpc requests. + + It would be probably better to "attach" this to main thread (as for + example Harmony needs to run something on main thread), but currently + it creates separate thread and separate asyncio event loop + """ + def __init__(self, module, port, loop): + super(WebsocketServerThread, self).__init__() + self.is_running = False + self.server_is_running = False + self.port = port + self.module = module + self.loop = loop + self.runner = None + self.site = None + self.tasks = [] + + def run(self): + self.is_running = True + + try: + log.debug("Starting websocket server") + + self.loop.run_until_complete(self.start_server()) + + log.info( + "Running Websocket server on URL:" + " \"ws://localhost:{}\"".format(self.port) + ) + + asyncio.ensure_future(self.check_shutdown(), loop=self.loop) + + self.server_is_running = True + self.loop.run_forever() + + except Exception: + log.warning( + "Websocket Server service has failed", exc_info=True + ) + finally: + self.server_is_running = False + # optional + self.loop.close() + + self.is_running = False + log.info("Websocket server stopped") + + async def start_server(self): + """ Starts runner and TCPsite """ + self.runner = web.AppRunner(self.module.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, "localhost", self.port) + await self.site.start() + + def stop(self): + """Sets is_running flag to false, 'check_shutdown' shuts server down""" + self.is_running = False + + async def check_shutdown(self): + """ Future that is running and checks if server should be running + periodically. + """ + while self.is_running: + while self.tasks: + task = self.tasks.pop(0) + log.debug("waiting for task {}".format(task)) + await task + log.debug("returned value {}".format(task.result)) + + await asyncio.sleep(0.5) + + log.debug("## Server shutdown started") + + await self.site.stop() + log.debug("# Site stopped") + await self.runner.cleanup() + log.debug("# Server runner stopped") + tasks = [ + task for task in asyncio.all_tasks() + if task is not asyncio.current_task() + ] + list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks + results = await asyncio.gather(*tasks, return_exceptions=True) + log.debug(f"Finished awaiting cancelled tasks, results: {results}...") + await self.loop.shutdown_asyncgens() + # to really make sure everything else has time to stop + await asyncio.sleep(0.07) + self.loop.stop() + + +class BaseTVPaintRpc(JsonRpc): + def __init__(self, communication_obj, route_name="", **kwargs): + super().__init__(**kwargs) + self.requests_ids = collections.defaultdict(lambda: 0) + self.waiting_requests = collections.defaultdict(list) + self.responses = collections.defaultdict(list) + + self.route_name = route_name + self.communication_obj = communication_obj + + # async def _handle_rpc_msg(self, http_request, raw_msg): + # # This is duplicated code from super but there is no way how to do it + # # to be able handle server->client requests + # host = http_request.host + # if host in self.waiting_requests: + # try: + # _raw_message = raw_msg.data + # msg = decode_msg(_raw_message) + + # except RpcError as error: + # await self._ws_send_str(http_request, encode_error(error)) + # return + + # if msg.type in (JsonRpcMsgTyp.RESULT, JsonRpcMsgTyp.ERROR): + # msg_data = json.loads(_raw_message) + # if msg_data.get("id") in self.waiting_requests[host]: + # self.responses[host].append(msg_data) + # return + + # return await super()._handle_rpc_msg(http_request, raw_msg) + + def client_connected(self): + # TODO This is poor check. Add check it is client from TVPaint + if self.clients: + return True + return False + + def send_notification(self, client, method, params=None): + if params is None: + params = [] + asyncio.run_coroutine_threadsafe( + client.ws.send_str(encode_request(method, params=params)), + loop=self.loop + ) + + def send_request(self, client, method, params=None, timeout=0): + if params is None: + params = [] + + client_host = client.host + + request_id = self.requests_ids[client_host] + self.requests_ids[client_host] += 1 + + self.waiting_requests[client_host].append(request_id) + + log.debug("Sending request to client {} ({}, {}) id: {}".format( + client_host, method, params, request_id + )) + future = asyncio.run_coroutine_threadsafe( + client.ws.send_str(encode_request(method, request_id, params)), + loop=self.loop + ) + result = future.result() + + not_found = object() + response = not_found + start = time.time() + while True: + if client.ws.closed: + return None + + for _response in self.responses[client_host]: + _id = _response.get("id") + if _id == request_id: + response = _response + break + + if response is not not_found: + break + + if timeout > 0 and (time.time() - start) > timeout: + raise Exception("Timeout passed") + return + + time.sleep(0.1) + + if response is not_found: + raise Exception("Connection closed") + + self.responses[client_host].remove(response) + + error = response.get("error") + result = response.get("result") + if error: + raise Exception("Error happened: {}".format(error)) + return result + + +class QtTVPaintRpc(BaseTVPaintRpc): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + from openpype.tools.utils import host_tools + self.tools_helper = host_tools.HostToolsHelper() + + route_name = self.route_name + + # Register methods + self.add_methods( + (route_name, self.workfiles_tool), + (route_name, self.loader_tool), + (route_name, self.creator_tool), + (route_name, self.subset_manager_tool), + (route_name, self.publish_tool), + (route_name, self.scene_inventory_tool), + (route_name, self.library_loader_tool), + (route_name, self.experimental_tools) + ) + + # Panel routes for tools + async def workfiles_tool(self): + log.info("Triggering Workfile tool") + item = MainThreadItem(self.tools_helper.show_workfiles) + self._execute_in_main_thread(item) + return + + async def loader_tool(self): + log.info("Triggering Loader tool") + item = MainThreadItem(self.tools_helper.show_loader) + self._execute_in_main_thread(item) + return + + async def creator_tool(self): + log.info("Triggering Creator tool") + item = MainThreadItem(self.tools_helper.show_creator) + await self._async_execute_in_main_thread(item, wait=False) + + async def subset_manager_tool(self): + log.info("Triggering Subset Manager tool") + item = MainThreadItem(self.tools_helper.show_subset_manager) + # Do not wait for result of callback + self._execute_in_main_thread(item, wait=False) + return + + async def publish_tool(self): + log.info("Triggering Publish tool") + item = MainThreadItem(self.tools_helper.show_publish) + self._execute_in_main_thread(item) + return + + async def scene_inventory_tool(self): + """Open Scene Inventory tool. + + Function can't confirm if tool was opened becauise one part of + SceneInventory initialization is calling websocket request to host but + host can't response because is waiting for response from this call. + """ + log.info("Triggering Scene inventory tool") + item = MainThreadItem(self.tools_helper.show_scene_inventory) + # Do not wait for result of callback + self._execute_in_main_thread(item, wait=False) + return + + async def library_loader_tool(self): + log.info("Triggering Library loader tool") + item = MainThreadItem(self.tools_helper.show_library_loader) + self._execute_in_main_thread(item) + return + + async def experimental_tools(self): + log.info("Triggering Library loader tool") + item = MainThreadItem(self.tools_helper.show_experimental_tools_dialog) + self._execute_in_main_thread(item) + return + + async def _async_execute_in_main_thread(self, item, **kwargs): + await self.communication_obj.async_execute_in_main_thread( + item, **kwargs + ) + + def _execute_in_main_thread(self, item, **kwargs): + return self.communication_obj.execute_in_main_thread(item, **kwargs) + + +class MainThreadItem: + """Structure to store information about callback in main thread. + + Item should be used to execute callback in main thread which may be needed + for execution of Qt objects. + + Item store callback (callable variable), arguments and keyword arguments + for the callback. Item hold information about it's process. + """ + not_set = object() + sleep_time = 0.1 + + def __init__(self, callback, *args, **kwargs): + self.done = False + self.exception = self.not_set + self.result = self.not_set + self.callback = callback + self.args = args + self.kwargs = kwargs + + def execute(self): + """Execute callback and store it's result. + + Method must be called from main thread. Item is marked as `done` + when callback execution finished. Store output of callback of exception + information when callback raise one. + """ + log.debug("Executing process in main thread") + if self.done: + log.warning("- item is already processed") + return + + callback = self.callback + args = self.args + kwargs = self.kwargs + log.info("Running callback: {}".format(str(callback))) + try: + result = callback(*args, **kwargs) + self.result = result + + except Exception as exc: + self.exception = exc + + finally: + self.done = True + + def wait(self): + """Wait for result from main thread. + + This method stops current thread until callback is executed. + + Returns: + object: Output of callback. May be any type or object. + + Raises: + Exception: Reraise any exception that happened during callback + execution. + """ + while not self.done: + time.sleep(self.sleep_time) + + if self.exception is self.not_set: + return self.result + raise self.exception + + async def async_wait(self): + """Wait for result from main thread. + + Returns: + object: Output of callback. May be any type or object. + + Raises: + Exception: Reraise any exception that happened during callback + execution. + """ + while not self.done: + await asyncio.sleep(self.sleep_time) + + if self.exception is self.not_set: + return self.result + raise self.exception + + +class BaseCommunicator: + def __init__(self): + self.process = None + self.websocket_server = None + self.websocket_rpc = None + self.exit_code = None + self._connected_client = None + + @property + def server_is_running(self): + if self.websocket_server is None: + return False + return self.websocket_server.server_is_running + + def _windows_file_process(self, src_dst_mapping, to_remove): + """Windows specific file processing asking for admin permissions. + + It is required to have administration permissions to modify plugin + files in TVPaint installation folder. + + Method requires `pywin32` python module. + + Args: + src_dst_mapping (list, tuple, set): Mapping of source file to + destination. Both must be full path. Each item must be iterable + of size 2 `(C:/src/file.dll, C:/dst/file.dll)`. + to_remove (list): Fullpath to files that should be removed. + """ + + import pythoncom + from win32comext.shell import shell + + # Create temp folder where plugin files are temporary copied + # - reason is that copy to TVPaint requires administartion permissions + # but admin may not have access to source folder + tmp_dir = os.path.normpath( + tempfile.mkdtemp(prefix="tvpaint_copy_") + ) + + # Copy source to temp folder and create new mapping + dst_folders = collections.defaultdict(list) + new_src_dst_mapping = [] + for old_src, dst in src_dst_mapping: + new_src = os.path.join(tmp_dir, os.path.split(old_src)[1]) + shutil.copy(old_src, new_src) + new_src_dst_mapping.append((new_src, dst)) + + for src, dst in new_src_dst_mapping: + src = os.path.normpath(src) + dst = os.path.normpath(dst) + dst_filename = os.path.basename(dst) + dst_folder_path = os.path.dirname(dst) + dst_folders[dst_folder_path].append((dst_filename, src)) + + # create an instance of IFileOperation + fo = pythoncom.CoCreateInstance( + shell.CLSID_FileOperation, + None, + pythoncom.CLSCTX_ALL, + shell.IID_IFileOperation + ) + # Add delete command to file operation object + for filepath in to_remove: + item = shell.SHCreateItemFromParsingName( + filepath, None, shell.IID_IShellItem + ) + fo.DeleteItem(item) + + # here you can use SetOperationFlags, progress Sinks, etc. + for folder_path, items in dst_folders.items(): + # create an instance of IShellItem for the target folder + folder_item = shell.SHCreateItemFromParsingName( + folder_path, None, shell.IID_IShellItem + ) + for _dst_filename, source_file_path in items: + # create an instance of IShellItem for the source item + copy_item = shell.SHCreateItemFromParsingName( + source_file_path, None, shell.IID_IShellItem + ) + # queue the copy operation + fo.CopyItem(copy_item, folder_item, _dst_filename, None) + + # commit + fo.PerformOperations() + + # Remove temp folder + shutil.rmtree(tmp_dir) + + # def _prepare_windows_plugin(self, launch_args): + # """Copy plugin to TVPaint plugins and set PATH to dependencies. + + # Check if plugin in TVPaint's plugins exist and match to plugin + # version to current implementation version. Based on 64-bit or 32-bit + # version of the plugin. Path to libraries required for plugin is added + # to PATH variable. + # """ + + # host_executable = launch_args[0] + # executable_file = os.path.basename(host_executable) + # if "64bit" in executable_file: + # subfolder = "windows_x64" + # elif "32bit" in executable_file: + # subfolder = "windows_x86" + # else: + # raise ValueError( + # "Can't determine if executable " + # "leads to 32-bit or 64-bit TVPaint!" + # ) + + # plugin_files_path = get_plugin_files_path() + # # Folder for right windows plugin files + # source_plugins_dir = os.path.join(plugin_files_path, subfolder) + + # # Path to libraries (.dll) required for plugin library + # # - additional libraries can be copied to TVPaint installation folder + # # (next to executable) or added to PATH environment variable + # additional_libs_folder = os.path.join( + # source_plugins_dir, + # "additional_libraries" + # ) + # additional_libs_folder = additional_libs_folder.replace("\\", "/") + # if ( + # os.path.exists(additional_libs_folder) + # and additional_libs_folder not in os.environ["PATH"] + # ): + # os.environ["PATH"] += (os.pathsep + additional_libs_folder) + + # # Path to TVPaint's plugins folder (where we want to add our plugin) + # host_plugins_path = os.path.join( + # os.path.dirname(host_executable), + # "plugins" + # ) + + # # Files that must be copied to TVPaint's plugin folder + # plugin_dir = os.path.join(source_plugins_dir, "plugin") + + # to_copy = [] + # to_remove = [] + # # Remove old plugin name + # deprecated_filepath = os.path.join( + # host_plugins_path, "AvalonPlugin.dll" + # ) + # if os.path.exists(deprecated_filepath): + # to_remove.append(deprecated_filepath) + + # for filename in os.listdir(plugin_dir): + # src_full_path = os.path.join(plugin_dir, filename) + # dst_full_path = os.path.join(host_plugins_path, filename) + # if dst_full_path in to_remove: + # to_remove.remove(dst_full_path) + + # if ( + # not os.path.exists(dst_full_path) + # or not filecmp.cmp(src_full_path, dst_full_path) + # ): + # to_copy.append((src_full_path, dst_full_path)) + + # # Skip copy if everything is done + # if not to_copy and not to_remove: + # return + + # # Try to copy + # try: + # self._windows_file_process(to_copy, to_remove) + # except Exception: + # log.error("Plugin copy failed", exc_info=True) + + # # Validate copy was done + # invalid_copy = [] + # for src, dst in to_copy: + # if not os.path.exists(dst) or not filecmp.cmp(src, dst): + # invalid_copy.append((src, dst)) + + # # Validate delete was dones + # invalid_remove = [] + # for filepath in to_remove: + # if os.path.exists(filepath): + # invalid_remove.append(filepath) + + # if not invalid_remove and not invalid_copy: + # return + + # msg_parts = [] + # if invalid_remove: + # msg_parts.append( + # "Failed to remove files: {}".format(", ".join(invalid_remove)) + # ) + + # if invalid_copy: + # _invalid = [ + # "\"{}\" -> \"{}\"".format(src, dst) + # for src, dst in invalid_copy + # ] + # msg_parts.append( + # "Failed to copy files: {}".format(", ".join(_invalid)) + # ) + # raise RuntimeError(" & ".join(msg_parts)) + + # def _launch_tv_paint(self, launch_args): + # flags = ( + # subprocess.DETACHED_PROCESS + # | subprocess.CREATE_NEW_PROCESS_GROUP + # ) + # env = os.environ.copy() + # # Remove QuickTime from PATH on windows + # # - quicktime overrides TVPaint's ffmpeg encode/decode which may + # # cause issues on loading + # if platform.system().lower() == "windows": + # new_path = [] + # for path in env["PATH"].split(os.pathsep): + # if path and "quicktime" not in path.lower(): + # new_path.append(path) + # env["PATH"] = os.pathsep.join(new_path) + + # kwargs = { + # "env": env, + # "creationflags": flags + # } + # self.process = subprocess.Popen(launch_args, **kwargs) + + def _launch_unreal(self, launch_args): + flags = ( + subprocess.DETACHED_PROCESS + | subprocess.CREATE_NEW_PROCESS_GROUP + ) + env = os.environ.copy() + + kwargs = { + "env": env, + "creationflags": flags + } + self.process = subprocess.Popen(launch_args, **kwargs) + + def _create_routes(self): + self.websocket_rpc = BaseTVPaintRpc( + self, loop=self.websocket_server.loop + ) + self.websocket_server.add_route( + "*", "/", self.websocket_handler + ) + + async def websocket_handler(self, request): + print('Websocket connection starting') + ws = aiohttp.web.WebSocketResponse() + await ws.prepare(request) + print('Websocket connection ready') + + async for msg in ws: + print(msg) + if msg.type == aiohttp.WSMsgType.TEXT: + print(msg.data) + if msg.data == 'close': + await ws.close() + else: + await ws.send_str(msg.data + '/answer') + + print('Websocket connection closed') + return ws + + def _start_webserver(self): + self.websocket_server.start() + # Make sure RPC is using same loop as websocket server + while not self.websocket_server.server_is_running: + time.sleep(0.1) + + def _stop_webserver(self): + self.websocket_server.stop() + + def _exit(self, exit_code=None): + self._stop_webserver() + if exit_code is not None: + self.exit_code = exit_code + + if self.exit_code is None: + self.exit_code = 0 + + def stop(self): + """Stop communication and currently running python process.""" + log.info("Stopping communication") + self._exit() + + def launch(self, launch_args): + """Prepare all required data and launch host. + + First is prepared websocket server as communication point for host, + when server is ready to use host is launched as subprocess. + """ + # if platform.system().lower() == "windows": + # self._prepare_windows_plugin(launch_args) + + # Launch TVPaint and the websocket server. + log.info("Launching Unreal") + self.websocket_server = WebSocketServer() + + self._create_routes() + + os.environ["WEBSOCKET_URL"] = "ws://localhost:{}".format( + self.websocket_server.port + ) + + log.info("Added request handler for url: {}".format( + os.environ["WEBSOCKET_URL"] + )) + + self._start_webserver() + + # Start TVPaint when server is running + # self._launch_tv_paint(launch_args) + self._launch_unreal(launch_args) + + log.info("Waiting for client connection") + while True: + if self.process.poll() is not None: + log.debug("Host process is not alive. Exiting") + self._exit(1) + return + + if self.websocket_rpc.client_connected(): + log.info("Client has connected") + break + time.sleep(0.5) + + self._on_client_connect() + + emit_event("application.launched") + + def _on_client_connect(self): + self._initial_textfile_write() + + def _initial_textfile_write(self): + """Show popup about Write to file at start of TVPaint.""" + tmp_file = tempfile.NamedTemporaryFile( + mode="w", prefix="a_tvp_", suffix=".txt", delete=False + ) + tmp_file.close() + tmp_filepath = tmp_file.name.replace("\\", "/") + george_script = ( + "tv_writetextfile \"strict\" \"append\" \"{}\" \"empty\"" + ).format(tmp_filepath) + + result = CommunicationWrapper.execute_george(george_script) + + # Remote the file + os.remove(tmp_filepath) + + if result is None: + log.warning( + "Host was probably closed before plugin was initialized." + ) + elif result.lower() == "forbidden": + log.warning("User didn't confirm saving files.") + + def _client(self): + if not self.websocket_rpc: + log.warning("Communicator's server did not start yet.") + return None + + for client in self.websocket_rpc.clients: + if not client.ws.closed: + return client + log.warning("Client is not yet connected to Communicator.") + return None + + def client(self): + if not self._connected_client or self._connected_client.ws.closed: + self._connected_client = self._client() + return self._connected_client + + def send_request(self, method, params=None): + client = self.client() + if not client: + return + + return self.websocket_rpc.send_request( + client, method, params + ) + + def send_notification(self, method, params=None): + client = self.client() + if not client: + return + + self.websocket_rpc.send_notification( + client, method, params + ) + + def execute_george(self, george_script): + """Execute passed goerge script in TVPaint.""" + return self.send_request( + "execute_george", [george_script] + ) + + def execute_george_through_file(self, george_script): + """Execute george script with temp file. + + Allows to execute multiline george script without stopping websocket + client. + + On windows make sure script does not contain paths with backwards + slashes in paths, TVPaint won't execute properly in that case. + + Args: + george_script (str): George script to execute. May be multilined. + """ + temporary_file = tempfile.NamedTemporaryFile( + mode="w", prefix="a_tvp_", suffix=".grg", delete=False + ) + temporary_file.write(george_script) + temporary_file.close() + temp_file_path = temporary_file.name.replace("\\", "/") + self.execute_george("tv_runscript {}".format(temp_file_path)) + os.remove(temp_file_path) + + +class QtCommunicator(BaseCommunicator): + menu_definitions = { + "title": "OpenPype Tools", + "menu_items": [ + { + "callback": "workfiles_tool", + "label": "Workfiles", + "help": "Open workfiles tool" + }, { + "callback": "loader_tool", + "label": "Load", + "help": "Open loader tool" + }, { + "callback": "creator_tool", + "label": "Create", + "help": "Open creator tool" + }, { + "callback": "scene_inventory_tool", + "label": "Scene inventory", + "help": "Open scene inventory tool" + }, { + "callback": "publish_tool", + "label": "Publish", + "help": "Open publisher" + }, { + "callback": "library_loader_tool", + "label": "Library", + "help": "Open library loader tool" + }, { + "callback": "subset_manager_tool", + "label": "Subset Manager", + "help": "Open subset manager tool" + }, { + "callback": "experimental_tools", + "label": "Experimental tools", + "help": "Open experimental tools dialog" + } + ] + } + + def __init__(self, qt_app): + super().__init__() + self.callback_queue = Queue() + self.qt_app = qt_app + + def _create_routes(self): + self.websocket_rpc = QtTVPaintRpc( + self, loop=self.websocket_server.loop + ) + self.websocket_server.add_route( + "*", "/", self.websocket_rpc.handle_request + ) + + def execute_in_main_thread(self, main_thread_item, wait=True): + """Add `MainThreadItem` to callback queue and wait for result.""" + self.callback_queue.put(main_thread_item) + if wait: + return main_thread_item.wait() + return + + async def async_execute_in_main_thread(self, main_thread_item, wait=True): + """Add `MainThreadItem` to callback queue and wait for result.""" + self.callback_queue.put(main_thread_item) + if wait: + return await main_thread_item.async_wait() + + def main_thread_listen(self): + """Get last `MainThreadItem` from queue. + + Must be called from main thread. + + Method checks if host process is still running as it may cause + issues if not. + """ + # check if host still running + if self.process.poll() is not None: + self._exit() + return None + + if self.callback_queue.empty(): + return None + return self.callback_queue.get() + + def _on_client_connect(self): + super()._on_client_connect() + self._build_menu() + + def _build_menu(self): + self.send_request( + "define_menu", [self.menu_definitions] + ) + + def _exit(self, *args, **kwargs): + super()._exit(*args, **kwargs) + emit_event("application.exit") + self.qt_app.exit(self.exit_code) diff --git a/openpype/hosts/unreal/remote/remote_execution.py b/openpype/hosts/unreal/remote/remote_execution.py new file mode 100644 index 00000000000..c2083ec6228 --- /dev/null +++ b/openpype/hosts/unreal/remote/remote_execution.py @@ -0,0 +1,639 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +import sys as _sys +import json as _json +import uuid as _uuid +import time as _time +import socket as _socket +import logging as _logging +import threading as _threading + +# Protocol constants (see PythonScriptRemoteExecution.cpp for the full protocol definition) +_PROTOCOL_VERSION = 1 # Protocol version number +_PROTOCOL_MAGIC = 'ue_py' # Protocol magic identifier +_TYPE_PING = 'ping' # Service discovery request (UDP) +_TYPE_PONG = 'pong' # Service discovery response (UDP) +_TYPE_OPEN_CONNECTION = 'open_connection' # Open a TCP command connection with the requested server (UDP) +_TYPE_CLOSE_CONNECTION = 'close_connection' # Close any active TCP command connection (UDP) +_TYPE_COMMAND = 'command' # Execute a remote Python command (TCP) +_TYPE_COMMAND_RESULT = 'command_result' # Result of executing a remote Python command (TCP) + +_NODE_PING_SECONDS = 1 # Number of seconds to wait before sending another "ping" message to discover remote notes +_NODE_TIMEOUT_SECONDS = 5 # Number of seconds to wait before timing out a remote node that was discovered via UDP and has stopped sending "pong" responses + +DEFAULT_MULTICAST_TTL = 0 # Multicast TTL (0 is limited to the local host, 1 is limited to the local subnet) +DEFAULT_MULTICAST_GROUP_ENDPOINT = ('239.0.0.1', 6766) # The multicast group endpoint tuple that the UDP multicast socket should join (must match the "Multicast Group Endpoint" setting in the Python plugin) +DEFAULT_MULTICAST_BIND_ADDRESS = '0.0.0.0' # The adapter address that the UDP multicast socket should bind to, or 0.0.0.0 to bind to all adapters (must match the "Multicast Bind Address" setting in the Python plugin) +DEFAULT_COMMAND_ENDPOINT = ('127.0.0.1', 6776) # The endpoint tuple for the TCP command connection hosted by this client (that the remote client will connect to) +DEFAULT_RECEIVE_BUFFER_SIZE = 8192 # The default receive buffer size + +# Execution modes (these must match the names given to LexToString for EPythonCommandExecutionMode in IPythonScriptPlugin.h) +MODE_EXEC_FILE = 'ExecuteFile' # Execute the Python command as a file. This allows you to execute either a literal Python script containing multiple statements, or a file with optional arguments +MODE_EXEC_STATEMENT = 'ExecuteStatement' # Execute the Python command as a single statement. This will execute a single statement and print the result. This mode cannot run files +MODE_EVAL_STATEMENT = 'EvaluateStatement' # Evaluate the Python command as a single statement. This will evaluate a single statement and return the result. This mode cannot run files + +class RemoteExecutionConfig(object): + ''' + Configuration data for establishing a remote connection with a UE4 instance running Python. + ''' + def __init__(self): + self.multicast_ttl = DEFAULT_MULTICAST_TTL + self.multicast_group_endpoint = DEFAULT_MULTICAST_GROUP_ENDPOINT + self.multicast_bind_address = DEFAULT_MULTICAST_BIND_ADDRESS + self.command_endpoint = DEFAULT_COMMAND_ENDPOINT + +class RemoteExecution(object): + ''' + A remote execution session. This class can discover remote "nodes" (UE4 instances running Python), and allow you to open a command channel to a particular instance. + + Args: + config (RemoteExecutionConfig): Configuration controlling the connection settings for this session. + ''' + def __init__(self, config=RemoteExecutionConfig()): + self._config = config + self._broadcast_connection = None + self._command_connection = None + self._node_id = str(_uuid.uuid4()) + + @property + def remote_nodes(self): + ''' + Get the current set of discovered remote "nodes" (UE4 instances running Python). + + Returns: + list: A list of dicts containg the node ID and the other data. + ''' + return self._broadcast_connection.remote_nodes if self._broadcast_connection else [] + + def start(self): + ''' + Start the remote execution session. This will begin the discovey process for remote "nodes" (UE4 instances running Python). + ''' + self._broadcast_connection = _RemoteExecutionBroadcastConnection(self._config, self._node_id) + self._broadcast_connection.open() + + def stop(self): + ''' + Stop the remote execution session. This will end the discovey process for remote "nodes" (UE4 instances running Python), and close any open command connection. + ''' + self.close_command_connection() + if self._broadcast_connection: + self._broadcast_connection.close() + self._broadcast_connection = None + + def has_command_connection(self): + ''' + Check whether the remote execution session has an active command connection. + + Returns: + bool: True if the remote execution session has an active command connection, False otherwise. + ''' + return self._command_connection is not None + + def open_command_connection(self, remote_node_id): + ''' + Open a command connection to the given remote "node" (a UE4 instance running Python), closing any command connection that may currently be open. + + Args: + remote_node_id (string): The ID of the remote node (this can be obtained by querying `remote_nodes`). + ''' + self._command_connection = _RemoteExecutionCommandConnection(self._config, self._node_id, remote_node_id) + self._command_connection.open(self._broadcast_connection) + + def close_command_connection(self): + ''' + Close any command connection that may currently be open. + ''' + if self._command_connection: + self._command_connection.close(self._broadcast_connection) + self._command_connection = None + + def run_command(self, command, unattended=True, exec_mode=MODE_EXEC_FILE, raise_on_failure=False): + ''' + Run a command remotely based on the current command connection. + + Args: + command (string): The Python command to run remotely. + unattended (bool): True to run this command in "unattended" mode (suppressing some UI). + exec_mode (string): The execution mode to use as a string value (must be one of MODE_EXEC_FILE, MODE_EXEC_STATEMENT, or MODE_EVAL_STATEMENT). + raise_on_failure (bool): True to raise a RuntimeError if the command fails on the remote target. + + Returns: + dict: The result from running the remote command (see `command_result` from the protocol definition). + ''' + data = self._command_connection.run_command(command, unattended, exec_mode) + if raise_on_failure and not data['success']: + raise RuntimeError('Remote Python Command failed! {0}'.format(data['result'])) + return data + +class _RemoteExecutionNode(object): + ''' + A discovered remote "node" (aka, a UE4 instance running Python). + + Args: + data (dict): The data representing this node (from its "pong" reponse). + now (float): The timestamp at which this node was last seen. + ''' + def __init__(self, data, now=None): + self.data = data + self._last_pong = _time_now(now) + + def should_timeout(self, now=None): + ''' + Check to see whether this remote node should be considered timed-out. + + Args: + now (float): The current timestamp. + + Returns: + bool: True of the node has exceeded the timeout limit (`_NODE_TIMEOUT_SECONDS`), False otherwise. + ''' + return (self._last_pong + _NODE_TIMEOUT_SECONDS) < _time_now(now) + +class _RemoteExecutionBroadcastNodes(object): + ''' + A thread-safe set of remote execution "nodes" (UE4 instances running Python). + ''' + def __init__(self): + self._remote_nodes = {} + self._remote_nodes_lock = _threading.RLock() + + @property + def remote_nodes(self): + ''' + Get the current set of discovered remote "nodes" (UE4 instances running Python). + + Returns: + list: A list of dicts containg the node ID and the other data. + ''' + with self._remote_nodes_lock: + remote_nodes_list = [] + for node_id, node in self._remote_nodes.items(): + remote_node_data = dict(node.data) + remote_node_data['node_id'] = node_id + remote_nodes_list.append(remote_node_data) + return remote_nodes_list + + def update_remote_node(self, node_id, node_data, now=None): + ''' + Update a remote node, replacing any existing data. + + Args: + node_id (str): The ID of the remote node (from its "pong" reponse). + node_data (dict): The data representing this node (from its "pong" reponse). + now (float): The timestamp at which this node was last seen. + ''' + now = _time_now(now) + with self._remote_nodes_lock: + if node_id not in self._remote_nodes: + _logger.debug('Found Node {0}: {1}'.format(node_id, node_data)) + self._remote_nodes[node_id] = _RemoteExecutionNode(node_data, now) + + def timeout_remote_nodes(self, now=None): + ''' + Check to see whether any remote nodes should be considered timed-out, and if so, remove them from this set. + + Args: + now (float): The current timestamp. + ''' + now = _time_now(now) + with self._remote_nodes_lock: + for node_id, node in list(self._remote_nodes.items()): + if node.should_timeout(now): + _logger.debug('Lost Node {0}: {1}'.format(node_id, node.data)) + del self._remote_nodes[node_id] + +class _RemoteExecutionBroadcastConnection(object): + ''' + A remote execution broadcast connection (for UDP based messaging and node discovery). + + Args: + config (RemoteExecutionConfig): Configuration controlling the connection settings. + node_id (string): The ID of the local "node" (this session). + ''' + def __init__(self, config, node_id): + self._config = config + self._node_id = node_id + self._nodes = None + self._running = False + self._broadcast_socket = None + self._broadcast_listen_thread = None + + @property + def remote_nodes(self): + ''' + Get the current set of discovered remote "nodes" (UE4 instances running Python). + + Returns: + list: A list of dicts containg the node ID and the other data. + ''' + return self._nodes.remote_nodes if self._nodes else [] + + def open(self): + ''' + Open the UDP based messaging and discovery connection. This will begin the discovey process for remote "nodes" (UE4 instances running Python). + ''' + self._running = True + self._last_ping = None + self._nodes = _RemoteExecutionBroadcastNodes() + self._init_broadcast_socket() + self._init_broadcast_listen_thread() + + def close(self): + ''' + Close the UDP based messaging and discovery connection. This will end the discovey process for remote "nodes" (UE4 instances running Python). + ''' + self._running = False + if self._broadcast_listen_thread: + self._broadcast_listen_thread.join() + if self._broadcast_socket: + self._broadcast_socket.close() + self._broadcast_socket = None + self._nodes = None + + def _init_broadcast_socket(self): + ''' + Initialize the UDP based broadcast socket based on the current configuration. + ''' + self._broadcast_socket = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM, _socket.IPPROTO_UDP) # UDP/IP socket + if hasattr(_socket, 'SO_REUSEPORT'): + self._broadcast_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEPORT, 1) + else: + self._broadcast_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) + self._broadcast_socket.bind((self._config.multicast_bind_address, self._config.multicast_group_endpoint[1])) + self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_MULTICAST_LOOP, 1) + self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_MULTICAST_TTL, self._config.multicast_ttl) + self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_MULTICAST_IF, _socket.inet_aton(self._config.multicast_bind_address)) + self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_ADD_MEMBERSHIP, _socket.inet_aton(self._config.multicast_group_endpoint[0]) + _socket.inet_aton(self._config.multicast_bind_address)) + self._broadcast_socket.settimeout(0.1) + + def _init_broadcast_listen_thread(self): + ''' + Initialize the listen thread for the UDP based broadcast socket to allow discovery to run async. + ''' + self._broadcast_listen_thread = _threading.Thread(target=self._run_broadcast_listen_thread) + self._broadcast_listen_thread.daemon = True + self._broadcast_listen_thread.start() + + def _run_broadcast_listen_thread(self): + ''' + Main loop for the listen thread that handles processing discovery messages. + ''' + while self._running: + # Receive and process all pending data + while True: + try: + data = self._broadcast_socket.recv(DEFAULT_RECEIVE_BUFFER_SIZE) + except _socket.timeout: + data = None + if data: + self._handle_data(data) + else: + break + # Run tick logic + now = _time_now() + self._broadcast_ping(now) + self._nodes.timeout_remote_nodes(now) + _time.sleep(0.1) + + def _broadcast_message(self, message): + ''' + Broadcast the given message over the UDP socket to anything that might be listening. + + Args: + message (_RemoteExecutionMessage): The message to broadcast. + ''' + self._broadcast_socket.sendto(message.to_json_bytes(), self._config.multicast_group_endpoint) + + def _broadcast_ping(self, now=None): + ''' + Broadcast a "ping" message over the UDP socket to anything that might be listening. + + Args: + now (float): The current timestamp. + ''' + now = _time_now(now) + if not self._last_ping or ((self._last_ping + _NODE_PING_SECONDS) < now): + self._last_ping = now + self._broadcast_message(_RemoteExecutionMessage(_TYPE_PING, self._node_id)) + + def broadcast_open_connection(self, remote_node_id): + ''' + Broadcast an "open_connection" message over the UDP socket to be handled by the specified remote node. + + Args: + remote_node_id (string): The ID of the remote node that we want to open a command connection with. + ''' + self._broadcast_message(_RemoteExecutionMessage(_TYPE_OPEN_CONNECTION, self._node_id, remote_node_id, { + 'command_ip': self._config.command_endpoint[0], + 'command_port': self._config.command_endpoint[1], + })) + + def broadcast_close_connection(self, remote_node_id): + ''' + Broadcast a "close_connection" message over the UDP socket to be handled by the specified remote node. + + Args: + remote_node_id (string): The ID of the remote node that we want to close a command connection with. + ''' + self._broadcast_message(_RemoteExecutionMessage(_TYPE_CLOSE_CONNECTION, self._node_id, remote_node_id)) + + def _handle_data(self, data): + ''' + Handle data received from the UDP broadcast socket. + + Args: + data (bytes): The raw bytes received from the socket. + ''' + message = _RemoteExecutionMessage(None, None) + if message.from_json_bytes(data): + self._handle_message(message) + + def _handle_message(self, message): + ''' + Handle a message received from the UDP broadcast socket. + + Args: + message (_RemoteExecutionMessage): The message received from the socket. + ''' + if not message.passes_receive_filter(self._node_id): + return + if message.type_ == _TYPE_PONG: + self._handle_pong_message(message) + return + _logger.debug('Unhandled remote execution message type "{0}"'.format(message.type_)) + + def _handle_pong_message(self, message): + ''' + Handle a "pong" message received from the UDP broadcast socket. + + Args: + message (_RemoteExecutionMessage): The message received from the socket. + ''' + self._nodes.update_remote_node(message.source, message.data) + +class _RemoteExecutionCommandConnection(object): + ''' + A remote execution command connection (for TCP based command processing). + + Args: + config (RemoteExecutionConfig): Configuration controlling the connection settings. + node_id (string): The ID of the local "node" (this session). + remote_node_id (string): The ID of the remote "node" (the UE4 instance running Python). + ''' + def __init__(self, config, node_id, remote_node_id): + self._config = config + self._node_id = node_id + self._remote_node_id = remote_node_id + self._command_listen_socket = None + self._command_channel_socket = _socket.socket() # This type is only here to appease PyLint + + def open(self, broadcast_connection): + ''' + Open the TCP based command connection, and wait to accept the connection from the remote party. + + Args: + broadcast_connection (_RemoteExecutionBroadcastConnection): The broadcast connection to send UDP based messages over. + ''' + self._nodes = _RemoteExecutionBroadcastNodes() + self._init_command_listen_socket() + self._try_accept(broadcast_connection) + + def close(self, broadcast_connection): + ''' + Close the TCP based command connection, attempting to notify the remote party. + + Args: + broadcast_connection (_RemoteExecutionBroadcastConnection): The broadcast connection to send UDP based messages over. + ''' + broadcast_connection.broadcast_close_connection(self._remote_node_id) + if self._command_channel_socket: + self._command_channel_socket.close() + self._command_channel_socket = None + if self._command_listen_socket: + self._command_listen_socket.close() + self._command_listen_socket = None + + def run_command(self, command, unattended, exec_mode): + ''' + Run a command on the remote party. + + Args: + command (string): The Python command to run remotely. + unattended (bool): True to run this command in "unattended" mode (suppressing some UI). + exec_mode (string): The execution mode to use as a string value (must be one of MODE_EXEC_FILE, MODE_EXEC_STATEMENT, or MODE_EVAL_STATEMENT). + + Returns: + dict: The result from running the remote command (see `command_result` from the protocol definition). + ''' + self._send_message(_RemoteExecutionMessage(_TYPE_COMMAND, self._node_id, self._remote_node_id, { + 'command': command, + 'unattended': unattended, + 'exec_mode': exec_mode, + })) + result = self._receive_message(_TYPE_COMMAND_RESULT) + return result.data + + def _send_message(self, message): + ''' + Send the given message over the TCP socket to the remote party. + + Args: + message (_RemoteExecutionMessage): The message to send. + ''' + self._command_channel_socket.sendall(message.to_json_bytes()) + + def _receive_message(self, expected_type): + ''' + Receive a message over the TCP socket from the remote party. + + Args: + expected_type (string): The type of message we expect to receive. + + Returns: + The message that was received. + ''' + data = self._command_channel_socket.recv(DEFAULT_RECEIVE_BUFFER_SIZE) + if data: + message = _RemoteExecutionMessage(None, None) + if message.from_json_bytes(data) and message.passes_receive_filter(self._node_id) and message.type_ == expected_type: + return message + raise RuntimeError('Remote party failed to send a valid response!') + + def _init_command_listen_socket(self): + ''' + Initialize the TCP based command socket based on the current configuration, and set it to listen for an incoming connection. + ''' + self._command_listen_socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM, _socket.IPPROTO_TCP) # TCP/IP socket + if hasattr(_socket, 'SO_REUSEPORT'): + self._command_listen_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEPORT, 1) + else: + self._command_listen_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) + self._command_listen_socket.bind(self._config.command_endpoint) + self._command_listen_socket.listen(1) + self._command_listen_socket.settimeout(5) + + def _try_accept(self, broadcast_connection): + ''' + Wait to accept a connection on the TCP based command connection. This makes 6 attempts to receive a connection, waiting for 5 seconds between each attempt (30 seconds total). + + Args: + broadcast_connection (_RemoteExecutionBroadcastConnection): The broadcast connection to send UDP based messages over. + ''' + for _n in range(6): + broadcast_connection.broadcast_open_connection(self._remote_node_id) + try: + self._command_channel_socket = self._command_listen_socket.accept()[0] + self._command_channel_socket.setblocking(True) + return + except _socket.timeout: + continue + raise RuntimeError('Remote party failed to attempt the command socket connection!') + +class _RemoteExecutionMessage(object): + ''' + A message sent or received by remote execution (on either the UDP or TCP connection), as UTF-8 encoded JSON. + + Args: + type_ (string): The type of this message (see the `_TYPE_` constants). + source (string): The ID of the node that sent this message. + dest (string): The ID of the destination node of this message, or None to send to all nodes (for UDP broadcast). + data (dict): The message specific payload data. + ''' + def __init__(self, type_, source, dest=None, data=None): + self.type_ = type_ + self.source = source + self.dest = dest + self.data = data + + def passes_receive_filter(self, node_id): + ''' + Test to see whether this message should be received by the current node (wasn't sent to itself, and has a compatible destination ID). + + Args: + node_id (string): The ID of the local "node" (this session). + + Returns: + bool: True if this message should be received by the current node, False otherwise. + ''' + return self.source != node_id and (not self.dest or self.dest == node_id) + + def to_json(self): + ''' + Convert this message to its JSON representation. + + Returns: + str: The JSON representation of this message. + ''' + if not self.type_: + raise ValueError('"type" cannot be empty!') + if not self.source: + raise ValueError('"source" cannot be empty!') + json_obj = { + 'version': _PROTOCOL_VERSION, + 'magic': _PROTOCOL_MAGIC, + 'type': self.type_, + 'source': self.source, + } + if self.dest: + json_obj['dest'] = self.dest + if self.data: + json_obj['data'] = self.data + return _json.dumps(json_obj, ensure_ascii=False) + + def to_json_bytes(self): + ''' + Convert this message to its JSON representation as UTF-8 bytes. + + Returns: + bytes: The JSON representation of this message as UTF-8 bytes. + ''' + json_str = self.to_json() + return json_str.encode('utf-8') + + def from_json(self, json_str): + ''' + Parse this message from its JSON representation. + + Args: + json_str (str): The JSON representation of this message. + + Returns: + bool: True if this message could be parsed, False otherwise. + ''' + try: + json_obj = _json.loads(json_str) + # Read and validate required protocol version information + if json_obj['version'] != _PROTOCOL_VERSION: + raise ValueError('"version" is incorrect (got {0}, expected {1})!'.format(json_obj['version'], _PROTOCOL_VERSION)) + if json_obj['magic'] != _PROTOCOL_MAGIC: + raise ValueError('"magic" is incorrect (got "{0}", expected "{1}")!'.format(json_obj['magic'], _PROTOCOL_MAGIC)) + # Read required fields + local_type = json_obj['type'] + local_source = json_obj['source'] + self.type_ = local_type + self.source = local_source + # Read optional fields + self.dest = json_obj.get('dest') + self.data = json_obj.get('data') + except Exception as e: + _logger.error('Failed to deserialize JSON "{0}": {1}'.format(json_str, str(e))) + return False + return True + + def from_json_bytes(self, json_bytes): + ''' + Parse this message from its JSON representation as UTF-8 bytes. + + Args: + json_bytes (bytes): The JSON representation of this message as UTF-8 bytes. + + Returns: + bool: True if this message could be parsed, False otherwise. + ''' + json_str = json_bytes.decode('utf-8') + return self.from_json(json_str) + +def _time_now(now=None): + ''' + Utility function to resolve a potentially cached time value. + + Args: + now (float): The cached timestamp, or None to return the current time. + + Returns: + float: The cached timestamp (if set), otherwise the current time. + ''' + return _time.time() if now is None else now + +# Log handling +_logger = _logging.getLogger(__name__) +_log_handler = _logging.StreamHandler() +_logger.addHandler(_log_handler) +def set_log_level(log_level): + _logger.setLevel(log_level) + _log_handler.setLevel(log_level) + +# Usage example +if __name__ == '__main__': + set_log_level(_logging.DEBUG) + remote_exec = RemoteExecution() + remote_exec.start() + # Ask for a remote node ID + _sys.stdout.write('Enter remote node ID to connect to: ') + remote_node_id = _sys.stdin.readline().rstrip() + # Connect to it + remote_exec.open_command_connection(remote_node_id) + # Process commands remotely + _sys.stdout.write('Connected. Enter commands, or an empty line to quit.\n') + exec_mode = MODE_EXEC_FILE + while True: + input = _sys.stdin.readline().rstrip() + if input: + if input.startswith('set mode '): + exec_mode = input[9:] + else: + print(remote_exec.run_command(input, exec_mode=exec_mode)) + else: + break + remote_exec.stop() diff --git a/openpype/hosts/unreal/remote/rpc/__init__.py b/openpype/hosts/unreal/remote/rpc/__init__.py new file mode 100644 index 00000000000..6dd57c290e2 --- /dev/null +++ b/openpype/hosts/unreal/remote/rpc/__init__.py @@ -0,0 +1,6 @@ +from . import client, factory + +__all__ = [ + client, + factory +] diff --git a/openpype/hosts/unreal/remote/rpc/base_server.py b/openpype/hosts/unreal/remote/rpc/base_server.py new file mode 100644 index 00000000000..a74c5a1d2ee --- /dev/null +++ b/openpype/hosts/unreal/remote/rpc/base_server.py @@ -0,0 +1,245 @@ +import os +import sys +import abc +import queue +import time +import logging +import threading +from xmlrpc.server import SimpleXMLRPCServer + +# importlib machinery needs to be available for importing client modules +from importlib.machinery import SourceFileLoader + +logger = logging.getLogger(__name__) + +EXECUTION_QUEUE = queue.Queue() +RETURN_VALUE_NAME = 'RPC_SERVER_RETURN_VALUE' +ERROR_VALUE_NAME = 'RPC_SERVER_ERROR_VALUE' + + +def run_in_main_thread(callable_instance, *args): + """ + Runs the provided callable instance in the main thread by added it to a que + that is processed by a recurring event in an integration like a timer. + + :param call callable_instance: A callable. + :return: The return value of any call from the client. + """ + timeout = int(os.environ.get('RPC_TIME_OUT', 20)) + + globals().pop(RETURN_VALUE_NAME, None) + globals().pop(ERROR_VALUE_NAME, None) + EXECUTION_QUEUE.put((callable_instance, args)) + + for attempt in range(timeout * 10): + if RETURN_VALUE_NAME in globals(): + return globals().get(RETURN_VALUE_NAME) + elif ERROR_VALUE_NAME in globals(): + raise globals()[ERROR_VALUE_NAME] + else: + time.sleep(0.1) + + if RETURN_VALUE_NAME not in globals(): + raise TimeoutError( + f'The call "{callable_instance.__name__}" timed out because it hit the timeout limit' + f' of {timeout} seconds.' + ) + + +def execute_queued_calls(*extra_args): + """ + Runs calls in the execution que till they are gone. Designed to be passed to a + recurring event in an integration like a timer. + """ + while not EXECUTION_QUEUE.empty(): + if RETURN_VALUE_NAME not in globals(): + callable_instance, args = EXECUTION_QUEUE.get() + try: + globals()[RETURN_VALUE_NAME] = callable_instance(*args) + except Exception as error: + # store the error in the globals and re-raise it + globals()[ERROR_VALUE_NAME] = error + raise error + + +class BaseServer(SimpleXMLRPCServer): + def serve_until_killed(self): + """ + Serves till killed by the client. + """ + self.quit = False + while not self.quit: + self.handle_request() + + +class BaseRPCServer: + def __init__(self, name, port, is_thread=False): + """ + Initialize the base server. + + :param str name: The name of the server. + :param int port: The number of the server port. + :param bool is_thread: Whether or not the server is encapsulated in a thread. + """ + self.server = BaseServer( + (os.environ.get('RPC_HOST', '127.0.0.1'), port), + logRequests=False, + allow_none=True + ) + self.is_thread = is_thread + self.server.register_function(self.add_new_callable) + self.server.register_function(self.kill) + self.server.register_function(self.is_running) + self.server.register_function(self.set_env) + self.server.register_introspection_functions() + self.server.register_multicall_functions() + logger.info(f'Started RPC server "{name}".') + + @staticmethod + def is_running(): + """ + Responds if the server is running. + """ + return True + + @staticmethod + def set_env(name, value): + """ + Sets an environment variable in the server's python environment. + + :param str name: The name of the variable. + :param str value: The value. + """ + os.environ[name] = str(value) + + def kill(self): + """ + Kill the running server from the client. Only if running in blocking mode. + """ + self.server.quit = True + return True + + def add_new_callable(self, callable_name, code, client_system_path, remap_pairs=None): + """ + Adds a new callable defined in the client to the server. + + :param str callable_name: The name of the function that will added to the server. + :param str code: The code of the callable that will be added to the server. + :param list[str] client_system_path: The list of python system paths from the client. + :param list(tuple) remap_pairs: A list of tuples with first value being the client python path root and the + second being the new server path root. This can be useful if the client and server are on two different file + systems and the root of the import paths need to be dynamically replaced. + :return str: A response message back to the client. + """ + for path in client_system_path: + # if a list of remap pairs are provided, they will be remapped before being added to the system path + for client_path_root, matching_server_path_root in remap_pairs or []: + if path.startswith(client_path_root): + path = os.path.join( + matching_server_path_root, + path.replace(client_path_root, '').replace(os.sep, '/').strip('/') + ) + + if path not in sys.path: + sys.path.append(path) + + # run the function code + exec(code) + callable_instance = locals().copy().get(callable_name) + + # grab it from the locals and register it with the server + if callable_instance: + if self.is_thread: + self.server.register_function( + self.thread_safe_call(callable_instance), + callable_name + ) + else: + self.server.register_function( + callable_instance, + callable_name + ) + return f'The function "{callable_name}" has been successfully registered with the server!' + + +class BaseRPCServerThread(threading.Thread, BaseRPCServer): + def __init__(self, name, port): + """ + Initialize the base rpc server. + + :param str name: The name of the server. + :param int port: The number of the server port. + """ + threading.Thread.__init__(self, name=name, daemon=True) + BaseRPCServer.__init__(self, name, port, is_thread=True) + + def run(self): + """ + Overrides the run method. + """ + self.server.serve_forever() + + @abc.abstractmethod + def thread_safe_call(self, callable_instance, *args): + """ + Implements thread safe execution of a call. + """ + return + + +class BaseRPCServerManager: + @abc.abstractmethod + def __init__(self): + """ + Initialize the server manager. + Note: when this class is subclassed `name`, `port`, `threaded_server_class` need to be defined. + """ + self.server_thread = None + self.server_blocking = None + + def start_server_thread(self): + """ + Starts the server in a thread. + """ + self.server_thread = self.threaded_server_class(self.name, self.port) + self.server_thread.start() + + def start_server_blocking(self): + """ + Starts the server in the main thread, which blocks all other processes. This can only + be killed by the client. + """ + self.server_blocking = BaseRPCServer(self.name, self.port) + self.server_blocking.server.serve_until_killed() + + def start(self, threaded=True): + """ + Starts the server. + + :param bool threaded: Whether or not to start the server in a thread. If not threaded + it will block all other processes. + """ + # start the server in a thread + if threaded and not self.server_thread: + self.start_server_thread() + + # start the blocking server + elif not threaded and not self.server_blocking: + self.start_server_blocking() + + else: + logger.info(f'RPC server "{self.name}" is already running...') + + def shutdown(self): + """ + Shuts down the server. + """ + if self.server_thread: + logger.info(f'RPC server "{self.name}" is shutting down...') + + # kill the server in the thread + if self.server_thread: + self.server_thread.server.shutdown() + self.server_thread.join() + + logger.info(f'RPC server "{self.name}" has shutdown.') diff --git a/openpype/hosts/unreal/remote/rpc/client.py b/openpype/hosts/unreal/remote/rpc/client.py new file mode 100644 index 00000000000..42658914aa2 --- /dev/null +++ b/openpype/hosts/unreal/remote/rpc/client.py @@ -0,0 +1,103 @@ +import os +import re +import logging +import inspect +from xmlrpc.client import ( + ServerProxy, + Unmarshaller, + Transport, + ExpatParser, + Fault, + ResponseError +) +logger = logging.getLogger(__package__) + + +class RPCUnmarshaller(Unmarshaller): + def __init__(self, *args, **kwargs): + Unmarshaller.__init__(self, *args, **kwargs) + self.error_pattern = re.compile(r'(?P[^:]*):(?P.*$)') + self.builtin_exceptions = self._get_built_in_exceptions() + + @staticmethod + def _get_built_in_exceptions(): + """ + Gets a list of the built in exception classes in python. + + :return list[BaseException] A list of the built in exception classes in python: + """ + builtin_exceptions = [] + for builtin_name, builtin_class in globals().get('__builtins__').items(): + if builtin_class and inspect.isclass(builtin_class) and issubclass(builtin_class, BaseException): + builtin_exceptions.append(builtin_class) + + return builtin_exceptions + + def close(self): + """ + Override so we redefine the unmarshaller. + + :return tuple: A tuple of marshallables. + """ + if self._type is None or self._marks: + raise ResponseError() + + if self._type == 'fault': + marshallables = self._stack[0] + match = self.error_pattern.match(marshallables.get('faultString', '')) + if match: + exception_name = match.group('exception').strip("") + exception_message = match.group('exception_message') + + if exception_name: + for exception in self.builtin_exceptions: + if exception.__name__ == exception_name: + raise exception(exception_message) + + # if all else fails just raise the fault + raise Fault(**marshallables) + return tuple(self._stack) + + +class RPCTransport(Transport): + def getparser(self): + """ + Override so we can redefine our transport to use its own custom unmarshaller. + + :return tuple(ExpatParser, RPCUnmarshaller): The parser and unmarshaller instances. + """ + unmarshaller = RPCUnmarshaller() + parser = ExpatParser(unmarshaller) + return parser, unmarshaller + + +class RPCServerProxy(ServerProxy): + def __init__(self, *args, **kwargs): + """ + Override so we can redefine the ServerProxy to use our custom transport. + """ + kwargs['transport'] = RPCTransport() + ServerProxy.__init__(self, *args, **kwargs) + + +class RPCClient: + def __init__(self, port, marshall_exceptions=True): + """ + Initializes the rpc client. + + :param int port: A port number the client should connect to. + :param bool marshall_exceptions: Whether or not the exceptions should be marshalled. + """ + if marshall_exceptions: + proxy_class = RPCServerProxy + else: + proxy_class = ServerProxy + + server_ip = os.environ.get('RPC_SERVER_IP', '127.0.0.1') + + self.proxy = proxy_class( + f"http://{server_ip}:{port}", + allow_none=True, + ) + self.marshall_exceptions = marshall_exceptions + self.port = port diff --git a/openpype/hosts/unreal/remote/rpc/exceptions.py b/openpype/hosts/unreal/remote/rpc/exceptions.py new file mode 100644 index 00000000000..1a143d04fe5 --- /dev/null +++ b/openpype/hosts/unreal/remote/rpc/exceptions.py @@ -0,0 +1,79 @@ + +class BaseRPCException(Exception): + """ + Raised when a rpc class method is not authored as a static method. + """ + def __init__(self, message=None, line_link=''): + self.message = message + line_link + super().__init__(self.message) + + +class InvalidClassMethod(BaseRPCException): + """ + Raised when a rpc class method is not authored as a static method. + """ + def __init__(self, cls, method, message=None, line_link=''): + self.message = message + + if message is None: + self.message = ( + f'\n {cls.__name__}.{method.__name__} is not a static method. Please decorate with @staticmethod.' + ) + BaseRPCException.__init__(self, self.message, line_link) + + +class InvalidTestCasePort(BaseRPCException): + """ + Raised when a rpc test case class does not have a port defined. + """ + def __init__(self, cls, message=None, line_link=''): + self.message = message + + if message is None: + self.message = f'\n You must set {cls.__name__}.port to a supported RPC port.' + BaseRPCException.__init__(self, self.message, line_link) + + +class InvalidKeyWordParameters(BaseRPCException): + """ + Raised when a rpc function has key word arguments in its parameters. + """ + def __init__(self, function, kwargs, message=None, line_link=''): + self.message = message + + if message is None: + self.message = ( + f'\n Keyword arguments "{kwargs}" were found on "{function.__name__}". The RPC client does not ' + f'support key word arguments . Please change your code to use only arguments.' + ) + BaseRPCException.__init__(self, self.message, line_link) + + +class UnsupportedArgumentType(BaseRPCException): + """ + Raised when a rpc function's argument type is not supported. + """ + def __init__(self, function, arg, supported_types, message=None, line_link=''): + self.message = message + + if message is None: + self.message = ( + f'\n "{function.__name__}" has an argument of type "{arg.__class__.__name__}". The only types that are' + f' supported by the RPC client are {[supported_type.__name__ for supported_type in supported_types]}.' + ) + BaseRPCException.__init__(self, self.message, line_link) + + +class FileNotSavedOnDisk(BaseRPCException): + """ + Raised when a rpc function is called in a context where it is not a saved file on disk. + """ + def __init__(self, function, message=None): + self.message = message + + if message is None: + self.message = ( + f'\n "{function.__name__}" is not being called from a saved file. The RPC client does not ' + f'support code that is not saved. Please save your code to a file on disk and re-run it.' + ) + BaseRPCException.__init__(self, self.message) diff --git a/openpype/hosts/unreal/remote/rpc/factory.py b/openpype/hosts/unreal/remote/rpc/factory.py new file mode 100644 index 00000000000..cc6ae964ffd --- /dev/null +++ b/openpype/hosts/unreal/remote/rpc/factory.py @@ -0,0 +1,319 @@ +import os +import re +import sys +import logging +import types +import inspect +import textwrap +import unittest +from xmlrpc.client import Fault + +from .client import RPCClient +from .validations import ( + validate_key_word_parameters, + validate_class_method, + get_source_file_path, + get_line_link, + validate_arguments, + validate_file_is_saved, +) + +logger = logging.getLogger(__package__) + + +class RPCFactory: + def __init__(self, rpc_client, remap_pairs=None, default_imports=None): + self.rpc_client = rpc_client + self.file_path = None + self.remap_pairs = remap_pairs + self.default_imports = default_imports or [] + + @staticmethod + def _get_docstring(code, function_name): + """ + Gets the docstring value from the functions code. + + :param list code: A list of code lines. + :param str function_name: The name of the function. + :returns: The docstring text. + :rtype: str + """ + # run the function code + exec('\n'.join(code)) + # get the function from the locals + function_instance = locals().copy().get(function_name) + # get the doc strings from the function + return function_instance.__doc__ + + @staticmethod + def _save_execution_history(code, function, args): + """ + Saves out the executed code to a file. + + :param list code: A list of code lines. + :param callable function: A function. + :param list args: A list of function arguments. + """ + history_file_path = os.environ.get('RPC_EXECUTION_HISTORY_FILE') + + if history_file_path and os.path.exists(os.path.dirname(history_file_path)): + file_size = 0 + if os.path.exists(history_file_path): + file_size = os.path.getsize(history_file_path) + + with open(history_file_path, 'a') as history_file: + # add the import for SourceFileLoader if the file is empty + if file_size == 0: + history_file.write('from importlib.machinery import SourceFileLoader\n') + + # space out the functions + history_file.write(f'\n\n') + + for line in code: + history_file.write(f'{line}\n') + + # convert the args to strings + formatted_args = [] + for arg in args: + if isinstance(arg, str): + formatted_args.append(f'r"{arg}"') + else: + formatted_args.append(str(arg)) + + # write the call with the arg values + params = ", ".join(formatted_args) if formatted_args else '' + history_file.write(f'{function.__name__}({params})\n') + + def _get_callstack_references(self, code, function): + """ + Gets all references for the given code. + + :param list[str] code: The code of the callable. + :param callable function: A callable. + :return str: The new code of the callable with all its references added. + """ + import_code = self.default_imports + + client_module = inspect.getmodule(function) + self.file_path = get_source_file_path(function) + + # if a list of remap pairs have been set, the file path will be remapped to the new server location + # Note: The is useful when the server and client are not on the same machine. + server_module_path = self.file_path + for client_path_root, matching_server_path_root in self.remap_pairs or []: + if self.file_path.startswith(client_path_root): + server_module_path = os.path.join( + matching_server_path_root, + self.file_path.replace(client_path_root, '').replace(os.sep, '/').strip('/') + ) + break + + for key in dir(client_module): + for line_number, line in enumerate(code): + if line.startswith('def '): + continue + + if key in re.split('\.|\(| ', line.strip()): + if os.path.basename(self.file_path) == '__init__.py': + base_name = os.path.basename(os.path.dirname(self.file_path)) + else: + base_name = os.path.basename(self.file_path) + + module_name, file_extension = os.path.splitext(base_name) + + # add the source file to the import code + source_import_code = f'{module_name} = SourceFileLoader("{module_name}", r"{server_module_path}").load_module()' + if source_import_code not in import_code: + import_code.append(source_import_code) + + # relatively import the module from the source file + relative_import_code = f'from {module_name} import {key}' + if relative_import_code not in import_code: + import_code.append(relative_import_code) + + break + + return textwrap.indent('\n'.join(import_code), ' ' * 4) + + def _get_code(self, function): + """ + Gets the code from a callable. + + :param callable function: A callable. + :return str: The code of the callable. + """ + code = textwrap.dedent(inspect.getsource(function)).split('\n') + code = [line for line in code if not line.startswith(('@', '#'))] + + # get the docstring from the code + doc_string = self._get_docstring(code, function.__name__) + + # get import code and insert them inside the function + import_code = self._get_callstack_references(code, function) + code.insert(1, import_code) + + # remove the doc string + if doc_string: + code = '\n'.join(code).replace(doc_string, '') + code = [line for line in code.split('\n') if not all([char == '"' or char == "'" for char in line.strip()])] + + return code + + def _register(self, function): + """ + Registers a given callable with the server. + + :param callable function: A callable. + :return: The code of the function. + :rtype: list + """ + code = self._get_code(function) + try: + # if additional paths are explicitly set, then use them. This is useful with the client is on another + # machine and the python paths are different + additional_paths = list(filter(None, os.environ.get('RPC_ADDITIONAL_PYTHON_PATHS', '').split(','))) + + if not additional_paths: + # otherwise use the current system path + additional_paths = sys.path + + response = self.rpc_client.proxy.add_new_callable( + function.__name__, '\n'.join(code), + additional_paths + ) + if os.environ.get('RPC_DEBUG'): + logger.debug(response) + + except ConnectionRefusedError: + server_name = os.environ.get(f'RPC_SERVER_{self.rpc_client.port}', self.rpc_client.port) + raise ConnectionRefusedError(f'No connection could be made with "{server_name}"') + + return code + + def run_function_remotely(self, function, args): + """ + Handles running the given function on remotely. + + :param callable function: A function reference. + :param tuple(Any) args: The function's arguments. + :return callable: A remote callable. + """ + validate_arguments(function, args) + + # get the remote function instance + code = self._register(function) + remote_function = getattr(self.rpc_client.proxy, function.__name__) + self._save_execution_history(code, function, args) + + current_frame = inspect.currentframe() + outer_frame_info = inspect.getouterframes(current_frame) + # step back 2 frames in the callstack + caller_frame = outer_frame_info[2][0] + # create a trace back that is relevant to the remote code rather than the code transporting it + call_traceback = types.TracebackType(None, caller_frame, caller_frame.f_lasti, caller_frame.f_lineno) + # call the remote function + if not self.rpc_client.marshall_exceptions: + # if exceptions are not marshalled then receive the default Fault + return remote_function(*args) + + # otherwise catch them and add a line link to them + try: + return remote_function(*args) + except Exception as exception: + stack_trace = str(exception) + get_line_link(function) + if isinstance(exception, Fault): + raise Fault(exception.faultCode, exception.faultString) + raise exception.__class__(stack_trace).with_traceback(call_traceback) + + +def remote_call(port, default_imports=None, remap_pairs=None): + """ + A decorator that makes this function run remotely. + + :param Enum port: The name of the port application i.e. maya, blender, unreal. + :param list[str] default_imports: A list of import commands that include modules in every call. + :param list(tuple) remap_pairs: A list of tuples with first value being the client file path root and the + second being the matching server path root. This can be useful if the client and server are on two different file + systems and the root of the import paths need to be dynamically replaced. + """ + def decorator(function): + def wrapper(*args, **kwargs): + validate_file_is_saved(function) + validate_key_word_parameters(function, kwargs) + rpc_factory = RPCFactory( + rpc_client=RPCClient(port), + remap_pairs=remap_pairs, + default_imports=default_imports + ) + return rpc_factory.run_function_remotely(function, args) + return wrapper + return decorator + + +def remote_class(decorator): + """ + A decorator that makes this class run remotely. + + :param remote_call decorator: The remote call decorator. + :return: A decorated class. + """ + def decorate(cls): + for attribute, value in cls.__dict__.items(): + validate_class_method(cls, value) + if callable(getattr(cls, attribute)): + setattr(cls, attribute, decorator(getattr(cls, attribute))) + return cls + return decorate + + +class RPCTestCase(unittest.TestCase): + """ + Subclasses unittest.TestCase to implement a RPC compatible TestCase. + """ + port = None + remap_pairs = None + default_imports = None + + @classmethod + def run_remotely(cls, method, args): + """ + Run the given method remotely. + + :param callable method: A method to wrap. + """ + default_imports = cls.__dict__.get('default_imports', None) + port = cls.__dict__.get('port', None) + remap_pairs = cls.__dict__.get('remap_pairs', None) + rpc_factory = RPCFactory( + rpc_client=RPCClient(port), + default_imports=default_imports, + remap_pairs=remap_pairs + ) + return rpc_factory.run_function_remotely(method, args) + + def _callSetUp(self): + """ + Overrides the TestCase._callSetUp method by passing it to be run remotely. + Notice None is passed as an argument instead of self. This is because only static methods + are allowed by the RPCClient. + """ + self.run_remotely(self.setUp, [None]) + + def _callTearDown(self): + """ + Overrides the TestCase._callTearDown method by passing it to be run remotely. + Notice None is passed as an argument instead of self. This is because only static methods + are allowed by the RPCClient. + """ + # notice None is passed as an argument instead of self so self can't be used + self.run_remotely(self.tearDown, [None]) + + def _callTestMethod(self, method): + """ + Overrides the TestCase._callTestMethod method by capturing the test case method that would be run and then + passing it to be run remotely. Notice no arguments are passed. This is because only static methods + are allowed by the RPCClient. + + :param callable method: A method from the test case. + """ + self.run_remotely(method, []) diff --git a/openpype/hosts/unreal/remote/rpc/unreal_server.py b/openpype/hosts/unreal/remote/rpc/unreal_server.py new file mode 100644 index 00000000000..9ab598c5966 --- /dev/null +++ b/openpype/hosts/unreal/remote/rpc/unreal_server.py @@ -0,0 +1,35 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +import os + +from . import base_server +from .base_server import BaseRPCServerThread, BaseRPCServerManager + + +class UnrealRPCServerThread(BaseRPCServerThread): + def thread_safe_call(self, callable_instance, *args): + """ + Implementation of a thread safe call in Unreal. + """ + return lambda *args: base_server.run_in_main_thread(callable_instance, *args) + + +class RPCServer(BaseRPCServerManager): + def __init__(self): + """ + Initialize the unreal rpc server, with its name and specific port. + """ + super(RPCServer, self).__init__() + self.name = 'UnrealRPCServer' + self.port = int(os.environ.get('RPC_PORT', 9998)) + self.threaded_server_class = UnrealRPCServerThread + + def start_server_thread(self): + """ + Starts the server thread. + """ + # TODO use a timer exposed from FTSTicker instead of slate tick, less aggressive and safer + # https://docs.unrealengine.com/4.27/en-US/PythonAPI/class/AutomationScheduler.html?highlight=automationscheduler#unreal.AutomationScheduler + import unreal + unreal.register_slate_post_tick_callback(base_server.execute_queued_calls) + super(RPCServer, self).start_server_thread() diff --git a/openpype/hosts/unreal/remote/rpc/validations.py b/openpype/hosts/unreal/remote/rpc/validations.py new file mode 100644 index 00000000000..e4a95877005 --- /dev/null +++ b/openpype/hosts/unreal/remote/rpc/validations.py @@ -0,0 +1,105 @@ +import inspect + +from .exceptions import ( + InvalidClassMethod, + InvalidTestCasePort, + InvalidKeyWordParameters, + UnsupportedArgumentType, + FileNotSavedOnDisk, +) + + +def get_source_file_path(function): + """ + Gets the full path to the source code. + + :param callable function: A callable. + :return str: A file path. + """ + client_module = inspect.getmodule(function) + return client_module.__file__ + + +def get_line_link(function): + """ + Gets the line number of a function. + + :param callable function: A callable. + :return int: The line number + """ + lines, line_number = inspect.getsourcelines(function) + file_path = get_source_file_path(function) + return f' File "{file_path}", line {line_number}' + + +def validate_arguments(function, args): + """ + Validates arguments to ensure they are a supported type. + + :param callable function: A function reference. + :param tuple(Any) args: A list of arguments. + """ + supported_types = [str, int, float, tuple, list, dict, bool] + line_link = get_line_link(function) + for arg in args: + if arg is None: + continue + + if type(arg) not in supported_types: + raise UnsupportedArgumentType(function, arg, supported_types, line_link=line_link) + + +def validate_test_case_class(cls): + """ + This is use to validate a subclass of RPCTestCase. While building your test + suite you can call this method on each class preemptively to validate that it + was defined correctly. + + :param RPCTestCase cls: A class. + :param str file_path: Optionally, a file path to the test case can be passed to give + further context into where the error is occurring. + """ + line_link = get_line_link(cls) + if not cls.__dict__.get('port'): + raise InvalidTestCasePort(cls, line_link=line_link) + + for attribute, method in cls.__dict__.items(): + if callable(method) and not isinstance(method, staticmethod): + if method.__name__.startswith('test'): + raise InvalidClassMethod(cls, method, line_link=line_link) + + +def validate_class_method(cls, method): + """ + Validates a method on a class. + + :param Any cls: A class. + :param callable method: A callable. + """ + if callable(method) and not isinstance(method, staticmethod): + line_link = get_line_link(method) + raise InvalidClassMethod(cls, method, line_link=line_link) + + +def validate_key_word_parameters(function, kwargs): + """ + Validates a method on a class. + + :param callable function: A callable. + :param dict kwargs: A dictionary of key word arguments. + """ + if kwargs: + line_link = get_line_link(function) + raise InvalidKeyWordParameters(function, kwargs, line_link=line_link) + + +def validate_file_is_saved(function): + """ + Validates that the file that the function is from is saved on disk. + + :param callable function: A callable. + """ + try: + inspect.getsourcelines(function) + except OSError: + raise FileNotSavedOnDisk(function) diff --git a/openpype/hosts/unreal/remote/unreal.py b/openpype/hosts/unreal/remote/unreal.py new file mode 100644 index 00000000000..f01c4368e8e --- /dev/null +++ b/openpype/hosts/unreal/remote/unreal.py @@ -0,0 +1,992 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +import os +import json +import time +import sys +import inspect +from http.client import RemoteDisconnected + +sys.path.append(os.path.dirname(__file__)) +import rpc.factory +import remote_execution + +try: + import unreal +except ModuleNotFoundError: + pass + +REMAP_PAIRS = [] +UNREAL_PORT = int(os.environ.get('UNREAL_PORT', 9998)) + +# use a different remap pairs when inside a container +if os.environ.get('TEST_ENVIRONMENT'): + UNREAL_PORT = int(os.environ.get('UNREAL_PORT', 8998)) + REMAP_PAIRS = [(os.environ.get('HOST_REPO_FOLDER'), os.environ.get('CONTAINER_REPO_FOLDER'))] + +# this defines a the decorator that makes function run as remote call in unreal +remote_unreal_decorator = rpc.factory.remote_call( + port=UNREAL_PORT, + default_imports=['import unreal'], + remap_pairs=REMAP_PAIRS, +) +rpc_client = rpc.client.RPCClient(port=UNREAL_PORT) +unreal_response = '' + + +def get_response(): + """ + Gets the stdout produced by the remote python call. + + :return str: The stdout produced by the remote python command. + """ + if unreal_response: + full_output = [] + output = unreal_response.get('output') + if output: + full_output.append('\n'.join([line['output'] for line in output if line['type'] != 'Warning'])) + + result = unreal_response.get('result') + if result != 'None': + full_output.append(result) + + return '\n'.join(full_output) + return '' + + +def add_indent(commands, indent): + """ + Adds an indent to the list of python commands. + + :param list commands: A list of python commands that will be run by unreal engine. + :param str indent: A str of tab characters. + :return str: A list of python commands that will be run by unreal engine. + """ + indented_line = [] + for command in commands: + for line in command.split('\n'): + indented_line.append(f'{indent}{line}') + + return indented_line + + +def print_python(commands): + """ + Prints the list of commands as formatted output for debugging and development. + + :param list commands: A list of python commands that will be run by unreal engine. + """ + if os.environ.get('REMOTE_EXECUTION_SHOW_PYTHON'): + dashes = '-' * 50 + label = 'Remote Execution' + sys.stdout.write(f'{dashes}{label}{dashes}\n') + + # get the function name + current_frame = inspect.currentframe() + caller_frame = inspect.getouterframes(current_frame, 2) + function_name = caller_frame[3][3] + + kwargs = caller_frame[3][0].f_locals + kwargs.pop('commands', None) + kwargs.pop('result', None) + + # write out the function name and its arguments + sys.stdout.write( + f'{function_name}(kwargs={json.dumps(kwargs, indent=2, default=lambda element: type(element).__name__)})\n') + + # write out the code with the lines numbers + for index, line in enumerate(commands, 1): + sys.stdout.write(f'{index} {line}\n') + + sys.stdout.write(f'{dashes}{"-" * len(label)}{dashes}\n') + + +def run_unreal_python_commands(remote_exec, commands, failed_connection_attempts=0): + """ + Finds the open unreal editor with remote connection enabled, and sends it python commands. + + :param object remote_exec: A RemoteExecution instance. + :param list commands: A list of python commands that will be run by unreal engine. + :param int failed_connection_attempts: A counter that keeps track of how many times an editor connection attempt + was made. + """ + if failed_connection_attempts == 0: + print_python(commands) + + # wait a tenth of a second before attempting to connect + time.sleep(0.1) + try: + # try to connect to an editor + for node in remote_exec.remote_nodes: + remote_exec.open_command_connection(node.get("node_id")) + + # if a connection is made + if remote_exec.has_command_connection(): + # run the import commands and save the response in the global unreal_response variable + global unreal_response + unreal_response = remote_exec.run_command('\n'.join(commands), unattended=False) + + # otherwise make an other attempt to connect to the engine + else: + if failed_connection_attempts < 50: + run_unreal_python_commands(remote_exec, commands, failed_connection_attempts + 1) + else: + remote_exec.stop() + raise ConnectionError("Could not find an open Unreal Editor instance!") + + # catch all errors + except: + raise ConnectionError("Could not find an open Unreal Editor instance!") + + # shutdown the connection + finally: + remote_exec.stop() + + return get_response() + + +def run_commands(commands): + """ + Runs a list of python commands and returns the result of the output. + + :param list commands: A formatted string of python commands that will be run by unreal engine. + :return str: The stdout produced by the remote python command. + """ + # wrap the commands in a try except so that all exceptions can be logged in the output + commands = ['try:'] + add_indent(commands, '\t') + ['except Exception as error:', '\tprint(error)'] + + # start a connection to the engine that lets you send python-commands.md strings + remote_exec = remote_execution.RemoteExecution() + remote_exec.start() + + # send over the python code as a string and run it + return run_unreal_python_commands(remote_exec, commands) + + +def is_connected(): + """ + Checks the rpc server connection + """ + try: + return rpc_client.proxy.is_running() + except (RemoteDisconnected, ConnectionRefusedError): + return False + + +def set_rpc_timeout(seconds): + """ + Sets the response timeout value of the unreal RPC server. + """ + rpc_client.proxy.set_env('RPC_TIME_OUT', seconds) + + +def bootstrap_unreal_with_rpc_server(): + """ + Bootstraps the running unreal editor with the unreal rpc server if it doesn't already exist. + """ + if not os.environ.get('TEST_ENVIRONMENT'): + if not is_connected(): + dependencies_path = os.path.dirname(__file__) + result = run_commands( + [ + 'import sys', + f'sys.path.append(r"{dependencies_path}")', + 'from rpc import unreal_server', + 'rpc_server = unreal_server.RPCServer()', + 'rpc_server.start(threaded=True)', + ] + ) + if result: + raise RuntimeError(result) + + +class Unreal: + @staticmethod + def get_value(value, unreal_type=None): + """ + Gets the value as an unreal type. + + :param Any value: A value that can be any generic python type. + :param str unreal_type: The name of an unreal type. + :return Any: The converted unreal value. + """ + if unreal_type == 'Array': + if isinstance(value, str): + value = value.split(',') + + if value: + array = unreal.Array(type(value[0])) + for element in value: + array.append(element) + return array + + elif unreal_type == 'Int32Interval': + int_32_interval = unreal.Int32Interval() + int_32_interval.set_editor_property("min", value[0]) + int_32_interval.set_editor_property("max", value[1]) + return int_32_interval + + elif unreal_type == 'Vector': + return unreal.Vector(x=value[0], y=value[1], z=value[2]) + + elif unreal_type == 'Rotator': + return unreal.Rotator(roll=value[0], pitch=value[1], yaw=value[2]) + + elif unreal_type == 'Color': + return unreal.Color(r=value[0], g=value[1], b=value[2], a=value[3]) + + elif unreal_type == 'Name': + return unreal.Name(value) + + elif unreal_type == 'SoftObjectPath': + return unreal.SoftObjectPath(path_string=value) + + elif unreal_type == 'Enum': + enum_value = unreal + for attribute in value.split('.')[1:]: + enum_value = getattr(enum_value, attribute) + return enum_value + + elif unreal_type == 'Asset': + if value: + return Unreal.get_asset(value) + else: + return None + else: + return value + + @staticmethod + def get_asset(asset_path): + """ + Adds the commands that load an unreal asset. + + :param str asset_path: The unreal project path of an asset. + :return str: A list of python commands that will be run by unreal engine. + """ + asset = unreal.load_asset(asset_path) + if not asset: + raise RuntimeError(f"The {asset_path} does not exist in the project!") + return asset + + @staticmethod + def set_settings(property_group, data_object): + """ + Sets a group of properties onto an unreal object. + + :param dict property_group: A dictionary of properties and their data. + :param object data_object: A object. + """ + for attribute, data in property_group.items(): + value = Unreal.get_value( + value=data.get('value'), + unreal_type=data.get('unreal_type'), + ) + data_object.set_editor_property(attribute, value) + return data_object + + @staticmethod + def object_attributes_to_dict(object_instance): + """ + Converts the attributes of the given python object to a dictionary. + + :param object object_instance: A object instance. + :return dict: A dictionary of attributes and values. + """ + data = {} + if object_instance: + for attribute in dir(object_instance): + value = getattr(object_instance, attribute) + if isinstance(value, (bool, str, float, int, list)) and not attribute.startswith("_"): + data[attribute] = getattr(object_instance, attribute) + return data + + +class UnrealImportAsset(Unreal): + def __init__(self, file_path, asset_data, property_data): + """ + Initializes the import with asset data and property data. + + :param str file_path: The full path to the file to import. + :param dict asset_data: A dictionary that contains various data about the asset. + :param PropertyData property_data: A property data instance that contains all property values of the tool. + """ + self._file_path = file_path + self._asset_data = asset_data + self._property_data = property_data + self._import_task = unreal.AssetImportTask() + self._options = None + + def set_skeleton(self): + """ + Sets a skeleton to the import options. + """ + skeleton_path = self._asset_data.get('skeleton_asset_path') + if skeleton_path: + self._options.skeleton = self.get_asset(skeleton_path) + + def set_physics_asset(self): + """ + Sets a physics asset to the import options. + """ + asset_path = self._asset_data.get('asset_path') + physics_asset_path = self._property_data.get('unreal_physics_asset_path', {}).get('value', '') + default_physics_asset = f'{asset_path}_PhysicsAsset' + # try to load the provided physics asset + if physics_asset_path: + physics_asset = unreal.load_asset(physics_asset_path) + else: + physics_asset = unreal.load_asset(default_physics_asset) + + if physics_asset: + self._options.create_physics_asset = False + self._options.physics_asset = physics_asset + else: + self._options.create_physics_asset = True + + def set_static_mesh_import_options(self): + """ + Sets the static mesh import options. + """ + if not self._asset_data.get('skeletal_mesh') and not self._asset_data.get('animation'): + self._options.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH + self._options.static_mesh_import_data.import_mesh_lo_ds = False + + import_data = unreal.FbxStaticMeshImportData() + self.set_settings( + self._property_data['unreal']['import_method']['fbx']['static_mesh_import_data'], + import_data + ) + self._options.static_mesh_import_data = import_data + + def set_skeletal_mesh_import_options(self): + """ + Sets the skeletal mesh import options. + """ + if self._asset_data.get('skeletal_mesh'): + self.set_skeleton() + self.set_physics_asset() + self._options.mesh_type_to_import = unreal.FBXImportType.FBXIT_SKELETAL_MESH + self._options.skeletal_mesh_import_data.import_mesh_lo_ds = False + import_data = unreal.FbxSkeletalMeshImportData() + self.set_settings( + self._property_data['unreal']['import_method']['fbx']['skeletal_mesh_import_data'], + import_data + ) + self._options.skeletal_mesh_import_data = import_data + + def set_animation_import_options(self): + """ + Sets the animation import options. + """ + if self._asset_data.get('animation'): + self.set_skeleton() + self.set_physics_asset() + self._options.mesh_type_to_import = unreal.FBXImportType.FBXIT_ANIMATION + import_data = unreal.FbxAnimSequenceImportData() + self.set_settings( + self._property_data['unreal']['import_method']['fbx']['anim_sequence_import_data'], + import_data, + ) + self._options.anim_sequence_import_data = import_data + + def set_texture_import_options(self): + """ + Sets the texture import options. + """ + if self._property_data.get('import_textures', {}).get('value', False): + import_data = unreal.FbxTextureImportData() + self.set_settings( + self._property_data['unreal']['import_method']['fbx']['texture_import_data'], + import_data + ) + self._options.texture_import_data = import_data + + def set_fbx_import_task_options(self): + """ + Sets the FBX import options. + """ + self._import_task.set_editor_property('filename', self._file_path) + self._import_task.set_editor_property('destination_path', self._asset_data.get('asset_folder')) + self._import_task.set_editor_property('replace_existing', True) + self._import_task.set_editor_property('replace_existing_settings', True) + self._import_task.set_editor_property( + 'automated', + not self._property_data.get('advanced_ui_import', {}).get('value', False) + ) + + import_materials_and_textures = self._property_data.get('import_materials_and_textures', {}).get('value', True) + + import_mesh = self._asset_data.get('import_mesh', False) + import_animations = self._asset_data.get('animation', False) + import_as_skeletal = self._asset_data.get('skeletal_mesh', False) + + # set the options + self._options = unreal.FbxImportUI() + self._options.set_editor_property('import_mesh', import_mesh) + self._options.set_editor_property('import_as_skeletal', import_as_skeletal) + self._options.set_editor_property('import_animations', import_animations) + self._options.set_editor_property('import_materials', import_materials_and_textures) + self._options.set_editor_property('import_textures', import_materials_and_textures) + + # set the static mesh import options + self.set_static_mesh_import_options() + + # add the skeletal mesh import options + self.set_skeletal_mesh_import_options() + + # add the animation import options + self.set_animation_import_options() + + # add the texture import options + self.set_texture_import_options() + + def run_import(self): + # assign the options object to the import task and import the asset + self._import_task.options = self._options + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([self._import_task]) + return list(self._import_task.get_editor_property('imported_object_paths')) + + +class UnrealImportSequence(Unreal): + def __init__(self, asset_path, file_path, track_name, start=None, end=None): + """ + Initializes the import with asset data and property data. + + :param str asset_path: The project path to the asset. + :param str file_path: The full file path to the import file. + :param str track_name: The name of the track. + :param int start: The start frame. + :param int end: The end frame. + """ + self._asset_path = asset_path + self._file_path = file_path + self._track_name = track_name + self._control_channel_mappings = [] + self._sequence = self.get_asset(asset_path) + self._control_rig_settings = unreal.MovieSceneUserImportFBXControlRigSettings() + + if start and end: + self._control_rig_settings.start_time_range = int(start) + self._control_rig_settings.end_time_range = int(end) + + @staticmethod + def get_control_rig_mappings(): + """ + Set the control rig mappings. + + :return list[tuple]: A list channels, transforms and negations that define the control rig mappings. + """ + return [ + (unreal.FControlRigChannelEnum.BOOL, unreal.FTransformChannelEnum.TRANSLATE_X, False), + (unreal.FControlRigChannelEnum.FLOAT, unreal.FTransformChannelEnum.TRANSLATE_Y, False), + (unreal.FControlRigChannelEnum.VECTOR2DX, unreal.FTransformChannelEnum.TRANSLATE_X, False), + (unreal.FControlRigChannelEnum.VECTOR2DY, unreal.FTransformChannelEnum.TRANSLATE_Y, False), + (unreal.FControlRigChannelEnum.POSITION_X, unreal.FTransformChannelEnum.TRANSLATE_X, False), + (unreal.FControlRigChannelEnum.POSITION_Y, unreal.FTransformChannelEnum.TRANSLATE_Y, False), + (unreal.FControlRigChannelEnum.POSITION_Z, unreal.FTransformChannelEnum.TRANSLATE_Z, False), + (unreal.FControlRigChannelEnum.ROTATOR_X, unreal.FTransformChannelEnum.ROTATE_X, False), + (unreal.FControlRigChannelEnum.ROTATOR_Y, unreal.FTransformChannelEnum.ROTATE_Y, False), + (unreal.FControlRigChannelEnum.ROTATOR_Z, unreal.FTransformChannelEnum.ROTATE_Z, False), + (unreal.FControlRigChannelEnum.SCALE_X, unreal.FTransformChannelEnum.SCALE_X, False), + (unreal.FControlRigChannelEnum.SCALE_Y, unreal.FTransformChannelEnum.SCALE_Y, False), + (unreal.FControlRigChannelEnum.SCALE_Z, unreal.FTransformChannelEnum.SCALE_Z, False) + + ] + + def set_control_mapping(self, control_channel, fbx_channel, negate): + """ + Sets the control mapping. + + :param str control_channel: The unreal enum of the control channel. + :param str fbx_channel: The unreal enum of the transform channel. + :param bool negate: Whether or not the mapping is negated. + :return str: A list of python commands that will be run by unreal engine. + """ + control_map = unreal.ControlToTransformMappings() + control_map.set_editor_property('control_channel', control_channel) + control_map.set_editor_property('fbx_channel', fbx_channel) + control_map.set_editor_property('negate', negate) + self._control_maps.append(control_map) + + def remove_level_sequence_keyframes(self): + """ + Removes all key frames from the given sequence and track name. + """ + bindings = {binding.get_name(): binding for binding in self._sequence.get_bindings()} + binding = bindings.get(self._track_name) + track = binding.get_tracks()[0] + section = track.get_sections()[0] + for channel in section.get_channels(): + for key in channel.get_keys(): + channel.remove_key(key) + + def run_import(self, import_type='control_rig'): + """ + Imports key frames onto the given sequence track name from a file. + + :param str import_type: What type of sequence import to run. + """ + sequencer_tools = unreal.SequencerTools() + self.remove_level_sequence_keyframes() + + if import_type == 'control_rig': + for control_channel, fbx_channel, negate in self.get_control_rig_mappings(): + self.set_control_mapping(control_channel, fbx_channel, negate) + + self._control_rig_settings.control_channel_mappings = self._control_channel_mappings + self._control_rig_settings.insert_animation = False + self._control_rig_settings.import_onto_selected_controls = False + sequencer_tools.import_fbx_to_control_rig( + world=unreal.EditorLevelLibrary.get_editor_world(), + sequence=self._sequence, + actor_with_control_rig_track=self._track_name, + selected_control_rig_names=[], + import_fbx_control_rig_settings=self._control_rig_settings, + import_filename=self._file_path + ) + + +@rpc.factory.remote_class(remote_unreal_decorator) +class UnrealRemoteCalls: + @staticmethod + def get_lod_count(asset_path): + """ + Gets the number of lods on the given asset. + + :param str asset_path: The path to the unreal asset. + :return int: The number of lods on the asset. + """ + lod_count = 0 + asset = Unreal.get_asset(asset_path) + if asset.__class__.__name__ == 'SkeletalMesh': + lod_count = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem).get_lod_count(asset) + + if asset.__class__.__name__ == 'StaticMesh': + lod_count = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem).get_lod_count(asset) + + return lod_count + + @staticmethod + def asset_exists(asset_path): + """ + Checks to see if an asset exist in unreal. + + :param str asset_path: The path to the unreal asset. + :return bool: Whether or not the asset exists. + """ + return bool(unreal.load_asset(asset_path)) + + @staticmethod + def directory_exists(asset_path): + """ + Checks to see if a directory exist in unreal. + + :param str asset_path: The path to the unreal asset. + :return bool: Whether or not the asset exists. + """ + # TODO fix this when the unreal API is fixed where it queries the registry correctly + # https://jira.it.epicgames.com/browse/UE-142234 + # return unreal.EditorAssetLibrary.does_directory_exist(asset_path) + return True + + @staticmethod + def get_static_mesh_collision_info(asset_path): + """ + Gets the number of convex and simple collisions on a static mesh. + + :param str asset_path: The path to the unreal asset. + :return str: The name of the complex collision. + """ + mesh = Unreal.get_asset(asset_path) + return { + 'simple': unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem).get_simple_collision_count(mesh), + 'convex': unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem).get_convex_collision_count(mesh), + 'customized': mesh.get_editor_property('customized_collision') + } + + @staticmethod + def get_material_index_by_name(asset_path, material_name): + """ + Checks to see if an asset has a complex collision. + + :param str asset_path: The path to the unreal asset. + :param str material_name: The name of the material. + :return str: The name of the complex collision. + """ + mesh = Unreal.get_asset(asset_path) + if mesh.__class__.__name__ == 'SkeletalMesh': + for index, material in enumerate(mesh.materials): + if material.material_slot_name == material_name: + return index + if mesh.__class__.__name__ == 'StaticMesh': + for index, material in enumerate(mesh.static_materials): + if material.material_slot_name == material_name: + return index + + @staticmethod + def has_socket(asset_path, socket_name): + """ + Checks to see if an asset has a socket. + + :param str asset_path: The path to the unreal asset. + :param str socket_name: The name of the socket to look for. + :return bool: Whether or not the asset has the given socket or not. + """ + mesh = Unreal.get_asset(asset_path) + return bool(mesh.find_socket(socket_name)) + + @staticmethod + def delete_asset(asset_path): + """ + Deletes an asset in unreal. + + :param str asset_path: The path to the unreal asset. + :return bool: Whether or not the asset was deleted. + """ + if unreal.EditorAssetLibrary.does_asset_exist(asset_path): + unreal.EditorAssetLibrary.delete_asset(asset_path) + + @staticmethod + def delete_directory(directory_path): + """ + Deletes an folder and its contents in unreal. + + :param str directory_path: The game path to the unreal project folder. + :return bool: Whether or not the directory was deleted. + """ + # API BUG:cant check if exists https://jira.it.epicgames.com/browse/UE-142234 + # if unreal.EditorAssetLibrary.does_directory_exist(directory_path): + unreal.EditorAssetLibrary.delete_directory(directory_path) + + @staticmethod + def import_asset(file_path, asset_data, property_data, file_type='fbx'): + """ + Imports an asset to unreal based on the asset data in the provided dictionary. + + :param str file_path: The full path to the file to import. + :param dict asset_data: A dictionary of import parameters. + :param dict property_data: A dictionary representation of the properties. + :param str file_type: The import file type. + """ + unreal_import_asset = UnrealImportAsset( + file_path=file_path, + asset_data=asset_data, + property_data=property_data + ) + if file_type.lower() == 'fbx': + unreal_import_asset.set_fbx_import_task_options() + + # run the import task + return unreal_import_asset.run_import() + + @staticmethod + def import_sequence_track(asset_path, file_path, track_name, start=None, end=None): + """ + Initializes the import with asset data and property data. + + :param str asset_path: The project path to the asset. + :param str file_path: The full file path to the import file. + :param str track_name: The name of the track. + :param int start: The start frame. + :param int end: The end frame. + """ + unreal_import_sequence = UnrealImportSequence( + asset_path=asset_path, + file_path=file_path, + track_name=track_name, + start=start, + end=start + ) + # run the import task + unreal_import_sequence.run_import() + + @staticmethod + def import_skeletal_mesh_lod(asset_path, file_path, index): + """ + Imports a lod onto a skeletal mesh. + + :param str asset_path: The project path to the skeletal mesh in unreal. + :param str file_path: The path to the file that contains the lods on disk. + :param int index: Which lod index to import the lod on. + """ + skeletal_mesh = Unreal.get_asset(asset_path) + skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) + result = skeletal_mesh_subsystem.import_lod(skeletal_mesh, index, file_path) + if result == -1: + raise RuntimeError(f"{file_path} import failed!") + + @staticmethod + def import_static_mesh_lod(asset_path, file_path, index): + """ + Imports a lod onto a static mesh. + + :param str asset_path: The project path to the skeletal mesh in unreal. + :param str file_path: The path to the file that contains the lods on disk. + :param int index: Which lod index to import the lod on. + """ + static_mesh = Unreal.get_asset(asset_path) + static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) + result = static_mesh_subsystem.import_lod(static_mesh, index, file_path) + if result == -1: + raise RuntimeError(f"{file_path} import failed!") + + @staticmethod + def set_skeletal_mesh_lod_build_settings(asset_path, index, property_data): + """ + Sets the lod build settings for skeletal mesh. + + :param str asset_path: The project path to the skeletal mesh in unreal. + :param int index: Which lod index to import the lod on. + :param dict property_data: A dictionary representation of the properties. + """ + skeletal_mesh = Unreal.get_asset(asset_path) + skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) + options = unreal.SkeletalMeshBuildSettings() + options = Unreal.set_settings( + property_data['unreal']['editor_skeletal_mesh_library']['lod_build_settings'], + options + ) + skeletal_mesh_subsystem.set_lod_build_settings(skeletal_mesh, index, options) + + @staticmethod + def set_static_mesh_lod_build_settings(asset_path, index, property_data): + """ + Sets the lod build settings for static mesh. + + :param str asset_path: The project path to the static mesh in unreal. + :param int index: Which lod index to import the lod on. + :param dict property_data: A dictionary representation of the properties. + """ + static_mesh = Unreal.get_asset(asset_path) + static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) + options = unreal.MeshBuildSettings() + options = Unreal.set_settings( + property_data['unreal']['editor_static_mesh_library']['lod_build_settings'], + options + ) + static_mesh_subsystem.set_lod_build_settings(static_mesh, index, options) + + @staticmethod + def reset_skeletal_mesh_lods(asset_path, property_data): + """ + Removes all lods on the given skeletal mesh. + + :param str asset_path: The project path to the skeletal mesh in unreal. + :param dict property_data: A dictionary representation of the properties. + """ + skeletal_mesh = Unreal.get_asset(asset_path) + skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) + lod_count = skeletal_mesh_subsystem.get_lod_count(skeletal_mesh) + if lod_count > 1: + skeletal_mesh_subsystem.remove_lo_ds(skeletal_mesh, list(range(1, lod_count))) + + lod_settings_path = property_data.get('unreal_skeletal_mesh_lod_settings_path', {}).get('value', '') + if lod_settings_path: + data_asset = Unreal.get_asset(asset_path) + skeletal_mesh.lod_settings = data_asset + skeletal_mesh_subsystem.regenerate_lod(skeletal_mesh, new_lod_count=lod_count) + + @staticmethod + def reset_static_mesh_lods(asset_path): + """ + Removes all lods on the given static mesh. + + :param str asset_path: The project path to the static mesh in unreal. + """ + static_mesh = Unreal.get_asset(asset_path) + static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) + lod_count = static_mesh_subsystem.get_lod_count(static_mesh) + if lod_count > 1: + static_mesh_subsystem.remove_lods(static_mesh) + + @staticmethod + def set_static_mesh_sockets(asset_path, asset_data): + """ + Sets sockets on a static mesh. + + :param str asset_path: The project path to the skeletal mesh in unreal. + :param dict asset_data: A dictionary of import parameters. + """ + static_mesh = Unreal.get_asset(asset_path) + for socket_name, socket_data in asset_data.get('sockets').items(): + socket = unreal.StaticMeshSocket() + + # apply the socket settings + socket.set_editor_property('relative_location', socket_data.get('relative_location')) + socket.set_editor_property('relative_rotation', socket_data.get('relative_rotation')) + socket.set_editor_property('relative_scale', socket_data.get('relative_scale')) + socket.set_editor_property('socket_name', socket_name) + + # if that socket already exists remove it + existing_socket = static_mesh.find_socket(socket_name) + if existing_socket: + static_mesh.remove_socket(existing_socket) + + # create a new socket + static_mesh.add_socket(socket) + + @staticmethod + def get_lod_build_settings(asset_path, index): + """ + Gets the lod build settings from the given asset. + + :param str asset_path: The project path to the asset. + :param int index: The lod index to check. + :return dict: A dictionary of lod build settings. + """ + build_settings = None + mesh = Unreal.get_asset(asset_path) + if not mesh: + raise RuntimeError(f'"{asset_path}" was not found in the unreal project!') + if mesh.__class__.__name__ == 'SkeletalMesh': + skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) + build_settings = skeletal_mesh_subsystem.get_lod_build_settings(mesh, index) + if mesh.__class__.__name__ == 'StaticMesh': + static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) + build_settings = static_mesh_subsystem.get_lod_build_settings(mesh, index) + + return Unreal.object_attributes_to_dict(build_settings) + + @staticmethod + def get_bone_path_to_root(asset_path, bone_name): + """ + Gets the path to the root bone from the given skeleton. + + :param str asset_path: The project path to the asset. + :param str bone_name: The name of the bone to start from. + :return list: A list of bone name all the way to the root bone. + """ + animation = Unreal.get_asset(asset_path) + path = unreal.AnimationLibrary.find_bone_path_to_root(animation, bone_name) + return [str(i) for i in path] + + @staticmethod + def get_bone_transform_for_frame(asset_path, bone_name, frame): + """ + Gets the transformations of the given bone on the given frame. + + :param str asset_path: The project path to the asset. + :param str bone_name: The name of the bone to get the transforms of. + :param float frame: The frame number. + :return dict: A dictionary of transformation values. + """ + animation = Unreal.get_asset(asset_path) + path = unreal.AnimationLibrary.find_bone_path_to_root(animation, bone_name) + transform = unreal.AnimationLibrary.get_bone_pose_for_frame(animation, bone_name, frame, True) + world_rotation = unreal.Rotator() + world_location = unreal.Transform() + for bone in path: + bone_transform = unreal.AnimationLibrary.get_bone_pose_for_frame(animation, str(bone), frame, True) + world_rotation = world_rotation.combine(bone_transform.rotation.rotator()) + world_location = world_location.multiply(bone_transform) + + return { + 'scale': transform.scale3d.to_tuple(), + 'world_rotation': world_rotation.transform().rotation.euler().to_tuple(), + 'local_rotation': transform.rotation.euler().to_tuple(), + 'world_location': world_location.translation.to_tuple(), + 'local_location': transform.translation.to_tuple() + } + + @staticmethod + def get_bone_count(skeleton_path): + """ + Gets the bone count from the given skeleton. + + :param str skeleton_path: The project path to the skeleton. + :return int: The number of bones. + """ + skeleton = unreal.load_asset(skeleton_path) + return len(skeleton.get_editor_property('bone_tree')) + + @staticmethod + def get_origin(asset_path): + """ + Gets the location of the assets origin. + + :param str asset_path: The project path to the asset. + :return list: A list of bone name all the way to the root bone. + """ + mesh = Unreal.get_asset(asset_path) + return mesh.get_bounds().origin.to_tuple() + + @staticmethod + def get_sequence_track_keyframe(asset_path, track_name, curve_name, frame): + """ + Gets the transformations of the given bone on the given frame. + + :param str asset_path: The project path to the asset. + :param str track_name: The name of the track. + :param str curve_name: The curve name. + :param float frame: The frame number. + :return dict: A dictionary of transformation values. + """ + sequence = unreal.load_asset(asset_path) + bindings = {binding.get_name(): binding for binding in sequence.get_bindings()} + binding = bindings.get(track_name) + track = binding.get_tracks()[0] + section = track.get_sections()[0] + data = {} + for channel in section.get_channels(): + if channel.get_name().startswith(curve_name): + for key in channel.get_keys(): + if key.get_time().frame_number.value == frame: + data[channel.get_name()] = key.get_value() + return data + + @staticmethod + def create_asset(asset_path, asset_class=None, asset_factory=None, unique_name=True): + """ + Creates a new unreal asset. + + :param str asset_path: The project path to the asset. + :param str asset_class: The name of the unreal asset class. + :param str asset_factory: The name of the unreal factory. + :param bool unique_name: Whether or not the check if the name is unique before creating the asset. + """ + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + if unique_name: + asset_path, _ = asset_tools.create_unique_asset_name( + base_package_name=asset_path, + suffix='' + ) + path = asset_path.rsplit("/", 1)[0] + name = asset_path.rsplit("/", 1)[1] + asset_tools.create_asset( + asset_name=name, + package_path=path, + asset_class=asset_class, + factory=asset_factory + ) + + @staticmethod + def create_folder(folder_path): + unreal.EditorAssetLibrary.make_directory(folder_path) + + @staticmethod + def import_animation_fcurves(asset_path, fcurve_file_path): + """ + Imports fcurves from a file onto an animation sequence. + + :param str asset_path: The project path to the skeletal mesh in unreal. + :param str fcurve_file_path: The file path to the fcurve file. + """ + animation_sequence = Unreal.get_asset(asset_path) + with open(fcurve_file_path, 'r') as fcurve_file: + fcurve_data = json.load(fcurve_file) + + for fcurve_name, keys in fcurve_data.items(): + unreal.AnimationLibrary.add_curve(animation_sequence, fcurve_name) + for key in keys: + unreal.AnimationLibrary.add_float_curve_key(animation_sequence, fcurve_name, key[0], key[1]) + + @staticmethod + def does_curve_exist(asset_path, curve_name): + """ + Checks if the fcurve exists on the animation sequence. + + :param str asset_path: The project path to the skeletal mesh in unreal. + :param str curve_name: The fcurve name. + """ + animation_sequence = Unreal.get_asset(asset_path) + return unreal.AnimationLibrary.does_curve_exist(animation_sequence, curve_name, unreal.RawCurveTrackTypes.RCT_FLOAT) From 7f3ee0cdaa378ed8349b4926288d20ea28c0a773 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Oct 2022 16:16:49 +0100 Subject: [PATCH 02/55] Added websockets to UE4 --- .../UE_4.7/Source/OpenPype/OpenPype.Build.cs | 1 + .../Source/OpenPype/Private/OpenPype.cpp | 55 +++++++++++++++++++ .../UE_4.7/Source/OpenPype/Public/OpenPype.h | 6 ++ 3 files changed, 62 insertions(+) diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs index c30835b63dc..414ae02c4bd 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs @@ -26,6 +26,7 @@ public OpenPype(ReadOnlyTargetRules Target) : base(Target) new string[] { "Core", + "WebSockets", // ... add other public dependencies that you statically link with here ... } ); diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp index 15c46b38628..76fd99bdc57 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp @@ -15,6 +15,9 @@ void FOpenPypeModule::StartupModule() FOpenPypeStyle::Initialize(); FOpenPypeStyle::SetIcon("Logo", "openpype40"); + CreateSocket(); + ConnectToSocket(); + // Create the Extender that will add content to the menu FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); @@ -100,4 +103,56 @@ void FOpenPypeModule::MenuDialog() { bridge->RunInPython_Dialog(); } +void FOpenPypeModule::CreateSocket() { + UE_LOG(LogTemp, Warning, TEXT("Starting web socket...")); + + FString url = FWindowsPlatformMisc::GetEnvironmentVariable(*FString("WEBSOCKET_URL")); + + UE_LOG(LogTemp, Warning, TEXT("Websocket URL: %s"), *url); + + const FString ServerURL = url; // Your server URL. You can use ws, wss or wss+insecure. + const FString ServerProtocol = TEXT("ws"); // The WebServer protocol you want to use. + + TMap UpgradeHeaders; + UpgradeHeaders.Add(TEXT("upgrade"), TEXT("websocket")); + + Socket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol, UpgradeHeaders); +} + +void FOpenPypeModule::ConnectToSocket() { + // We bind all available events + Socket->OnConnected().AddLambda([]() -> void { + // This code will run once connected. + UE_LOG(LogTemp, Warning, TEXT("Connected")); + }); + + Socket->OnConnectionError().AddLambda([](const FString & Error) -> void { + // This code will run if the connection failed. Check Error to see what happened. + UE_LOG(LogTemp, Warning, TEXT("Error during connection")); + UE_LOG(LogTemp, Warning, TEXT("%s"), *Error); + }); + + Socket->OnClosed().AddLambda([](int32 StatusCode, const FString& Reason, bool bWasClean) -> void { + // This code will run when the connection to the server has been terminated. + // Because of an error or a call to Socket->Close(). + }); + + Socket->OnMessage().AddLambda([](const FString & Message) -> void { + // This code will run when we receive a string message from the server. + }); + + Socket->OnRawMessage().AddLambda([](const void* Data, SIZE_T Size, SIZE_T BytesRemaining) -> void { + // This code will run when we receive a raw (binary) message from the server. + }); + + Socket->OnMessageSent().AddLambda([](const FString& MessageString) -> void { + // This code is called after we sent a message to the server. + }); + + UE_LOG(LogTemp, Warning, TEXT("Connecting web socket to server...")); + + // And we finally connect to the server. + Socket->Connect(); +} + IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h index db3f2993548..713ef636891 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h @@ -18,4 +18,10 @@ class FOpenPypeModule : public IModuleInterface void MenuPopup(); void MenuDialog(); + void CreateSocket(); + void ConnectToSocket(); + +private: + TSharedPtr Socket; + }; From 1d82ed6893f798314afc5c24bd92193ca3731403 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 17:52:18 +0200 Subject: [PATCH 03/55] fix includes in 4.27 plugin --- .../integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp | 4 +++- .../integration/UE_4.7/Source/OpenPype/Public/OpenPype.h | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp index 76fd99bdc57..1a22c653220 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp @@ -2,6 +2,8 @@ #include "LevelEditor.h" #include "OpenPypePythonBridge.h" #include "OpenPypeStyle.h" +#include "GenericPlatform/GenericPlatformMisc.h" +#include "WebSocketsModule.h" // Module definition static const FName OpenPypeTabName("OpenPype"); @@ -20,7 +22,7 @@ void FOpenPypeModule::StartupModule() // Create the Extender that will add content to the menu FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); - + TSharedPtr MenuExtender = MakeShareable(new FExtender()); TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h index 713ef636891..f251248f034 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h @@ -4,6 +4,8 @@ #include "Engine.h" +#include "IWebSocket.h" // Socket definition + class FOpenPypeModule : public IModuleInterface { From 89e9683b12f2bad112560595c90767234c1a96e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 17:52:30 +0200 Subject: [PATCH 04/55] fix websocket communication handshake --- openpype/hosts/unreal/remote/communication_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/remote/communication_server.py b/openpype/hosts/unreal/remote/communication_server.py index 203a16ffa6c..5e3a07f7363 100644 --- a/openpype/hosts/unreal/remote/communication_server.py +++ b/openpype/hosts/unreal/remote/communication_server.py @@ -704,7 +704,7 @@ def _create_routes(self): self, loop=self.websocket_server.loop ) self.websocket_server.add_route( - "*", "/", self.websocket_handler + "*", "/ws", self.websocket_handler ) async def websocket_handler(self, request): @@ -762,7 +762,7 @@ def launch(self, launch_args): self._create_routes() - os.environ["WEBSOCKET_URL"] = "ws://localhost:{}".format( + os.environ["WEBSOCKET_URL"] = "ws://localhost:{}/ws".format( self.websocket_server.port ) @@ -930,7 +930,7 @@ def _create_routes(self): self, loop=self.websocket_server.loop ) self.websocket_server.add_route( - "*", "/", self.websocket_rpc.handle_request + "*", "/ws", self.websocket_rpc.handle_request ) def execute_in_main_thread(self, main_thread_item, wait=True): From ec70e2826f9b11578e53529938e3205ea5f50cb1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 17:52:54 +0200 Subject: [PATCH 05/55] remove unused code --- .../unreal/remote/communication_server.py | 262 ------------------ 1 file changed, 262 deletions(-) diff --git a/openpype/hosts/unreal/remote/communication_server.py b/openpype/hosts/unreal/remote/communication_server.py index 5e3a07f7363..6efd260e969 100644 --- a/openpype/hosts/unreal/remote/communication_server.py +++ b/openpype/hosts/unreal/remote/communication_server.py @@ -23,7 +23,6 @@ from aiohttp_json_rpc.exceptions import RpcError from openpype.lib import emit_event -# from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -480,212 +479,6 @@ def server_is_running(self): return False return self.websocket_server.server_is_running - def _windows_file_process(self, src_dst_mapping, to_remove): - """Windows specific file processing asking for admin permissions. - - It is required to have administration permissions to modify plugin - files in TVPaint installation folder. - - Method requires `pywin32` python module. - - Args: - src_dst_mapping (list, tuple, set): Mapping of source file to - destination. Both must be full path. Each item must be iterable - of size 2 `(C:/src/file.dll, C:/dst/file.dll)`. - to_remove (list): Fullpath to files that should be removed. - """ - - import pythoncom - from win32comext.shell import shell - - # Create temp folder where plugin files are temporary copied - # - reason is that copy to TVPaint requires administartion permissions - # but admin may not have access to source folder - tmp_dir = os.path.normpath( - tempfile.mkdtemp(prefix="tvpaint_copy_") - ) - - # Copy source to temp folder and create new mapping - dst_folders = collections.defaultdict(list) - new_src_dst_mapping = [] - for old_src, dst in src_dst_mapping: - new_src = os.path.join(tmp_dir, os.path.split(old_src)[1]) - shutil.copy(old_src, new_src) - new_src_dst_mapping.append((new_src, dst)) - - for src, dst in new_src_dst_mapping: - src = os.path.normpath(src) - dst = os.path.normpath(dst) - dst_filename = os.path.basename(dst) - dst_folder_path = os.path.dirname(dst) - dst_folders[dst_folder_path].append((dst_filename, src)) - - # create an instance of IFileOperation - fo = pythoncom.CoCreateInstance( - shell.CLSID_FileOperation, - None, - pythoncom.CLSCTX_ALL, - shell.IID_IFileOperation - ) - # Add delete command to file operation object - for filepath in to_remove: - item = shell.SHCreateItemFromParsingName( - filepath, None, shell.IID_IShellItem - ) - fo.DeleteItem(item) - - # here you can use SetOperationFlags, progress Sinks, etc. - for folder_path, items in dst_folders.items(): - # create an instance of IShellItem for the target folder - folder_item = shell.SHCreateItemFromParsingName( - folder_path, None, shell.IID_IShellItem - ) - for _dst_filename, source_file_path in items: - # create an instance of IShellItem for the source item - copy_item = shell.SHCreateItemFromParsingName( - source_file_path, None, shell.IID_IShellItem - ) - # queue the copy operation - fo.CopyItem(copy_item, folder_item, _dst_filename, None) - - # commit - fo.PerformOperations() - - # Remove temp folder - shutil.rmtree(tmp_dir) - - # def _prepare_windows_plugin(self, launch_args): - # """Copy plugin to TVPaint plugins and set PATH to dependencies. - - # Check if plugin in TVPaint's plugins exist and match to plugin - # version to current implementation version. Based on 64-bit or 32-bit - # version of the plugin. Path to libraries required for plugin is added - # to PATH variable. - # """ - - # host_executable = launch_args[0] - # executable_file = os.path.basename(host_executable) - # if "64bit" in executable_file: - # subfolder = "windows_x64" - # elif "32bit" in executable_file: - # subfolder = "windows_x86" - # else: - # raise ValueError( - # "Can't determine if executable " - # "leads to 32-bit or 64-bit TVPaint!" - # ) - - # plugin_files_path = get_plugin_files_path() - # # Folder for right windows plugin files - # source_plugins_dir = os.path.join(plugin_files_path, subfolder) - - # # Path to libraries (.dll) required for plugin library - # # - additional libraries can be copied to TVPaint installation folder - # # (next to executable) or added to PATH environment variable - # additional_libs_folder = os.path.join( - # source_plugins_dir, - # "additional_libraries" - # ) - # additional_libs_folder = additional_libs_folder.replace("\\", "/") - # if ( - # os.path.exists(additional_libs_folder) - # and additional_libs_folder not in os.environ["PATH"] - # ): - # os.environ["PATH"] += (os.pathsep + additional_libs_folder) - - # # Path to TVPaint's plugins folder (where we want to add our plugin) - # host_plugins_path = os.path.join( - # os.path.dirname(host_executable), - # "plugins" - # ) - - # # Files that must be copied to TVPaint's plugin folder - # plugin_dir = os.path.join(source_plugins_dir, "plugin") - - # to_copy = [] - # to_remove = [] - # # Remove old plugin name - # deprecated_filepath = os.path.join( - # host_plugins_path, "AvalonPlugin.dll" - # ) - # if os.path.exists(deprecated_filepath): - # to_remove.append(deprecated_filepath) - - # for filename in os.listdir(plugin_dir): - # src_full_path = os.path.join(plugin_dir, filename) - # dst_full_path = os.path.join(host_plugins_path, filename) - # if dst_full_path in to_remove: - # to_remove.remove(dst_full_path) - - # if ( - # not os.path.exists(dst_full_path) - # or not filecmp.cmp(src_full_path, dst_full_path) - # ): - # to_copy.append((src_full_path, dst_full_path)) - - # # Skip copy if everything is done - # if not to_copy and not to_remove: - # return - - # # Try to copy - # try: - # self._windows_file_process(to_copy, to_remove) - # except Exception: - # log.error("Plugin copy failed", exc_info=True) - - # # Validate copy was done - # invalid_copy = [] - # for src, dst in to_copy: - # if not os.path.exists(dst) or not filecmp.cmp(src, dst): - # invalid_copy.append((src, dst)) - - # # Validate delete was dones - # invalid_remove = [] - # for filepath in to_remove: - # if os.path.exists(filepath): - # invalid_remove.append(filepath) - - # if not invalid_remove and not invalid_copy: - # return - - # msg_parts = [] - # if invalid_remove: - # msg_parts.append( - # "Failed to remove files: {}".format(", ".join(invalid_remove)) - # ) - - # if invalid_copy: - # _invalid = [ - # "\"{}\" -> \"{}\"".format(src, dst) - # for src, dst in invalid_copy - # ] - # msg_parts.append( - # "Failed to copy files: {}".format(", ".join(_invalid)) - # ) - # raise RuntimeError(" & ".join(msg_parts)) - - # def _launch_tv_paint(self, launch_args): - # flags = ( - # subprocess.DETACHED_PROCESS - # | subprocess.CREATE_NEW_PROCESS_GROUP - # ) - # env = os.environ.copy() - # # Remove QuickTime from PATH on windows - # # - quicktime overrides TVPaint's ffmpeg encode/decode which may - # # cause issues on loading - # if platform.system().lower() == "windows": - # new_path = [] - # for path in env["PATH"].split(os.pathsep): - # if path and "quicktime" not in path.lower(): - # new_path.append(path) - # env["PATH"] = os.pathsep.join(new_path) - - # kwargs = { - # "env": env, - # "creationflags": flags - # } - # self.process = subprocess.Popen(launch_args, **kwargs) - def _launch_unreal(self, launch_args): flags = ( subprocess.DETACHED_PROCESS @@ -788,36 +581,8 @@ def launch(self, launch_args): break time.sleep(0.5) - self._on_client_connect() - emit_event("application.launched") - def _on_client_connect(self): - self._initial_textfile_write() - - def _initial_textfile_write(self): - """Show popup about Write to file at start of TVPaint.""" - tmp_file = tempfile.NamedTemporaryFile( - mode="w", prefix="a_tvp_", suffix=".txt", delete=False - ) - tmp_file.close() - tmp_filepath = tmp_file.name.replace("\\", "/") - george_script = ( - "tv_writetextfile \"strict\" \"append\" \"{}\" \"empty\"" - ).format(tmp_filepath) - - result = CommunicationWrapper.execute_george(george_script) - - # Remote the file - os.remove(tmp_filepath) - - if result is None: - log.warning( - "Host was probably closed before plugin was initialized." - ) - elif result.lower() == "forbidden": - log.warning("User didn't confirm saving files.") - def _client(self): if not self.websocket_rpc: log.warning("Communicator's server did not start yet.") @@ -852,33 +617,6 @@ def send_notification(self, method, params=None): client, method, params ) - def execute_george(self, george_script): - """Execute passed goerge script in TVPaint.""" - return self.send_request( - "execute_george", [george_script] - ) - - def execute_george_through_file(self, george_script): - """Execute george script with temp file. - - Allows to execute multiline george script without stopping websocket - client. - - On windows make sure script does not contain paths with backwards - slashes in paths, TVPaint won't execute properly in that case. - - Args: - george_script (str): George script to execute. May be multilined. - """ - temporary_file = tempfile.NamedTemporaryFile( - mode="w", prefix="a_tvp_", suffix=".grg", delete=False - ) - temporary_file.write(george_script) - temporary_file.close() - temp_file_path = temporary_file.name.replace("\\", "/") - self.execute_george("tv_runscript {}".format(temp_file_path)) - os.remove(temp_file_path) - class QtCommunicator(BaseCommunicator): menu_definitions = { From eac1bb4a7252a1abc93fbe3f0ea6a404ee3e87c0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 17:58:14 +0200 Subject: [PATCH 06/55] uncomment part code --- .../unreal/remote/communication_server.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/unreal/remote/communication_server.py b/openpype/hosts/unreal/remote/communication_server.py index 6efd260e969..43b8b815355 100644 --- a/openpype/hosts/unreal/remote/communication_server.py +++ b/openpype/hosts/unreal/remote/communication_server.py @@ -207,26 +207,26 @@ def __init__(self, communication_obj, route_name="", **kwargs): self.route_name = route_name self.communication_obj = communication_obj - # async def _handle_rpc_msg(self, http_request, raw_msg): - # # This is duplicated code from super but there is no way how to do it - # # to be able handle server->client requests - # host = http_request.host - # if host in self.waiting_requests: - # try: - # _raw_message = raw_msg.data - # msg = decode_msg(_raw_message) - - # except RpcError as error: - # await self._ws_send_str(http_request, encode_error(error)) - # return - - # if msg.type in (JsonRpcMsgTyp.RESULT, JsonRpcMsgTyp.ERROR): - # msg_data = json.loads(_raw_message) - # if msg_data.get("id") in self.waiting_requests[host]: - # self.responses[host].append(msg_data) - # return - - # return await super()._handle_rpc_msg(http_request, raw_msg) + async def _handle_rpc_msg(self, http_request, raw_msg): + # This is duplicated code from super but there is no way how to do it + # to be able handle server->client requests + host = http_request.host + if host in self.waiting_requests: + try: + _raw_message = raw_msg.data + msg = decode_msg(_raw_message) + + except RpcError as error: + await self._ws_send_str(http_request, encode_error(error)) + return + + if msg.type in (JsonRpcMsgTyp.RESULT, JsonRpcMsgTyp.ERROR): + msg_data = json.loads(_raw_message) + if msg_data.get("id") in self.waiting_requests[host]: + self.responses[host].append(msg_data) + return + + return await super()._handle_rpc_msg(http_request, raw_msg) def client_connected(self): # TODO This is poor check. Add check it is client from TVPaint From fdcdf90da73738fea36a5aa60832c2467517bd03 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 19 Oct 2022 10:41:26 +0100 Subject: [PATCH 07/55] Unreal host installs correctly OpenPype --- openpype/hosts/unreal/api/launch_script.py | 6 + openpype/hosts/unreal/api/pipeline.py | 277 +++++++++--------- openpype/hosts/unreal/api/tools_ui.py | 246 ++++++++-------- .../UE_5.0/Content/Python/init_unreal.py | 51 ++-- .../UE_5.0/Source/OpenPype/OpenPype.Build.cs | 2 + .../Source/OpenPype/Private/OpenPype.cpp | 99 ++----- .../OpenPype/Private/OpenPypeCommands.cpp | 1 + .../Private/OpenPypeCommunication.cpp | 129 ++++++++ .../Source/OpenPype/Private/OpenPypeStyle.cpp | 3 +- .../UE_5.0/Source/OpenPype/Public/OpenPype.h | 6 +- .../Source/OpenPype/Public/OpenPypeCommands.h | 1 + .../OpenPype/Public/OpenPypeCommunication.h | 99 +++++++ .../unreal/remote/communication_server.py | 8 +- 13 files changed, 567 insertions(+), 361 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h diff --git a/openpype/hosts/unreal/api/launch_script.py b/openpype/hosts/unreal/api/launch_script.py index 8d3467a2c93..6e85eb0881b 100644 --- a/openpype/hosts/unreal/api/launch_script.py +++ b/openpype/hosts/unreal/api/launch_script.py @@ -12,6 +12,9 @@ from Qt import QtWidgets, QtCore, QtGui from openpype import style +from openpype.pipeline import install_host +from openpype.hosts.unreal.api import UnrealHost +# from openpype.hosts.unreal import api as unreal_host from openpype.hosts.unreal.remote.communication_server import ( CommunicationWrapper ) @@ -29,6 +32,9 @@ def main(launch_args): # - QApplicaiton is also main thread/event loop of the server qt_app = QtWidgets.QApplication([]) + unreal_host = UnrealHost() + install_host(unreal_host) + # Create Communicator object and trigger launch # - this must be done before anything is processed communicator = CommunicationWrapper.create_qt_communicator(qt_app) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index d396b64072c..535bebcbb2f 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -16,7 +16,7 @@ import openpype.hosts.unreal from openpype.host import HostBase, ILoadHost -import unreal # noqa +# import unreal # noqa logger = logging.getLogger("openpype.hosts.unreal") @@ -110,20 +110,20 @@ def ls(): metadata from them. Adding `objectName` to set. """ - ar = unreal.AssetRegistryHelpers.get_asset_registry() - openpype_containers = ar.get_assets_by_class("AssetContainer", True) + # ar = unreal.AssetRegistryHelpers.get_asset_registry() + # openpype_containers = ar.get_assets_by_class("AssetContainer", 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 openpype_containers: - asset = asset_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset_data.asset_name - data = cast_map_to_str_dict(data) + # # 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 openpype_containers: + # asset = asset_data.get_asset() + # data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + # data["objectName"] = asset_data.asset_name + # data = cast_map_to_str_dict(data) - yield data + # yield data def parse_container(container): @@ -135,12 +135,12 @@ def parse_container(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) + # 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 + # return data def publish(): @@ -168,28 +168,28 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): `Material /Game/OpenPype/Test/TestMaterial.TestMaterial` """ - # 1 - create directory for container - root = "/Game" - container_name = "{}{}".format(name, suffix) - new_name = move_assets_to_path(root, container_name, nodes) - - # 2 - create Asset Container there - path = "{}/{}".format(root, new_name) - create_container(container=container_name, path=path) - - namespace = path - - data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "name": new_name, - "namespace": namespace, - "loader": str(loader), - "representation": context["representation"]["_id"], - } - # 3 - imprint data - imprint("{}/{}".format(path, container_name), data) - return path + # # 1 - create directory for container + # root = "/Game" + # container_name = "{}{}".format(name, suffix) + # new_name = move_assets_to_path(root, container_name, nodes) + + # # 2 - create Asset Container there + # path = "{}/{}".format(root, new_name) + # create_container(container=container_name, path=path) + + # namespace = path + + # data = { + # "schema": "openpype:container-2.0", + # "id": AVALON_CONTAINER_ID, + # "name": new_name, + # "namespace": namespace, + # "loader": str(loader), + # "representation": context["representation"]["_id"], + # } + # # 3 - imprint data + # imprint("{}/{}".format(path, container_name), data) + # return path def instantiate(root, name, data, assets=None, suffix="_INS"): @@ -210,36 +210,37 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): suffix (str): suffix string to append to instance name """ - container_name = "{}{}".format(name, suffix) + # container_name = "{}{}".format(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) + # # 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 = "{}/{}".format(root, new_name) - create_publish_instance(instance=container_name, path=path) + # path = "{}/{}".format(root, new_name) + # create_publish_instance(instance=container_name, path=path) - imprint("{}/{}".format(path, container_name), data) + # imprint("{}/{}".format(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("OpenPype containerising"): - unreal.EditorAssetLibrary.save_asset(node) + pass + # 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("OpenPype containerising"): + # unreal.EditorAssetLibrary.save_asset(node) def show_tools_popup(): @@ -302,17 +303,17 @@ def create_folder(root: str, name: str) -> str: /Game/Foo1 """ - eal = unreal.EditorAssetLibrary - index = 1 - while True: - if eal.does_directory_exist("{}/{}".format(root, name)): - name = "{}{}".format(name, index) - index += 1 - else: - eal.make_directory("{}/{}".format(root, name)) - break + # eal = unreal.EditorAssetLibrary + # index = 1 + # while True: + # if eal.does_directory_exist("{}/{}".format(root, name)): + # name = "{}{}".format(name, index) + # index += 1 + # else: + # eal.make_directory("{}/{}".format(root, name)) + # break - return name + # return name def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: @@ -336,73 +337,73 @@ def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: NewTest """ - eal = unreal.EditorAssetLibrary - name = create_folder(root, name) + # eal = unreal.EditorAssetLibrary + # name = create_folder(root, name) - unreal.log(assets) - for asset in assets: - loaded = eal.load_asset(asset) - eal.rename_asset( - asset, "{}/{}/{}".format(root, name, loaded.get_name()) - ) + # unreal.log(assets) + # for asset in assets: + # loaded = eal.load_asset(asset) + # eal.rename_asset( + # asset, "{}/{}/{}".format(root, name, loaded.get_name()) + # ) - return name + # return name -def create_container(container: str, path: str) -> unreal.Object: - """Helper function to create Asset Container class on given path. +# 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. +# 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 +# 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 +# Returns: +# :class:`unreal.Object`: instance of created asset - Example: +# Example: - create_container( - "/Game/modelingFooCharacter_CON", - "modelingFooCharacter_CON" - ) +# create_container( +# "/Game/modelingFooCharacter_CON", +# "modelingFooCharacter_CON" +# ) - """ - factory = unreal.AssetContainerFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() +# """ +# factory = unreal.AssetContainerFactory() +# tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(container, path, None, factory) - return asset +# asset = tools.create_asset(container, path, None, factory) +# return asset -def create_publish_instance(instance: str, path: str) -> unreal.Object: - """Helper function to create OpenPype Publish Instance on given path. +# def create_publish_instance(instance: str, path: str) -> unreal.Object: +# """Helper function to create OpenPype Publish Instance on given path. - This behaves similarly as :func:`create_openpype_container`. +# This behaves similarly as :func:`create_openpype_container`. - Args: - path (str): Path where to create Publish Instance. - This path should point into container folder - instance (str): Publish Instance name +# 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 +# Returns: +# :class:`unreal.Object`: instance of created asset - Example: +# Example: - create_publish_instance( - "/Game/modelingFooCharacter_INST", - "modelingFooCharacter_INST" - ) +# create_publish_instance( +# "/Game/modelingFooCharacter_INST", +# "modelingFooCharacter_INST" +# ) - """ - factory = unreal.OpenPypePublishInstanceFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(instance, path, None, factory) - return asset +# """ +# factory = unreal.OpenPypePublishInstanceFactory() +# tools = unreal.AssetToolsHelpers().get_asset_tools() +# asset = tools.create_asset(instance, path, None, factory) +# return asset def cast_map_to_str_dict(umap) -> dict: @@ -422,22 +423,22 @@ def cast_map_to_str_dict(umap) -> dict: return {str(key): str(value) for (key, value) in umap.items()} -def get_subsequences(sequence: unreal.LevelSequence): - """Get list of subsequences from sequence. +# def get_subsequences(sequence: unreal.LevelSequence): +# """Get list of subsequences from sequence. - Args: - sequence (unreal.LevelSequence): Sequence +# Args: +# sequence (unreal.LevelSequence): Sequence - Returns: - list(unreal.LevelSequence): List of subsequences +# Returns: +# list(unreal.LevelSequence): List of subsequences - """ - tracks = sequence.get_master_tracks() - subscene_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - break - if subscene_track is not None and subscene_track.get_sections(): - return subscene_track.get_sections() - return [] +# """ +# tracks = sequence.get_master_tracks() +# subscene_track = None +# for t in tracks: +# if t.get_class() == unreal.MovieSceneSubTrack.static_class(): +# subscene_track = t +# break +# if subscene_track is not None and subscene_track.get_sections(): +# return subscene_track.get_sections() +# return [] diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py index 2500f8495f6..554d4497b96 100644 --- a/openpype/hosts/unreal/api/tools_ui.py +++ b/openpype/hosts/unreal/api/tools_ui.py @@ -1,5 +1,5 @@ import sys -from Qt import QtWidgets, QtCore, QtGui +# from Qt import QtWidgets, QtCore, QtGui from openpype import ( resources, @@ -10,156 +10,156 @@ from openpype.hosts.unreal.api import rendering -class ToolsBtnsWidget(QtWidgets.QWidget): - """Widget containing buttons which are clickable.""" - tool_required = QtCore.Signal(str) +# 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) +# def __init__(self, parent=None): +# super(ToolsBtnsWidget, self).__init__(parent) - create_btn = QtWidgets.QPushButton("Create...", self) - load_btn = QtWidgets.QPushButton("Load...", self) - publish_btn = QtWidgets.QPushButton("Publish...", self) - manage_btn = QtWidgets.QPushButton("Manage...", self) - render_btn = QtWidgets.QPushButton("Render...", self) - experimental_tools_btn = QtWidgets.QPushButton( - "Experimental tools...", self - ) +# create_btn = QtWidgets.QPushButton("Create...", self) +# load_btn = QtWidgets.QPushButton("Load...", self) +# publish_btn = QtWidgets.QPushButton("Publish...", 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(create_btn, 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) +# layout = QtWidgets.QVBoxLayout(self) +# layout.setContentsMargins(0, 0, 0, 0) +# layout.addWidget(create_btn, 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) - create_btn.clicked.connect(self._on_create) - 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) +# create_btn.clicked.connect(self._on_create) +# 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_create(self): +# self.tool_required.emit("creator") - def _on_load(self): - self.tool_required.emit("loader") - - def _on_publish(self): - self.tool_required.emit("publish") - - def _on_manage(self): - self.tool_required.emit("sceneinventory") - - def _on_render(self): - rendering.start_rendering() +# def _on_load(self): +# self.tool_required.emit("loader") + +# def _on_publish(self): +# self.tool_required.emit("publish") + +# 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") +# 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) +# 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("OpenPype tools") - icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) - self.setWindowIcon(icon) +# self.setWindowTitle("OpenPype tools") +# icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) +# self.setWindowIcon(icon) - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.WindowStaysOnTopHint - ) - self.setFocusPolicy(QtCore.Qt.StrongFocus) +# self.setWindowFlags( +# QtCore.Qt.Window +# | QtCore.Qt.WindowStaysOnTopHint +# ) +# self.setFocusPolicy(QtCore.Qt.StrongFocus) - tools_widget = ToolsBtnsWidget(self) +# tools_widget = ToolsBtnsWidget(self) - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(tools_widget) +# layout = QtWidgets.QVBoxLayout(self) +# layout.addWidget(tools_widget) - tools_widget.tool_required.connect(self._on_tool_require) - self._tools_widget = tools_widget +# tools_widget.tool_required.connect(self._on_tool_require) +# self._tools_widget = tools_widget - self._first_show = True +# self._first_show = True - def sizeHint(self): - result = super(ToolsDialog, self).sizeHint() - result.setWidth(result.width() * 2) - return result +# 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 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) +# 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) +# 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 +# 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 exists yet.""" - if not cls._first_show: - return +# @classmethod +# def _before_show(cls): +# """Create QApplication if does not exists yet.""" +# if not cls._first_show: +# return - cls._first_show = False - if not QtWidgets.QApplication.instance(): - QtWidgets.QApplication(sys.argv) +# 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() +# @classmethod +# def show_popup(cls): +# cls._before_show() +# with qt_app_context(): +# if cls._popup is None: +# cls._popup = ToolsPopup() - cls._popup.show() +# cls._popup.show() - @classmethod - def show_dialog(cls): - cls._before_show() - with qt_app_context(): - if cls._dialog is None: - cls._dialog = ToolsDialog() +# @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() +# cls._dialog.show() +# cls._dialog.raise_() +# cls._dialog.activateWindow() -def show_tools_popup(): - WindowCache.show_popup() +# def show_tools_popup(): +# WindowCache.show_popup() -def show_tools_dialog(): - WindowCache.show_dialog() +# def show_tools_dialog(): +# WindowCache.show_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index b85f9706992..4a580a4f943 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -1,30 +1,33 @@ -import unreal +# import traceback -openpype_detected = True -try: - from openpype.pipeline import install_host - from openpype.hosts.unreal.api import UnrealHost +# import unreal - openpype_host = UnrealHost() -except ImportError as exc: - openpype_host = None - openpype_detected = False - unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) +# openpype_detected = True +# try: +# from openpype.pipeline import install_host +# from openpype.hosts.unreal.api import UnrealHost -if openpype_detected: - install_host(openpype_host) +# openpype_host = UnrealHost() +# except ImportError as exc: +# openpype_host = None +# openpype_detected = False +# # unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) +# unreal.log_error(traceback.format_exc()) +# if openpype_detected: +# install_host(openpype_host) -@unreal.uclass() -class OpenPypeIntegration(unreal.OpenPypePythonBridge): - @unreal.ufunction(override=True) - def RunInPython_Popup(self): - unreal.log_warning("OpenPype: showing tools popup") - if openpype_detected: - openpype_host.show_tools_popup() - @unreal.ufunction(override=True) - def RunInPython_Dialog(self): - unreal.log_warning("OpenPype: showing tools dialog") - if openpype_detected: - openpype_host.show_tools_dialog() +# @unreal.uclass() +# class OpenPypeIntegration(unreal.OpenPypePythonBridge): +# @unreal.ufunction(override=True) +# def RunInPython_Popup(self): +# unreal.log_warning("OpenPype: showing tools popup") +# if openpype_detected: +# openpype_host.show_tools_popup() + +# @unreal.ufunction(override=True) +# def RunInPython_Dialog(self): +# unreal.log_warning("OpenPype: showing tools dialog") +# if openpype_detected: +# openpype_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs index 88eaa74bb4f..c26f2713b5f 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs @@ -45,6 +45,8 @@ public OpenPype(ReadOnlyTargetRules Target) : base(Target) "Engine", "Slate", "SlateCore", + "Json", + "JsonUtilities", // ... add private dependencies that you statically link with here ... } ); diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp index 2654974a335..c6751b532b1 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp @@ -1,12 +1,11 @@ #include "OpenPype.h" #include "OpenPypeStyle.h" #include "OpenPypeCommands.h" +#include "OpenPypeCommunication.h" #include "OpenPypePythonBridge.h" #include "LevelEditor.h" #include "Misc/MessageDialog.h" #include "ToolMenus.h" -#include "GenericPlatform/GenericPlatformMisc.h" -#include "WebSocketsModule.h" // Module definition static const FName OpenPypeTabName("OpenPype"); @@ -27,25 +26,18 @@ void FOpenPypeModule::StartupModule() UE_LOG(LogTemp, Warning, TEXT("OpenPype Plugin Started")); - CreateSocket(); - ConnectToSocket(); + FOpenPypeCommunication::CreateSocket(); + FOpenPypeCommunication::ConnectToSocket(); - PluginCommands = MakeShareable(new FUICommandList); - - PluginCommands->MapAction( - FOpenPypeCommands::Get().OpenPypeTools, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), - FCanExecuteAction()); - PluginCommands->MapAction( - FOpenPypeCommands::Get().OpenPypeToolsDialog, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog), - FCanExecuteAction()); + MapCommands(); UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus)); } void FOpenPypeModule::ShutdownModule() { + FOpenPypeCommunication::CloseConnection(); + UToolMenus::UnRegisterStartupCallback(this); UToolMenus::UnregisterOwner(this); @@ -71,79 +63,52 @@ void FOpenPypeModule::RegisterMenus() ); Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTools, PluginCommands); Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeToolsDialog, PluginCommands); + Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTestMethod, PluginCommands); } UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); { FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); { - FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools)); + FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTestMethod)); Entry.SetCommandList(PluginCommands); } } } } +void FOpenPypeModule::MapCommands() +{ + PluginCommands = MakeShareable(new FUICommandList); + + PluginCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeTools, + FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), + FCanExecuteAction()); + PluginCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeToolsDialog, + FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog), + FCanExecuteAction()); + PluginCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeTestMethod, + FExecuteAction::CreateRaw(this, &FOpenPypeModule::TestMethod), + FCanExecuteAction()); +} -void FOpenPypeModule::MenuPopup() { +void FOpenPypeModule::MenuPopup() +{ UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); bridge->RunInPython_Popup(); } -void FOpenPypeModule::MenuDialog() { +void FOpenPypeModule::MenuDialog() +{ UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); bridge->RunInPython_Dialog(); } -void FOpenPypeModule::CreateSocket() { - UE_LOG(LogTemp, Warning, TEXT("Starting web socket...")); - - FString url = FWindowsPlatformMisc::GetEnvironmentVariable(*FString("WEBSOCKET_URL")); - - UE_LOG(LogTemp, Warning, TEXT("Websocket URL: %s"), *url); - - const FString ServerURL = url; // Your server URL. You can use ws, wss or wss+insecure. - const FString ServerProtocol = TEXT("ws"); // The WebServer protocol you want to use. - - TMap UpgradeHeaders; - UpgradeHeaders.Add(TEXT("upgrade"), TEXT("websocket")); - - Socket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol, UpgradeHeaders); -} - -void FOpenPypeModule::ConnectToSocket() { - // We bind all available events - Socket->OnConnected().AddLambda([]() -> void { - // This code will run once connected. - UE_LOG(LogTemp, Warning, TEXT("Connected")); - }); - - Socket->OnConnectionError().AddLambda([](const FString & Error) -> void { - // This code will run if the connection failed. Check Error to see what happened. - UE_LOG(LogTemp, Warning, TEXT("Error during connection")); - UE_LOG(LogTemp, Warning, TEXT("%s"), *Error); - }); - - Socket->OnClosed().AddLambda([](int32 StatusCode, const FString& Reason, bool bWasClean) -> void { - // This code will run when the connection to the server has been terminated. - // Because of an error or a call to Socket->Close(). - }); - - Socket->OnMessage().AddLambda([](const FString & Message) -> void { - // This code will run when we receive a string message from the server. - }); - - Socket->OnRawMessage().AddLambda([](const void* Data, SIZE_T Size, SIZE_T BytesRemaining) -> void { - // This code will run when we receive a raw (binary) message from the server. - }); - - Socket->OnMessageSent().AddLambda([](const FString& MessageString) -> void { - // This code is called after we sent a message to the server. - }); - - UE_LOG(LogTemp, Warning, TEXT("Connecting web socket to server...")); - - // And we finally connect to the server. - Socket->Connect(); +void FOpenPypeModule::TestMethod() +{ + FOpenPypeCommunication::CallMethod("loader_tool", TArray()); } IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp index 6187bd7c7ea..3932647dc1d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp @@ -8,6 +8,7 @@ void FOpenPypeCommands::RegisterCommands() { UI_COMMAND(OpenPypeTools, "OpenPype Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(OpenPypeToolsDialog, "OpenPype Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(OpenPypeTestMethod, "OpenPype Test Method", "", EUserInterfaceActionType::Button, FInputChord()); } #undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp new file mode 100644 index 00000000000..7f9b132cb6e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -0,0 +1,129 @@ +#include "OpenPypeCommunication.h" +#include "OpenPype.h" +#include "GenericPlatform/GenericPlatformMisc.h" +#include "WebSocketsModule.h" +#include "JsonObjectConverter.h" + + +// Initialize static attributes +TSharedPtr FOpenPypeCommunication::Socket = nullptr; +TArray FOpenPypeCommunication::RpcResponses = TArray(); +int32 FOpenPypeCommunication::Id = 0; + +void FOpenPypeCommunication::CreateSocket() +{ + UE_LOG(LogTemp, Display, TEXT("Starting web socket...")); + + FString url = FWindowsPlatformMisc::GetEnvironmentVariable(*FString("WEBSOCKET_URL")); + + UE_LOG(LogTemp, Display, TEXT("Websocket URL: %s"), *url); + + const FString ServerURL = url; // Your server URL. You can use ws, wss or wss+insecure. + const FString ServerProtocol = TEXT("ws"); // The WebServer protocol you want to use. + + TMap UpgradeHeaders; + UpgradeHeaders.Add(TEXT("upgrade"), TEXT("websocket")); + + Id = 0; + Socket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol, UpgradeHeaders); +} + +void FOpenPypeCommunication::ConnectToSocket() +{ + Socket->OnConnected().AddStatic(&FOpenPypeCommunication::OnConnected); + Socket->OnConnectionError().AddStatic(&FOpenPypeCommunication::OnConnectionError); + Socket->OnClosed().AddStatic(&FOpenPypeCommunication::OnClosed); + Socket->OnMessage().AddStatic(&FOpenPypeCommunication::OnMessage); + Socket->OnRawMessage().AddStatic(&FOpenPypeCommunication::OnRawMessage); + Socket->OnMessageSent().AddStatic(&FOpenPypeCommunication::OnMessageSent); + + UE_LOG(LogTemp, Display, TEXT("Connecting web socket to server...")); + + Socket->Connect(); +} + +void FOpenPypeCommunication::CloseConnection() +{ + Socket->Close(); +} + +bool FOpenPypeCommunication::IsConnected() +{ + return Socket->IsConnected(); +} + +void FOpenPypeCommunication::CallMethod(FString Method, TArray Params) +{ + if (Socket->IsConnected()) + { + UE_LOG(LogTemp, Display, TEXT("Calling method \"%s\"..."), *Method); + + int32 newId = Id++; + + FString Message; + FRpcCall RpcCall = { "2.0", Method, Params, newId }; + FJsonObjectConverter::UStructToJsonObjectString(RpcCall, Message); + + Socket->Send(Message); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Error calling method \"%s\"..."), *Method); + } +} + +void FOpenPypeCommunication::OnConnected() +{ + // This code will run once connected. + UE_LOG(LogTemp, Warning, TEXT("Connected")); +} + +void FOpenPypeCommunication::OnConnectionError(const FString & Error) +{ + // This code will run if the connection failed. Check Error to see what happened. + UE_LOG(LogTemp, Error, TEXT("Error during connection")); + UE_LOG(LogTemp, Error, TEXT("%s"), *Error); +} + +void FOpenPypeCommunication::OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean) +{ + // This code will run when the connection to the server has been terminated. + // Because of an error or a call to Socket->Close(). + UE_LOG(LogTemp, Warning, TEXT("Closed")); +} + +void FOpenPypeCommunication::OnMessage(const FString & Message) +{ + // This code will run when we receive a string message from the server. + UE_LOG(LogTemp, Display, TEXT("Message received: %s"), *Message); + + FRpcResponse RpcResponse; + + if(!FJsonObjectConverter::JsonObjectStringToUStruct(Message, &RpcResponse, 0, 0)) { + UE_LOG(LogTemp, Error, TEXT("Error during parsing message")); + return; + } + + RpcResponses.Add(RpcResponse); + + if (RpcResponse.error.code != 0) + { + UE_LOG(LogTemp, Error, TEXT("Error during calling method \"%s\""), *RpcResponse.error.message); + } + else + { + UE_LOG(LogTemp, Display, TEXT("Message parsed: %s"), *RpcResponse.result); + } +} + +void FOpenPypeCommunication::OnRawMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining) +{ + // This code will run when we receive a raw (binary) message from the server. + UE_LOG(LogTemp, Display, TEXT("Raw message received")); +} + +void FOpenPypeCommunication::OnMessageSent(const FString& MessageString) +{ + // This code is called after we sent a message to the server. + UE_LOG(LogTemp, Display, TEXT("Message sent: %s"), *MessageString); +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp index 4a53af26b55..1a886537a29 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp @@ -1,5 +1,5 @@ -#include "OpenPype.h" #include "OpenPypeStyle.h" +#include "OpenPype.h" #include "Framework/Application/SlateApplication.h" #include "Styling/SlateStyleRegistry.h" #include "Slate/SlateGameResources.h" @@ -43,6 +43,7 @@ TSharedRef< FSlateStyleSet > FOpenPypeStyle::Create() Style->Set("OpenPype.OpenPypeTools", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); Style->Set("OpenPype.OpenPypeToolsDialog", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); + Style->Set("OpenPype.OpenPypeTestMethod", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); return Style; } diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h index aa4cb468ca6..d0833090e51 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h @@ -16,15 +16,13 @@ class FOpenPypeModule : public IModuleInterface private: void RegisterMenus(); + void MapCommands(); void MenuPopup(); void MenuDialog(); - void CreateSocket(); - void ConnectToSocket(); + void TestMethod(); private: TSharedPtr PluginCommands; - - TSharedPtr Socket; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h index 62ffb8de33e..a963199badd 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h @@ -21,4 +21,5 @@ class FOpenPypeCommands : public TCommands public: TSharedPtr< FUICommandInfo > OpenPypeTools; TSharedPtr< FUICommandInfo > OpenPypeToolsDialog; + TSharedPtr< FUICommandInfo > OpenPypeTestMethod; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h new file mode 100644 index 00000000000..3c6dd8e8156 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h @@ -0,0 +1,99 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "IWebSocket.h" // Socket definition + +#include "OpenPypeCommunication.generated.h" + + +USTRUCT() +struct FRpcCall +{ + GENERATED_BODY() + +public: + UPROPERTY() + FString jsonrpc; + + UPROPERTY() + FString method; + + UPROPERTY() + TArray params; + + UPROPERTY() + int32 id; +}; + +USTRUCT() +struct FRpcError +{ + GENERATED_BODY() + +public: + UPROPERTY() + int32 code; + + UPROPERTY() + FString message; + + UPROPERTY() + FString data; +}; + +USTRUCT() +struct FRpcResponse +{ + GENERATED_BODY() + +public: + UPROPERTY() + FString jsonrpc; + + UPROPERTY() + FString result; + + UPROPERTY() + struct FRpcError error; + + UPROPERTY() + int32 id; +}; + +class FOpenPypeCommunication +{ +public: + static void CreateSocket(); + static void ConnectToSocket(); + static void CloseConnection(); + + static bool IsConnected(); + + static void CallMethod(FString, TArray); + +public: + UFUNCTION() + static void OnConnected(); + + UFUNCTION() + static void OnConnectionError(const FString & Error); + + UFUNCTION() + static void OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean); + + UFUNCTION() + static void OnMessage(const FString & Message); + + UFUNCTION() + static void OnRawMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining); + + UFUNCTION() + static void OnMessageSent(const FString& MessageString); + +private: + static TSharedPtr Socket; + static TArray RpcResponses; + static int32 Id; +}; diff --git a/openpype/hosts/unreal/remote/communication_server.py b/openpype/hosts/unreal/remote/communication_server.py index 43b8b815355..54f4ce9ed7d 100644 --- a/openpype/hosts/unreal/remote/communication_server.py +++ b/openpype/hosts/unreal/remote/communication_server.py @@ -197,7 +197,7 @@ async def check_shutdown(self): self.loop.stop() -class BaseTVPaintRpc(JsonRpc): +class BaseUnrealRpc(JsonRpc): def __init__(self, communication_obj, route_name="", **kwargs): super().__init__(**kwargs) self.requests_ids = collections.defaultdict(lambda: 0) @@ -296,7 +296,7 @@ def send_request(self, client, method, params=None, timeout=0): return result -class QtTVPaintRpc(BaseTVPaintRpc): +class QtUnrealRpc(BaseUnrealRpc): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -493,7 +493,7 @@ def _launch_unreal(self, launch_args): self.process = subprocess.Popen(launch_args, **kwargs) def _create_routes(self): - self.websocket_rpc = BaseTVPaintRpc( + self.websocket_rpc = BaseUnrealRpc( self, loop=self.websocket_server.loop ) self.websocket_server.add_route( @@ -664,7 +664,7 @@ def __init__(self, qt_app): self.qt_app = qt_app def _create_routes(self): - self.websocket_rpc = QtTVPaintRpc( + self.websocket_rpc = QtUnrealRpc( self, loop=self.websocket_server.loop ) self.websocket_server.add_route( From 76baba7ca71bbf8d7508ca4bbff7a74475d168e7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 20 Oct 2022 11:01:08 +0100 Subject: [PATCH 08/55] Added OpenPype Menu --- .../Source/OpenPype/Private/OpenPype.cpp | 104 ++++++++++-------- .../OpenPype/Private/OpenPypeCommands.cpp | 9 +- .../Private/OpenPypeCommunication.cpp | 8 +- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 4 +- .../UE_5.0/Source/OpenPype/Public/OpenPype.h | 18 +-- .../Source/OpenPype/Public/OpenPypeCommands.h | 7 +- .../OpenPype/Public/OpenPypeCommunication.h | 2 +- 7 files changed, 81 insertions(+), 71 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp index c6751b532b1..5b4e6a52740 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp @@ -5,12 +5,13 @@ #include "OpenPypePythonBridge.h" #include "LevelEditor.h" #include "Misc/MessageDialog.h" +#include "LevelEditorMenuContext.h" #include "ToolMenus.h" static const FName OpenPypeTabName("OpenPype"); -#define LOCTEXT_NAMESPACE "FOpenPypeModule" +#define LOCTEXT_NAMESPACE "OpenPypeModule" // This function is triggered when the plugin is staring up void FOpenPypeModule::StartupModule() @@ -47,68 +48,75 @@ void FOpenPypeModule::ShutdownModule() FOpenPypeCommands::Unregister(); } +TSharedRef FOpenPypeModule::GenerateOpenPypeMenuContent(TSharedRef InCommandList) +{ + FToolMenuContext MenuContext(InCommandList); + + return UToolMenus::Get()->GenerateWidget("LevelEditor.LevelEditorToolBar.OpenPype", MenuContext); +} + +void FOpenPypeModule::CallMethod(const FString MethodName, const TArray Args) +{ + FOpenPypeCommunication::CallMethod(MethodName, Args); +} + void FOpenPypeModule::RegisterMenus() { // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner FToolMenuOwnerScoped OwnerScoped(this); + RegisterOpenPypeMenu(); + + UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.User"); + + FToolMenuSection& Section = ToolbarMenu->AddSection("OpenPype"); + + FToolMenuEntry OpenPypeEntry = FToolMenuEntry::InitComboButton( + "OpenPype Menu", + FUIAction(), + FOnGetContent::CreateStatic(&FOpenPypeModule::GenerateOpenPypeMenuContent, OpenPypeCommands.ToSharedRef()), + LOCTEXT("OpenPypeMenu_Label", "OpenPype"), + LOCTEXT("OpenPypeMenu_Tooltip", "Open OpenPype Menu"), + FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.OpenPypeMenu") + ); + Section.AddEntry(OpenPypeEntry); +} + +void FOpenPypeModule::RegisterOpenPypeMenu() +{ + UToolMenu* OpenPypeMenu = UToolMenus::Get()->RegisterMenu("LevelEditor.LevelEditorToolBar.OpenPype"); { - UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); - { - // FToolMenuSection& Section = Menu->FindOrAddSection("OpenPype"); - FToolMenuSection& Section = Menu->AddSection( - "OpenPype", - TAttribute(FText::FromString("OpenPype")), - FToolMenuInsert("Programming", EToolMenuInsertType::Before) - ); - Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTools, PluginCommands); - Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeToolsDialog, PluginCommands); - Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTestMethod, PluginCommands); - } - UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); - { - FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); - { - FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTestMethod)); - Entry.SetCommandList(PluginCommands); - } - } + FToolMenuSection& Section = OpenPypeMenu->AddSection("OpenPype"); + + Section.InitSection("OpenPype", LOCTEXT("OpenPype_Label", "OpenPype"), FToolMenuInsert(NAME_None, EToolMenuInsertType::First)); + + Section.AddMenuEntry(FOpenPypeCommands::Get().OpenPypeLoaderTool); + Section.AddMenuEntry(FOpenPypeCommands::Get().OpenPypeCreatorTool); + Section.AddMenuEntry(FOpenPypeCommands::Get().OpenPypeSceneInventoryTool); + Section.AddMenuEntry(FOpenPypeCommands::Get().OpenPypePublishTool); } } void FOpenPypeModule::MapCommands() { - PluginCommands = MakeShareable(new FUICommandList); + OpenPypeCommands = MakeShareable(new FUICommandList); - PluginCommands->MapAction( - FOpenPypeCommands::Get().OpenPypeTools, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), + OpenPypeCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeLoaderTool, + FExecuteAction::CreateStatic(&FOpenPypeModule::CallMethod, FString("loader_tool"), TArray()), FCanExecuteAction()); - PluginCommands->MapAction( - FOpenPypeCommands::Get().OpenPypeToolsDialog, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog), + OpenPypeCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeCreatorTool, + FExecuteAction::CreateStatic(&FOpenPypeModule::CallMethod, FString("creator_tool"), TArray()), FCanExecuteAction()); - PluginCommands->MapAction( - FOpenPypeCommands::Get().OpenPypeTestMethod, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::TestMethod), + OpenPypeCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeSceneInventoryTool, + FExecuteAction::CreateStatic(&FOpenPypeModule::CallMethod, FString("scene_inventory_tool"), TArray()), + FCanExecuteAction()); + OpenPypeCommands->MapAction( + FOpenPypeCommands::Get().OpenPypePublishTool, + FExecuteAction::CreateStatic(&FOpenPypeModule::CallMethod, FString("publish_tool"), TArray()), FCanExecuteAction()); -} - -void FOpenPypeModule::MenuPopup() -{ - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FOpenPypeModule::MenuDialog() -{ - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -void FOpenPypeModule::TestMethod() -{ - FOpenPypeCommunication::CallMethod("loader_tool", TArray()); } IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp index 3932647dc1d..06012d196cb 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp @@ -2,13 +2,14 @@ #include "OpenPypeCommands.h" -#define LOCTEXT_NAMESPACE "FOpenPypeModule" +#define LOCTEXT_NAMESPACE "OpenPypeModule" void FOpenPypeCommands::RegisterCommands() { - UI_COMMAND(OpenPypeTools, "OpenPype Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(OpenPypeToolsDialog, "OpenPype Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(OpenPypeTestMethod, "OpenPype Test Method", "", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(OpenPypeLoaderTool, "Load", "Open loader tool", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(OpenPypeCreatorTool, "Create", "Open creator tool", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(OpenPypeSceneInventoryTool, "Scene inventory", "Open scene inventory tool", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(OpenPypePublishTool, "Publish", "Open publisher", EUserInterfaceActionType::Button, FInputChord()); } #undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp index 7f9b132cb6e..c19546ebb5f 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -18,8 +18,8 @@ void FOpenPypeCommunication::CreateSocket() UE_LOG(LogTemp, Display, TEXT("Websocket URL: %s"), *url); - const FString ServerURL = url; // Your server URL. You can use ws, wss or wss+insecure. - const FString ServerProtocol = TEXT("ws"); // The WebServer protocol you want to use. + const FString ServerURL = url; + const FString ServerProtocol = TEXT("ws"); TMap UpgradeHeaders; UpgradeHeaders.Add(TEXT("upgrade"), TEXT("websocket")); @@ -52,7 +52,7 @@ bool FOpenPypeCommunication::IsConnected() return Socket->IsConnected(); } -void FOpenPypeCommunication::CallMethod(FString Method, TArray Params) +void FOpenPypeCommunication::CallMethod(const FString Method, const TArray Args) { if (Socket->IsConnected()) { @@ -61,7 +61,7 @@ void FOpenPypeCommunication::CallMethod(FString Method, TArray Params) int32 newId = Id++; FString Message; - FRpcCall RpcCall = { "2.0", Method, Params, newId }; + FRpcCall RpcCall = { "2.0", *Method, Args, newId }; FJsonObjectConverter::UStructToJsonObjectString(RpcCall, Message); Socket->Send(Message); diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp index 1a886537a29..c666b3b8332 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp @@ -41,9 +41,7 @@ TSharedRef< FSlateStyleSet > FOpenPypeStyle::Create() TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("OpenPypeStyle")); Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); - Style->Set("OpenPype.OpenPypeTools", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); - Style->Set("OpenPype.OpenPypeToolsDialog", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); - Style->Set("OpenPype.OpenPypeTestMethod", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); + Style->Set("OpenPype.OpenPypeMenu", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); return Style; } diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h index d0833090e51..beeb0c27e65 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h @@ -4,8 +4,9 @@ #include "CoreMinimal.h" #include "Modules/ModuleManager.h" - -#include "IWebSocket.h" // Socket definition +#include "Widgets/SWidget.h" +#include "Framework/Commands/UICommandList.h" +#include "IWebSocket.h" class FOpenPypeModule : public IModuleInterface @@ -14,15 +15,16 @@ class FOpenPypeModule : public IModuleInterface virtual void StartupModule() override; virtual void ShutdownModule() override; +protected: + static TSharedRef GenerateOpenPypeMenuContent(TSharedRef InCommandList); + + static void CallMethod(const FString MethodName, const TArray Args); + private: void RegisterMenus(); + void RegisterOpenPypeMenu(); void MapCommands(); - void MenuPopup(); - void MenuDialog(); - - void TestMethod(); - private: - TSharedPtr PluginCommands; + TSharedPtr OpenPypeCommands; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h index a963199badd..4c461a74294 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h @@ -19,7 +19,8 @@ class FOpenPypeCommands : public TCommands virtual void RegisterCommands() override; public: - TSharedPtr< FUICommandInfo > OpenPypeTools; - TSharedPtr< FUICommandInfo > OpenPypeToolsDialog; - TSharedPtr< FUICommandInfo > OpenPypeTestMethod; + TSharedPtr< FUICommandInfo > OpenPypeLoaderTool; + TSharedPtr< FUICommandInfo > OpenPypeCreatorTool; + TSharedPtr< FUICommandInfo > OpenPypeSceneInventoryTool; + TSharedPtr< FUICommandInfo > OpenPypePublishTool; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h index 3c6dd8e8156..c8926d7162f 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h @@ -71,7 +71,7 @@ class FOpenPypeCommunication static bool IsConnected(); - static void CallMethod(FString, TArray); + static void CallMethod(const FString Method, const TArray Args); public: UFUNCTION() From 6abd8a1a0cb063bc70a44da15b3399a653daf38a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 20 Oct 2022 16:59:46 +0100 Subject: [PATCH 09/55] Fixed Unreal incoming communication parsing --- .../Private/OpenPypeCommunication.cpp | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp index c19546ebb5f..3e5c2ab686b 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -2,6 +2,7 @@ #include "OpenPype.h" #include "GenericPlatform/GenericPlatformMisc.h" #include "WebSocketsModule.h" +#include "Json.h" #include "JsonObjectConverter.h" @@ -97,22 +98,46 @@ void FOpenPypeCommunication::OnMessage(const FString & Message) // This code will run when we receive a string message from the server. UE_LOG(LogTemp, Display, TEXT("Message received: %s"), *Message); - FRpcResponse RpcResponse; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(Message); + TSharedPtr Root; - if(!FJsonObjectConverter::JsonObjectStringToUStruct(Message, &RpcResponse, 0, 0)) { - UE_LOG(LogTemp, Error, TEXT("Error during parsing message")); - return; - } - - RpcResponses.Add(RpcResponse); - - if (RpcResponse.error.code != 0) + if (FJsonSerializer::Deserialize(Reader, Root)) { - UE_LOG(LogTemp, Error, TEXT("Error during calling method \"%s\""), *RpcResponse.error.message); + if (Root->HasField(TEXT("result"))) + { + FString OutputMessage; + if (Root->TryGetStringField(TEXT("result"), OutputMessage)) + { + UE_LOG(LogTemp, Display, TEXT("Result: %s"), *OutputMessage); + } + else + { + UE_LOG(LogTemp, Display, TEXT("Function call successful without return value")); + } + } + else if (Root->HasField(TEXT("error"))) + { + auto Error = Root->GetObjectField(TEXT("error")); + + if (Error->HasField(TEXT("message"))) + { + FString ErrorMessage; + Error->TryGetStringField(TEXT("message"), ErrorMessage); + UE_LOG(LogTemp, Error, TEXT("Error: %s"), *ErrorMessage); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Error during parsing error")); + } + } + else + { + UE_LOG(LogTemp, Error, TEXT("Error during parsing message")); + } } else { - UE_LOG(LogTemp, Display, TEXT("Message parsed: %s"), *RpcResponse.result); + UE_LOG(LogTemp, Error, TEXT("Error during deserialization")); } } From f0a3783136a9723e72b55734ebf4576b5c184a29 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 21 Oct 2022 16:21:40 +0100 Subject: [PATCH 10/55] Better implementation of communication --- openpype/hosts/unreal/api/__init__.py | 4 - .../{remote => api}/communication_server.py | 0 openpype/hosts/unreal/api/launch_script.py | 3 +- openpype/hosts/unreal/api/pipeline.py | 289 +---- openpype/hosts/unreal/api/tools_ui.py | 165 --- .../UE_5.0/Content/Python/init_unreal.py | 341 +++++- .../Private/OpenPypeCommunication.cpp | 73 +- .../OpenPype/Public/OpenPypeCommunication.h | 16 +- .../OpenPype/Public/OpenPypePythonBridge.h | 8 +- openpype/hosts/unreal/remote/__init__.py | 1 - .../hosts/unreal/remote/remote_execution.py | 639 ----------- openpype/hosts/unreal/remote/rpc/__init__.py | 6 - .../hosts/unreal/remote/rpc/base_server.py | 245 ----- openpype/hosts/unreal/remote/rpc/client.py | 103 -- .../hosts/unreal/remote/rpc/exceptions.py | 79 -- openpype/hosts/unreal/remote/rpc/factory.py | 319 ------ .../hosts/unreal/remote/rpc/unreal_server.py | 35 - .../hosts/unreal/remote/rpc/validations.py | 105 -- openpype/hosts/unreal/remote/unreal.py | 992 ------------------ 19 files changed, 411 insertions(+), 3012 deletions(-) rename openpype/hosts/unreal/{remote => api}/communication_server.py (100%) delete mode 100644 openpype/hosts/unreal/api/tools_ui.py delete mode 100644 openpype/hosts/unreal/remote/__init__.py delete mode 100644 openpype/hosts/unreal/remote/remote_execution.py delete mode 100644 openpype/hosts/unreal/remote/rpc/__init__.py delete mode 100644 openpype/hosts/unreal/remote/rpc/base_server.py delete mode 100644 openpype/hosts/unreal/remote/rpc/client.py delete mode 100644 openpype/hosts/unreal/remote/rpc/exceptions.py delete mode 100644 openpype/hosts/unreal/remote/rpc/factory.py delete mode 100644 openpype/hosts/unreal/remote/rpc/unreal_server.py delete mode 100644 openpype/hosts/unreal/remote/rpc/validations.py delete mode 100644 openpype/hosts/unreal/remote/unreal.py diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index 870982f5f9a..78eba9265b6 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -16,8 +16,6 @@ show_publisher, show_manager, show_experimental_tools, - show_tools_dialog, - show_tools_popup, instantiate, UnrealHost, ) @@ -35,8 +33,6 @@ "show_publisher", "show_manager", "show_experimental_tools", - "show_tools_dialog", - "show_tools_popup", "instantiate", "UnrealHost", ] diff --git a/openpype/hosts/unreal/remote/communication_server.py b/openpype/hosts/unreal/api/communication_server.py similarity index 100% rename from openpype/hosts/unreal/remote/communication_server.py rename to openpype/hosts/unreal/api/communication_server.py diff --git a/openpype/hosts/unreal/api/launch_script.py b/openpype/hosts/unreal/api/launch_script.py index 6e85eb0881b..a40a530b573 100644 --- a/openpype/hosts/unreal/api/launch_script.py +++ b/openpype/hosts/unreal/api/launch_script.py @@ -14,8 +14,7 @@ from openpype import style from openpype.pipeline import install_host from openpype.hosts.unreal.api import UnrealHost -# from openpype.hosts.unreal import api as unreal_host -from openpype.hosts.unreal.remote.communication_server import ( +from openpype.hosts.unreal.api.communication_server import ( CommunicationWrapper ) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 535bebcbb2f..fe7e64d5923 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import ast import os import logging from typing import List @@ -15,8 +16,9 @@ from openpype.tools.utils import host_tools import openpype.hosts.unreal from openpype.host import HostBase, ILoadHost - -# import unreal # noqa +from openpype.hosts.unreal.api.communication_server import ( + CommunicationWrapper +) logger = logging.getLogger("openpype.hosts.unreal") @@ -45,16 +47,6 @@ def install(self): def get_containers(self): return ls() - def show_tools_popup(self): - """Show tools popup with actions leading to show other tools.""" - - show_tools_popup() - - def show_tools_dialog(self): - """Show tools dialog with actions leading to show other tools.""" - - show_tools_dialog() - def install(): """Install Unreal configuration for OpenPype.""" @@ -110,37 +102,8 @@ def ls(): metadata from them. Adding `objectName` to set. """ - # ar = unreal.AssetRegistryHelpers.get_asset_registry() - # openpype_containers = ar.get_assets_by_class("AssetContainer", 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 openpype_containers: - # asset = asset_data.get_asset() - # data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - # data["objectName"] = asset_data.asset_name - # data = cast_map_to_str_dict(data) - - # yield data - - -def parse_container(container): - """To get data from container, AssetContainer 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 + communicator = CommunicationWrapper.communicator + return ast.literal_eval(communicator.send_request("ls")) def publish(): @@ -168,28 +131,9 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): `Material /Game/OpenPype/Test/TestMaterial.TestMaterial` """ - # # 1 - create directory for container - # root = "/Game" - # container_name = "{}{}".format(name, suffix) - # new_name = move_assets_to_path(root, container_name, nodes) - - # # 2 - create Asset Container there - # path = "{}/{}".format(root, new_name) - # create_container(container=container_name, path=path) - - # namespace = path - - # data = { - # "schema": "openpype:container-2.0", - # "id": AVALON_CONTAINER_ID, - # "name": new_name, - # "namespace": namespace, - # "loader": str(loader), - # "representation": context["representation"]["_id"], - # } - # # 3 - imprint data - # imprint("{}/{}".format(path, container_name), data) - # return path + communicator = CommunicationWrapper.communicator + return communicator.send_request( + "containerise", [name, namespace, nodes, context, loader, suffix]) def instantiate(root, name, data, assets=None, suffix="_INS"): @@ -210,57 +154,9 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): suffix (str): suffix string to append to instance name """ - # container_name = "{}{}".format(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 = "{}/{}".format(root, new_name) - # create_publish_instance(instance=container_name, path=path) - - # imprint("{}/{}".format(path, container_name), data) - - -def imprint(node, data): - pass - # 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("OpenPype containerising"): - # unreal.EditorAssetLibrary.save_asset(node) - - -def show_tools_popup(): - """Show popup with tools. - - Popup will disappear on click or loosing focus. - """ - from openpype.hosts.unreal.api import tools_ui - - tools_ui.show_tools_popup() - - -def show_tools_dialog(): - """Show dialog with tools. - - Dialog will stay visible. - """ - from openpype.hosts.unreal.api import tools_ui - - tools_ui.show_tools_dialog() + communicator = CommunicationWrapper.communicator + return communicator.send_request( + "instantiate", params=[root, name, data, assets, suffix]) def show_creator(): @@ -281,164 +177,3 @@ def show_manager(): 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("{}/{}".format(root, name)): - # name = "{}{}".format(name, index) - # index += 1 - # else: - # eal.make_directory("{}/{}".format(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, "{}/{}/{}".format(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.AssetContainerFactory() -# tools = unreal.AssetToolsHelpers().get_asset_tools() - -# asset = tools.create_asset(container, path, None, factory) -# return asset - - -# def create_publish_instance(instance: str, path: str) -> unreal.Object: -# """Helper function to create OpenPype Publish Instance on given path. - -# This behaves similarly as :func:`create_openpype_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.OpenPypePublishInstanceFactory() -# tools = unreal.AssetToolsHelpers().get_asset_tools() -# asset = tools.create_asset(instance, path, None, factory) -# return asset - - -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 = None -# for t in tracks: -# if t.get_class() == unreal.MovieSceneSubTrack.static_class(): -# subscene_track = t -# break -# if subscene_track is not None and subscene_track.get_sections(): -# return subscene_track.get_sections() -# return [] diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py deleted file mode 100644 index 554d4497b96..00000000000 --- a/openpype/hosts/unreal/api/tools_ui.py +++ /dev/null @@ -1,165 +0,0 @@ -import sys -# from Qt import QtWidgets, QtCore, QtGui - -from openpype import ( - resources, - style -) -from openpype.tools.utils import host_tools -from openpype.tools.utils.lib import qt_app_context -from openpype.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) - -# create_btn = QtWidgets.QPushButton("Create...", self) -# load_btn = QtWidgets.QPushButton("Load...", self) -# publish_btn = QtWidgets.QPushButton("Publish...", 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(create_btn, 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) - -# create_btn.clicked.connect(self._on_create) -# 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("publish") - -# 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("OpenPype tools") -# icon = QtGui.QIcon(resources.get_openpype_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 exists 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() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 4a580a4f943..24ec4fad013 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -1,33 +1,308 @@ -# import traceback - -# import unreal - -# openpype_detected = True -# try: -# from openpype.pipeline import install_host -# from openpype.hosts.unreal.api import UnrealHost - -# openpype_host = UnrealHost() -# except ImportError as exc: -# openpype_host = None -# openpype_detected = False -# # unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) -# unreal.log_error(traceback.format_exc()) - -# if openpype_detected: -# install_host(openpype_host) - - -# @unreal.uclass() -# class OpenPypeIntegration(unreal.OpenPypePythonBridge): -# @unreal.ufunction(override=True) -# def RunInPython_Popup(self): -# unreal.log_warning("OpenPype: showing tools popup") -# if openpype_detected: -# openpype_host.show_tools_popup() - -# @unreal.ufunction(override=True) -# def RunInPython_Dialog(self): -# unreal.log_warning("OpenPype: showing tools dialog") -# if openpype_detected: -# openpype_host.show_tools_dialog() +import ast +from typing import List + +import unreal + + +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 parse_container(container): + """To get data from container, AssetContainer 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 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("OpenPype containerising"): + unreal.EditorAssetLibrary.save_asset(node) + +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("{}/{}".format(root, name)): + name = "{}{}".format(name, index) + index += 1 + else: + eal.make_directory("{}/{}".format(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, "{}/{}/{}".format(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.AssetContainerFactory() + tools = unreal.AssetToolsHelpers().get_asset_tools() + + asset = tools.create_asset(container, path, None, factory) + return asset + + +def create_publish_instance(instance: str, path: str) -> unreal.Object: + """Helper function to create OpenPype Publish Instance on given path. + + This behaves similarly as :func:`create_openpype_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.OpenPypePublishInstanceFactory() + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset = tools.create_asset(instance, path, None, factory) + return asset + +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 = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + break + if subscene_track is not None and subscene_track.get_sections(): + return subscene_track.get_sections() + return [] + + +@unreal.uclass() +class OpenPypeIntegration(unreal.OpenPypePythonBridge): + """OpenPype integration for Unreal Engine 5.0.""" + + @unreal.ufunction(override=True) + def ls(self): + """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() + openpype_containers = ar.get_assets_by_class("AssetContainer", True) + + containers = [] + + # 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 openpype_containers: + asset = asset_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = asset_data.asset_name + data = cast_map_to_str_dict(data) + + containers.append(data) + + return str(containers) + + @unreal.ufunction(override=True) + def containerise(self, name, namespc, str_nodes, str_context, loader="", 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. OpenPype 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/OpenPype/Test/TestMaterial.TestMaterial` + + """ + namespace = namespc + nodes = ast.literal_eval(str_nodes) + context = ast.literal_eval(str_context) + if loader == "": + loader = None + # 1 - create directory for container + root = "/Game" + container_name = "{}{}".format(name, suffix) + new_name = move_assets_to_path(root, container_name, nodes) + + # 2 - create Asset Container there + path = "{}/{}".format(root, new_name) + create_container(container=container_name, path=path) + + namespace = path + + data = { + "schema": "openpype:container-2.0", + "id": "pyblish.avalon.container", + "name": new_name, + "namespace": namespace, + "loader": str(loader), + "representation": context["representation"]["_id"], + } + # 3 - imprint data + imprint("{}/{}".format(path, container_name), data) + return path + + @unreal.ufunction(override=True) + def instantiate(self, root, name, str_data, str_assets="", suffix="_INS"): + """Bundles *nodes* into *container*. + + Marking it with metadata as publishable instance. If assets are provided, + they are moved to new path where `OpenPypePublishInstance` 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 + + """ + data = ast.literal_eval(str_data) + assets = ast.literal_eval(str_assets) + container_name = "{}{}".format(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 = "{}/{}".format(root, new_name) + create_publish_instance(instance=container_name, path=path) + + imprint("{}/{}".format(path, container_name), data) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp index 3e5c2ab686b..17ebe84a1c5 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -8,7 +8,6 @@ // Initialize static attributes TSharedPtr FOpenPypeCommunication::Socket = nullptr; -TArray FOpenPypeCommunication::RpcResponses = TArray(); int32 FOpenPypeCommunication::Id = 0; void FOpenPypeCommunication::CreateSocket() @@ -103,7 +102,77 @@ void FOpenPypeCommunication::OnMessage(const FString & Message) if (FJsonSerializer::Deserialize(Reader, Root)) { - if (Root->HasField(TEXT("result"))) + if (Root->HasField(TEXT("method"))) + { + FString Method = Root->GetStringField(TEXT("method")); + UE_LOG(LogTemp, Display, TEXT("Method: %s"), *Method); + + if (Method == "ls") + { + FString Result = UOpenPypePythonBridge::Get()->ls(); + + UE_LOG(LogTemp, Display, TEXT("Result: %s"), *Result); + + FString StringResponse; + FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + + Socket->Send(StringResponse); + } + else if (Method == "containerise") + { + auto params = Root->GetArrayField(TEXT("params")); + + FString Name = params[0]->AsString(); + FString Namespace = params[1]->AsString(); + FString Nodes = params[2]->AsString(); + FString Context = params[3]->AsString(); + FString Loader = params.Num() >= 5 ? params[4]->AsString() : TEXT(""); + FString Suffix = params.Num() == 6 ? params[5]->AsString() : TEXT("_CON"); + + UE_LOG(LogTemp, Display, TEXT("Name: %s"), *Name); + UE_LOG(LogTemp, Display, TEXT("Namespace: %s"), *Namespace); + UE_LOG(LogTemp, Display, TEXT("Nodes: %s"), *Nodes); + UE_LOG(LogTemp, Display, TEXT("Context: %s"), *Context); + UE_LOG(LogTemp, Display, TEXT("Loader: %s"), *Loader); + UE_LOG(LogTemp, Display, TEXT("Suffix: %s"), *Suffix); + + FString Result = UOpenPypePythonBridge::Get()->containerise(Name, Namespace, Nodes, Context, Loader, Suffix); + + UE_LOG(LogTemp, Display, TEXT("Result: %s"), *Result); + + FString StringResponse; + FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + + Socket->Send(StringResponse); + } + else if (Method == "instantiate") + { + auto params = Root->GetArrayField(TEXT("params")); + + FString RootParam = params[0]->AsString(); + FString Name = params[1]->AsString(); + FString Data = params[2]->AsString(); + FString Assets = params.Num() >= 4 ? params[3]->AsString() : TEXT(""); + FString Suffix = params.Num() == 5 ? params[4]->AsString() : TEXT("_INS"); + + UE_LOG(LogTemp, Display, TEXT("Root: %s"), *RootParam); + UE_LOG(LogTemp, Display, TEXT("Name: %s"), *Name); + UE_LOG(LogTemp, Display, TEXT("Data: %s"), *Data); + UE_LOG(LogTemp, Display, TEXT("Assets: %s"), *Assets); + UE_LOG(LogTemp, Display, TEXT("Suffix: %s"), *Suffix); + + UOpenPypePythonBridge::Get()->instantiate(RootParam, Name, Data, Assets, Suffix); + + FString StringResponse; + FRpcResponseResult RpcResponse = { "2.0", "", Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + + Socket->Send(StringResponse); + } + } + else if (Root->HasField(TEXT("result"))) { FString OutputMessage; if (Root->TryGetStringField(TEXT("result"), OutputMessage)) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h index c8926d7162f..495eb2bdec2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h @@ -44,7 +44,7 @@ struct FRpcError }; USTRUCT() -struct FRpcResponse +struct FRpcResponseResult { GENERATED_BODY() @@ -55,6 +55,19 @@ struct FRpcResponse UPROPERTY() FString result; + UPROPERTY() + int32 id; +}; + +USTRUCT() +struct FRpcResponseError +{ + GENERATED_BODY() + +public: + UPROPERTY() + FString jsonrpc; + UPROPERTY() struct FRpcError error; @@ -94,6 +107,5 @@ class FOpenPypeCommunication private: static TSharedPtr Socket; - static TArray RpcResponses; static int32 Id; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h index 692aab2e5e8..f70928d8e8e 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h @@ -9,12 +9,14 @@ class UOpenPypePythonBridge : public UObject public: UFUNCTION(BlueprintCallable, Category = Python) - static UOpenPypePythonBridge* Get(); + static UOpenPypePythonBridge* Get(); UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; + FString ls() const; UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; + FString containerise(const FString & name, const FString & namespc, const FString & str_nodes, const FString & str_context, const FString & loader, const FString & suffix) const; + UFUNCTION(BlueprintImplementableEvent, Category = Python) + FString instantiate(const FString & root, const FString & name, const FString & str_data, const FString & str_assets, const FString & suffix) const; }; diff --git a/openpype/hosts/unreal/remote/__init__.py b/openpype/hosts/unreal/remote/__init__.py deleted file mode 100644 index 99539baaa49..00000000000 --- a/openpype/hosts/unreal/remote/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Copyright Epic Games, Inc. All Rights Reserved. diff --git a/openpype/hosts/unreal/remote/remote_execution.py b/openpype/hosts/unreal/remote/remote_execution.py deleted file mode 100644 index c2083ec6228..00000000000 --- a/openpype/hosts/unreal/remote/remote_execution.py +++ /dev/null @@ -1,639 +0,0 @@ -# Copyright Epic Games, Inc. All Rights Reserved. - -import sys as _sys -import json as _json -import uuid as _uuid -import time as _time -import socket as _socket -import logging as _logging -import threading as _threading - -# Protocol constants (see PythonScriptRemoteExecution.cpp for the full protocol definition) -_PROTOCOL_VERSION = 1 # Protocol version number -_PROTOCOL_MAGIC = 'ue_py' # Protocol magic identifier -_TYPE_PING = 'ping' # Service discovery request (UDP) -_TYPE_PONG = 'pong' # Service discovery response (UDP) -_TYPE_OPEN_CONNECTION = 'open_connection' # Open a TCP command connection with the requested server (UDP) -_TYPE_CLOSE_CONNECTION = 'close_connection' # Close any active TCP command connection (UDP) -_TYPE_COMMAND = 'command' # Execute a remote Python command (TCP) -_TYPE_COMMAND_RESULT = 'command_result' # Result of executing a remote Python command (TCP) - -_NODE_PING_SECONDS = 1 # Number of seconds to wait before sending another "ping" message to discover remote notes -_NODE_TIMEOUT_SECONDS = 5 # Number of seconds to wait before timing out a remote node that was discovered via UDP and has stopped sending "pong" responses - -DEFAULT_MULTICAST_TTL = 0 # Multicast TTL (0 is limited to the local host, 1 is limited to the local subnet) -DEFAULT_MULTICAST_GROUP_ENDPOINT = ('239.0.0.1', 6766) # The multicast group endpoint tuple that the UDP multicast socket should join (must match the "Multicast Group Endpoint" setting in the Python plugin) -DEFAULT_MULTICAST_BIND_ADDRESS = '0.0.0.0' # The adapter address that the UDP multicast socket should bind to, or 0.0.0.0 to bind to all adapters (must match the "Multicast Bind Address" setting in the Python plugin) -DEFAULT_COMMAND_ENDPOINT = ('127.0.0.1', 6776) # The endpoint tuple for the TCP command connection hosted by this client (that the remote client will connect to) -DEFAULT_RECEIVE_BUFFER_SIZE = 8192 # The default receive buffer size - -# Execution modes (these must match the names given to LexToString for EPythonCommandExecutionMode in IPythonScriptPlugin.h) -MODE_EXEC_FILE = 'ExecuteFile' # Execute the Python command as a file. This allows you to execute either a literal Python script containing multiple statements, or a file with optional arguments -MODE_EXEC_STATEMENT = 'ExecuteStatement' # Execute the Python command as a single statement. This will execute a single statement and print the result. This mode cannot run files -MODE_EVAL_STATEMENT = 'EvaluateStatement' # Evaluate the Python command as a single statement. This will evaluate a single statement and return the result. This mode cannot run files - -class RemoteExecutionConfig(object): - ''' - Configuration data for establishing a remote connection with a UE4 instance running Python. - ''' - def __init__(self): - self.multicast_ttl = DEFAULT_MULTICAST_TTL - self.multicast_group_endpoint = DEFAULT_MULTICAST_GROUP_ENDPOINT - self.multicast_bind_address = DEFAULT_MULTICAST_BIND_ADDRESS - self.command_endpoint = DEFAULT_COMMAND_ENDPOINT - -class RemoteExecution(object): - ''' - A remote execution session. This class can discover remote "nodes" (UE4 instances running Python), and allow you to open a command channel to a particular instance. - - Args: - config (RemoteExecutionConfig): Configuration controlling the connection settings for this session. - ''' - def __init__(self, config=RemoteExecutionConfig()): - self._config = config - self._broadcast_connection = None - self._command_connection = None - self._node_id = str(_uuid.uuid4()) - - @property - def remote_nodes(self): - ''' - Get the current set of discovered remote "nodes" (UE4 instances running Python). - - Returns: - list: A list of dicts containg the node ID and the other data. - ''' - return self._broadcast_connection.remote_nodes if self._broadcast_connection else [] - - def start(self): - ''' - Start the remote execution session. This will begin the discovey process for remote "nodes" (UE4 instances running Python). - ''' - self._broadcast_connection = _RemoteExecutionBroadcastConnection(self._config, self._node_id) - self._broadcast_connection.open() - - def stop(self): - ''' - Stop the remote execution session. This will end the discovey process for remote "nodes" (UE4 instances running Python), and close any open command connection. - ''' - self.close_command_connection() - if self._broadcast_connection: - self._broadcast_connection.close() - self._broadcast_connection = None - - def has_command_connection(self): - ''' - Check whether the remote execution session has an active command connection. - - Returns: - bool: True if the remote execution session has an active command connection, False otherwise. - ''' - return self._command_connection is not None - - def open_command_connection(self, remote_node_id): - ''' - Open a command connection to the given remote "node" (a UE4 instance running Python), closing any command connection that may currently be open. - - Args: - remote_node_id (string): The ID of the remote node (this can be obtained by querying `remote_nodes`). - ''' - self._command_connection = _RemoteExecutionCommandConnection(self._config, self._node_id, remote_node_id) - self._command_connection.open(self._broadcast_connection) - - def close_command_connection(self): - ''' - Close any command connection that may currently be open. - ''' - if self._command_connection: - self._command_connection.close(self._broadcast_connection) - self._command_connection = None - - def run_command(self, command, unattended=True, exec_mode=MODE_EXEC_FILE, raise_on_failure=False): - ''' - Run a command remotely based on the current command connection. - - Args: - command (string): The Python command to run remotely. - unattended (bool): True to run this command in "unattended" mode (suppressing some UI). - exec_mode (string): The execution mode to use as a string value (must be one of MODE_EXEC_FILE, MODE_EXEC_STATEMENT, or MODE_EVAL_STATEMENT). - raise_on_failure (bool): True to raise a RuntimeError if the command fails on the remote target. - - Returns: - dict: The result from running the remote command (see `command_result` from the protocol definition). - ''' - data = self._command_connection.run_command(command, unattended, exec_mode) - if raise_on_failure and not data['success']: - raise RuntimeError('Remote Python Command failed! {0}'.format(data['result'])) - return data - -class _RemoteExecutionNode(object): - ''' - A discovered remote "node" (aka, a UE4 instance running Python). - - Args: - data (dict): The data representing this node (from its "pong" reponse). - now (float): The timestamp at which this node was last seen. - ''' - def __init__(self, data, now=None): - self.data = data - self._last_pong = _time_now(now) - - def should_timeout(self, now=None): - ''' - Check to see whether this remote node should be considered timed-out. - - Args: - now (float): The current timestamp. - - Returns: - bool: True of the node has exceeded the timeout limit (`_NODE_TIMEOUT_SECONDS`), False otherwise. - ''' - return (self._last_pong + _NODE_TIMEOUT_SECONDS) < _time_now(now) - -class _RemoteExecutionBroadcastNodes(object): - ''' - A thread-safe set of remote execution "nodes" (UE4 instances running Python). - ''' - def __init__(self): - self._remote_nodes = {} - self._remote_nodes_lock = _threading.RLock() - - @property - def remote_nodes(self): - ''' - Get the current set of discovered remote "nodes" (UE4 instances running Python). - - Returns: - list: A list of dicts containg the node ID and the other data. - ''' - with self._remote_nodes_lock: - remote_nodes_list = [] - for node_id, node in self._remote_nodes.items(): - remote_node_data = dict(node.data) - remote_node_data['node_id'] = node_id - remote_nodes_list.append(remote_node_data) - return remote_nodes_list - - def update_remote_node(self, node_id, node_data, now=None): - ''' - Update a remote node, replacing any existing data. - - Args: - node_id (str): The ID of the remote node (from its "pong" reponse). - node_data (dict): The data representing this node (from its "pong" reponse). - now (float): The timestamp at which this node was last seen. - ''' - now = _time_now(now) - with self._remote_nodes_lock: - if node_id not in self._remote_nodes: - _logger.debug('Found Node {0}: {1}'.format(node_id, node_data)) - self._remote_nodes[node_id] = _RemoteExecutionNode(node_data, now) - - def timeout_remote_nodes(self, now=None): - ''' - Check to see whether any remote nodes should be considered timed-out, and if so, remove them from this set. - - Args: - now (float): The current timestamp. - ''' - now = _time_now(now) - with self._remote_nodes_lock: - for node_id, node in list(self._remote_nodes.items()): - if node.should_timeout(now): - _logger.debug('Lost Node {0}: {1}'.format(node_id, node.data)) - del self._remote_nodes[node_id] - -class _RemoteExecutionBroadcastConnection(object): - ''' - A remote execution broadcast connection (for UDP based messaging and node discovery). - - Args: - config (RemoteExecutionConfig): Configuration controlling the connection settings. - node_id (string): The ID of the local "node" (this session). - ''' - def __init__(self, config, node_id): - self._config = config - self._node_id = node_id - self._nodes = None - self._running = False - self._broadcast_socket = None - self._broadcast_listen_thread = None - - @property - def remote_nodes(self): - ''' - Get the current set of discovered remote "nodes" (UE4 instances running Python). - - Returns: - list: A list of dicts containg the node ID and the other data. - ''' - return self._nodes.remote_nodes if self._nodes else [] - - def open(self): - ''' - Open the UDP based messaging and discovery connection. This will begin the discovey process for remote "nodes" (UE4 instances running Python). - ''' - self._running = True - self._last_ping = None - self._nodes = _RemoteExecutionBroadcastNodes() - self._init_broadcast_socket() - self._init_broadcast_listen_thread() - - def close(self): - ''' - Close the UDP based messaging and discovery connection. This will end the discovey process for remote "nodes" (UE4 instances running Python). - ''' - self._running = False - if self._broadcast_listen_thread: - self._broadcast_listen_thread.join() - if self._broadcast_socket: - self._broadcast_socket.close() - self._broadcast_socket = None - self._nodes = None - - def _init_broadcast_socket(self): - ''' - Initialize the UDP based broadcast socket based on the current configuration. - ''' - self._broadcast_socket = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM, _socket.IPPROTO_UDP) # UDP/IP socket - if hasattr(_socket, 'SO_REUSEPORT'): - self._broadcast_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEPORT, 1) - else: - self._broadcast_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) - self._broadcast_socket.bind((self._config.multicast_bind_address, self._config.multicast_group_endpoint[1])) - self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_MULTICAST_LOOP, 1) - self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_MULTICAST_TTL, self._config.multicast_ttl) - self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_MULTICAST_IF, _socket.inet_aton(self._config.multicast_bind_address)) - self._broadcast_socket.setsockopt(_socket.IPPROTO_IP, _socket.IP_ADD_MEMBERSHIP, _socket.inet_aton(self._config.multicast_group_endpoint[0]) + _socket.inet_aton(self._config.multicast_bind_address)) - self._broadcast_socket.settimeout(0.1) - - def _init_broadcast_listen_thread(self): - ''' - Initialize the listen thread for the UDP based broadcast socket to allow discovery to run async. - ''' - self._broadcast_listen_thread = _threading.Thread(target=self._run_broadcast_listen_thread) - self._broadcast_listen_thread.daemon = True - self._broadcast_listen_thread.start() - - def _run_broadcast_listen_thread(self): - ''' - Main loop for the listen thread that handles processing discovery messages. - ''' - while self._running: - # Receive and process all pending data - while True: - try: - data = self._broadcast_socket.recv(DEFAULT_RECEIVE_BUFFER_SIZE) - except _socket.timeout: - data = None - if data: - self._handle_data(data) - else: - break - # Run tick logic - now = _time_now() - self._broadcast_ping(now) - self._nodes.timeout_remote_nodes(now) - _time.sleep(0.1) - - def _broadcast_message(self, message): - ''' - Broadcast the given message over the UDP socket to anything that might be listening. - - Args: - message (_RemoteExecutionMessage): The message to broadcast. - ''' - self._broadcast_socket.sendto(message.to_json_bytes(), self._config.multicast_group_endpoint) - - def _broadcast_ping(self, now=None): - ''' - Broadcast a "ping" message over the UDP socket to anything that might be listening. - - Args: - now (float): The current timestamp. - ''' - now = _time_now(now) - if not self._last_ping or ((self._last_ping + _NODE_PING_SECONDS) < now): - self._last_ping = now - self._broadcast_message(_RemoteExecutionMessage(_TYPE_PING, self._node_id)) - - def broadcast_open_connection(self, remote_node_id): - ''' - Broadcast an "open_connection" message over the UDP socket to be handled by the specified remote node. - - Args: - remote_node_id (string): The ID of the remote node that we want to open a command connection with. - ''' - self._broadcast_message(_RemoteExecutionMessage(_TYPE_OPEN_CONNECTION, self._node_id, remote_node_id, { - 'command_ip': self._config.command_endpoint[0], - 'command_port': self._config.command_endpoint[1], - })) - - def broadcast_close_connection(self, remote_node_id): - ''' - Broadcast a "close_connection" message over the UDP socket to be handled by the specified remote node. - - Args: - remote_node_id (string): The ID of the remote node that we want to close a command connection with. - ''' - self._broadcast_message(_RemoteExecutionMessage(_TYPE_CLOSE_CONNECTION, self._node_id, remote_node_id)) - - def _handle_data(self, data): - ''' - Handle data received from the UDP broadcast socket. - - Args: - data (bytes): The raw bytes received from the socket. - ''' - message = _RemoteExecutionMessage(None, None) - if message.from_json_bytes(data): - self._handle_message(message) - - def _handle_message(self, message): - ''' - Handle a message received from the UDP broadcast socket. - - Args: - message (_RemoteExecutionMessage): The message received from the socket. - ''' - if not message.passes_receive_filter(self._node_id): - return - if message.type_ == _TYPE_PONG: - self._handle_pong_message(message) - return - _logger.debug('Unhandled remote execution message type "{0}"'.format(message.type_)) - - def _handle_pong_message(self, message): - ''' - Handle a "pong" message received from the UDP broadcast socket. - - Args: - message (_RemoteExecutionMessage): The message received from the socket. - ''' - self._nodes.update_remote_node(message.source, message.data) - -class _RemoteExecutionCommandConnection(object): - ''' - A remote execution command connection (for TCP based command processing). - - Args: - config (RemoteExecutionConfig): Configuration controlling the connection settings. - node_id (string): The ID of the local "node" (this session). - remote_node_id (string): The ID of the remote "node" (the UE4 instance running Python). - ''' - def __init__(self, config, node_id, remote_node_id): - self._config = config - self._node_id = node_id - self._remote_node_id = remote_node_id - self._command_listen_socket = None - self._command_channel_socket = _socket.socket() # This type is only here to appease PyLint - - def open(self, broadcast_connection): - ''' - Open the TCP based command connection, and wait to accept the connection from the remote party. - - Args: - broadcast_connection (_RemoteExecutionBroadcastConnection): The broadcast connection to send UDP based messages over. - ''' - self._nodes = _RemoteExecutionBroadcastNodes() - self._init_command_listen_socket() - self._try_accept(broadcast_connection) - - def close(self, broadcast_connection): - ''' - Close the TCP based command connection, attempting to notify the remote party. - - Args: - broadcast_connection (_RemoteExecutionBroadcastConnection): The broadcast connection to send UDP based messages over. - ''' - broadcast_connection.broadcast_close_connection(self._remote_node_id) - if self._command_channel_socket: - self._command_channel_socket.close() - self._command_channel_socket = None - if self._command_listen_socket: - self._command_listen_socket.close() - self._command_listen_socket = None - - def run_command(self, command, unattended, exec_mode): - ''' - Run a command on the remote party. - - Args: - command (string): The Python command to run remotely. - unattended (bool): True to run this command in "unattended" mode (suppressing some UI). - exec_mode (string): The execution mode to use as a string value (must be one of MODE_EXEC_FILE, MODE_EXEC_STATEMENT, or MODE_EVAL_STATEMENT). - - Returns: - dict: The result from running the remote command (see `command_result` from the protocol definition). - ''' - self._send_message(_RemoteExecutionMessage(_TYPE_COMMAND, self._node_id, self._remote_node_id, { - 'command': command, - 'unattended': unattended, - 'exec_mode': exec_mode, - })) - result = self._receive_message(_TYPE_COMMAND_RESULT) - return result.data - - def _send_message(self, message): - ''' - Send the given message over the TCP socket to the remote party. - - Args: - message (_RemoteExecutionMessage): The message to send. - ''' - self._command_channel_socket.sendall(message.to_json_bytes()) - - def _receive_message(self, expected_type): - ''' - Receive a message over the TCP socket from the remote party. - - Args: - expected_type (string): The type of message we expect to receive. - - Returns: - The message that was received. - ''' - data = self._command_channel_socket.recv(DEFAULT_RECEIVE_BUFFER_SIZE) - if data: - message = _RemoteExecutionMessage(None, None) - if message.from_json_bytes(data) and message.passes_receive_filter(self._node_id) and message.type_ == expected_type: - return message - raise RuntimeError('Remote party failed to send a valid response!') - - def _init_command_listen_socket(self): - ''' - Initialize the TCP based command socket based on the current configuration, and set it to listen for an incoming connection. - ''' - self._command_listen_socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM, _socket.IPPROTO_TCP) # TCP/IP socket - if hasattr(_socket, 'SO_REUSEPORT'): - self._command_listen_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEPORT, 1) - else: - self._command_listen_socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) - self._command_listen_socket.bind(self._config.command_endpoint) - self._command_listen_socket.listen(1) - self._command_listen_socket.settimeout(5) - - def _try_accept(self, broadcast_connection): - ''' - Wait to accept a connection on the TCP based command connection. This makes 6 attempts to receive a connection, waiting for 5 seconds between each attempt (30 seconds total). - - Args: - broadcast_connection (_RemoteExecutionBroadcastConnection): The broadcast connection to send UDP based messages over. - ''' - for _n in range(6): - broadcast_connection.broadcast_open_connection(self._remote_node_id) - try: - self._command_channel_socket = self._command_listen_socket.accept()[0] - self._command_channel_socket.setblocking(True) - return - except _socket.timeout: - continue - raise RuntimeError('Remote party failed to attempt the command socket connection!') - -class _RemoteExecutionMessage(object): - ''' - A message sent or received by remote execution (on either the UDP or TCP connection), as UTF-8 encoded JSON. - - Args: - type_ (string): The type of this message (see the `_TYPE_` constants). - source (string): The ID of the node that sent this message. - dest (string): The ID of the destination node of this message, or None to send to all nodes (for UDP broadcast). - data (dict): The message specific payload data. - ''' - def __init__(self, type_, source, dest=None, data=None): - self.type_ = type_ - self.source = source - self.dest = dest - self.data = data - - def passes_receive_filter(self, node_id): - ''' - Test to see whether this message should be received by the current node (wasn't sent to itself, and has a compatible destination ID). - - Args: - node_id (string): The ID of the local "node" (this session). - - Returns: - bool: True if this message should be received by the current node, False otherwise. - ''' - return self.source != node_id and (not self.dest or self.dest == node_id) - - def to_json(self): - ''' - Convert this message to its JSON representation. - - Returns: - str: The JSON representation of this message. - ''' - if not self.type_: - raise ValueError('"type" cannot be empty!') - if not self.source: - raise ValueError('"source" cannot be empty!') - json_obj = { - 'version': _PROTOCOL_VERSION, - 'magic': _PROTOCOL_MAGIC, - 'type': self.type_, - 'source': self.source, - } - if self.dest: - json_obj['dest'] = self.dest - if self.data: - json_obj['data'] = self.data - return _json.dumps(json_obj, ensure_ascii=False) - - def to_json_bytes(self): - ''' - Convert this message to its JSON representation as UTF-8 bytes. - - Returns: - bytes: The JSON representation of this message as UTF-8 bytes. - ''' - json_str = self.to_json() - return json_str.encode('utf-8') - - def from_json(self, json_str): - ''' - Parse this message from its JSON representation. - - Args: - json_str (str): The JSON representation of this message. - - Returns: - bool: True if this message could be parsed, False otherwise. - ''' - try: - json_obj = _json.loads(json_str) - # Read and validate required protocol version information - if json_obj['version'] != _PROTOCOL_VERSION: - raise ValueError('"version" is incorrect (got {0}, expected {1})!'.format(json_obj['version'], _PROTOCOL_VERSION)) - if json_obj['magic'] != _PROTOCOL_MAGIC: - raise ValueError('"magic" is incorrect (got "{0}", expected "{1}")!'.format(json_obj['magic'], _PROTOCOL_MAGIC)) - # Read required fields - local_type = json_obj['type'] - local_source = json_obj['source'] - self.type_ = local_type - self.source = local_source - # Read optional fields - self.dest = json_obj.get('dest') - self.data = json_obj.get('data') - except Exception as e: - _logger.error('Failed to deserialize JSON "{0}": {1}'.format(json_str, str(e))) - return False - return True - - def from_json_bytes(self, json_bytes): - ''' - Parse this message from its JSON representation as UTF-8 bytes. - - Args: - json_bytes (bytes): The JSON representation of this message as UTF-8 bytes. - - Returns: - bool: True if this message could be parsed, False otherwise. - ''' - json_str = json_bytes.decode('utf-8') - return self.from_json(json_str) - -def _time_now(now=None): - ''' - Utility function to resolve a potentially cached time value. - - Args: - now (float): The cached timestamp, or None to return the current time. - - Returns: - float: The cached timestamp (if set), otherwise the current time. - ''' - return _time.time() if now is None else now - -# Log handling -_logger = _logging.getLogger(__name__) -_log_handler = _logging.StreamHandler() -_logger.addHandler(_log_handler) -def set_log_level(log_level): - _logger.setLevel(log_level) - _log_handler.setLevel(log_level) - -# Usage example -if __name__ == '__main__': - set_log_level(_logging.DEBUG) - remote_exec = RemoteExecution() - remote_exec.start() - # Ask for a remote node ID - _sys.stdout.write('Enter remote node ID to connect to: ') - remote_node_id = _sys.stdin.readline().rstrip() - # Connect to it - remote_exec.open_command_connection(remote_node_id) - # Process commands remotely - _sys.stdout.write('Connected. Enter commands, or an empty line to quit.\n') - exec_mode = MODE_EXEC_FILE - while True: - input = _sys.stdin.readline().rstrip() - if input: - if input.startswith('set mode '): - exec_mode = input[9:] - else: - print(remote_exec.run_command(input, exec_mode=exec_mode)) - else: - break - remote_exec.stop() diff --git a/openpype/hosts/unreal/remote/rpc/__init__.py b/openpype/hosts/unreal/remote/rpc/__init__.py deleted file mode 100644 index 6dd57c290e2..00000000000 --- a/openpype/hosts/unreal/remote/rpc/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from . import client, factory - -__all__ = [ - client, - factory -] diff --git a/openpype/hosts/unreal/remote/rpc/base_server.py b/openpype/hosts/unreal/remote/rpc/base_server.py deleted file mode 100644 index a74c5a1d2ee..00000000000 --- a/openpype/hosts/unreal/remote/rpc/base_server.py +++ /dev/null @@ -1,245 +0,0 @@ -import os -import sys -import abc -import queue -import time -import logging -import threading -from xmlrpc.server import SimpleXMLRPCServer - -# importlib machinery needs to be available for importing client modules -from importlib.machinery import SourceFileLoader - -logger = logging.getLogger(__name__) - -EXECUTION_QUEUE = queue.Queue() -RETURN_VALUE_NAME = 'RPC_SERVER_RETURN_VALUE' -ERROR_VALUE_NAME = 'RPC_SERVER_ERROR_VALUE' - - -def run_in_main_thread(callable_instance, *args): - """ - Runs the provided callable instance in the main thread by added it to a que - that is processed by a recurring event in an integration like a timer. - - :param call callable_instance: A callable. - :return: The return value of any call from the client. - """ - timeout = int(os.environ.get('RPC_TIME_OUT', 20)) - - globals().pop(RETURN_VALUE_NAME, None) - globals().pop(ERROR_VALUE_NAME, None) - EXECUTION_QUEUE.put((callable_instance, args)) - - for attempt in range(timeout * 10): - if RETURN_VALUE_NAME in globals(): - return globals().get(RETURN_VALUE_NAME) - elif ERROR_VALUE_NAME in globals(): - raise globals()[ERROR_VALUE_NAME] - else: - time.sleep(0.1) - - if RETURN_VALUE_NAME not in globals(): - raise TimeoutError( - f'The call "{callable_instance.__name__}" timed out because it hit the timeout limit' - f' of {timeout} seconds.' - ) - - -def execute_queued_calls(*extra_args): - """ - Runs calls in the execution que till they are gone. Designed to be passed to a - recurring event in an integration like a timer. - """ - while not EXECUTION_QUEUE.empty(): - if RETURN_VALUE_NAME not in globals(): - callable_instance, args = EXECUTION_QUEUE.get() - try: - globals()[RETURN_VALUE_NAME] = callable_instance(*args) - except Exception as error: - # store the error in the globals and re-raise it - globals()[ERROR_VALUE_NAME] = error - raise error - - -class BaseServer(SimpleXMLRPCServer): - def serve_until_killed(self): - """ - Serves till killed by the client. - """ - self.quit = False - while not self.quit: - self.handle_request() - - -class BaseRPCServer: - def __init__(self, name, port, is_thread=False): - """ - Initialize the base server. - - :param str name: The name of the server. - :param int port: The number of the server port. - :param bool is_thread: Whether or not the server is encapsulated in a thread. - """ - self.server = BaseServer( - (os.environ.get('RPC_HOST', '127.0.0.1'), port), - logRequests=False, - allow_none=True - ) - self.is_thread = is_thread - self.server.register_function(self.add_new_callable) - self.server.register_function(self.kill) - self.server.register_function(self.is_running) - self.server.register_function(self.set_env) - self.server.register_introspection_functions() - self.server.register_multicall_functions() - logger.info(f'Started RPC server "{name}".') - - @staticmethod - def is_running(): - """ - Responds if the server is running. - """ - return True - - @staticmethod - def set_env(name, value): - """ - Sets an environment variable in the server's python environment. - - :param str name: The name of the variable. - :param str value: The value. - """ - os.environ[name] = str(value) - - def kill(self): - """ - Kill the running server from the client. Only if running in blocking mode. - """ - self.server.quit = True - return True - - def add_new_callable(self, callable_name, code, client_system_path, remap_pairs=None): - """ - Adds a new callable defined in the client to the server. - - :param str callable_name: The name of the function that will added to the server. - :param str code: The code of the callable that will be added to the server. - :param list[str] client_system_path: The list of python system paths from the client. - :param list(tuple) remap_pairs: A list of tuples with first value being the client python path root and the - second being the new server path root. This can be useful if the client and server are on two different file - systems and the root of the import paths need to be dynamically replaced. - :return str: A response message back to the client. - """ - for path in client_system_path: - # if a list of remap pairs are provided, they will be remapped before being added to the system path - for client_path_root, matching_server_path_root in remap_pairs or []: - if path.startswith(client_path_root): - path = os.path.join( - matching_server_path_root, - path.replace(client_path_root, '').replace(os.sep, '/').strip('/') - ) - - if path not in sys.path: - sys.path.append(path) - - # run the function code - exec(code) - callable_instance = locals().copy().get(callable_name) - - # grab it from the locals and register it with the server - if callable_instance: - if self.is_thread: - self.server.register_function( - self.thread_safe_call(callable_instance), - callable_name - ) - else: - self.server.register_function( - callable_instance, - callable_name - ) - return f'The function "{callable_name}" has been successfully registered with the server!' - - -class BaseRPCServerThread(threading.Thread, BaseRPCServer): - def __init__(self, name, port): - """ - Initialize the base rpc server. - - :param str name: The name of the server. - :param int port: The number of the server port. - """ - threading.Thread.__init__(self, name=name, daemon=True) - BaseRPCServer.__init__(self, name, port, is_thread=True) - - def run(self): - """ - Overrides the run method. - """ - self.server.serve_forever() - - @abc.abstractmethod - def thread_safe_call(self, callable_instance, *args): - """ - Implements thread safe execution of a call. - """ - return - - -class BaseRPCServerManager: - @abc.abstractmethod - def __init__(self): - """ - Initialize the server manager. - Note: when this class is subclassed `name`, `port`, `threaded_server_class` need to be defined. - """ - self.server_thread = None - self.server_blocking = None - - def start_server_thread(self): - """ - Starts the server in a thread. - """ - self.server_thread = self.threaded_server_class(self.name, self.port) - self.server_thread.start() - - def start_server_blocking(self): - """ - Starts the server in the main thread, which blocks all other processes. This can only - be killed by the client. - """ - self.server_blocking = BaseRPCServer(self.name, self.port) - self.server_blocking.server.serve_until_killed() - - def start(self, threaded=True): - """ - Starts the server. - - :param bool threaded: Whether or not to start the server in a thread. If not threaded - it will block all other processes. - """ - # start the server in a thread - if threaded and not self.server_thread: - self.start_server_thread() - - # start the blocking server - elif not threaded and not self.server_blocking: - self.start_server_blocking() - - else: - logger.info(f'RPC server "{self.name}" is already running...') - - def shutdown(self): - """ - Shuts down the server. - """ - if self.server_thread: - logger.info(f'RPC server "{self.name}" is shutting down...') - - # kill the server in the thread - if self.server_thread: - self.server_thread.server.shutdown() - self.server_thread.join() - - logger.info(f'RPC server "{self.name}" has shutdown.') diff --git a/openpype/hosts/unreal/remote/rpc/client.py b/openpype/hosts/unreal/remote/rpc/client.py deleted file mode 100644 index 42658914aa2..00000000000 --- a/openpype/hosts/unreal/remote/rpc/client.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import re -import logging -import inspect -from xmlrpc.client import ( - ServerProxy, - Unmarshaller, - Transport, - ExpatParser, - Fault, - ResponseError -) -logger = logging.getLogger(__package__) - - -class RPCUnmarshaller(Unmarshaller): - def __init__(self, *args, **kwargs): - Unmarshaller.__init__(self, *args, **kwargs) - self.error_pattern = re.compile(r'(?P[^:]*):(?P.*$)') - self.builtin_exceptions = self._get_built_in_exceptions() - - @staticmethod - def _get_built_in_exceptions(): - """ - Gets a list of the built in exception classes in python. - - :return list[BaseException] A list of the built in exception classes in python: - """ - builtin_exceptions = [] - for builtin_name, builtin_class in globals().get('__builtins__').items(): - if builtin_class and inspect.isclass(builtin_class) and issubclass(builtin_class, BaseException): - builtin_exceptions.append(builtin_class) - - return builtin_exceptions - - def close(self): - """ - Override so we redefine the unmarshaller. - - :return tuple: A tuple of marshallables. - """ - if self._type is None or self._marks: - raise ResponseError() - - if self._type == 'fault': - marshallables = self._stack[0] - match = self.error_pattern.match(marshallables.get('faultString', '')) - if match: - exception_name = match.group('exception').strip("") - exception_message = match.group('exception_message') - - if exception_name: - for exception in self.builtin_exceptions: - if exception.__name__ == exception_name: - raise exception(exception_message) - - # if all else fails just raise the fault - raise Fault(**marshallables) - return tuple(self._stack) - - -class RPCTransport(Transport): - def getparser(self): - """ - Override so we can redefine our transport to use its own custom unmarshaller. - - :return tuple(ExpatParser, RPCUnmarshaller): The parser and unmarshaller instances. - """ - unmarshaller = RPCUnmarshaller() - parser = ExpatParser(unmarshaller) - return parser, unmarshaller - - -class RPCServerProxy(ServerProxy): - def __init__(self, *args, **kwargs): - """ - Override so we can redefine the ServerProxy to use our custom transport. - """ - kwargs['transport'] = RPCTransport() - ServerProxy.__init__(self, *args, **kwargs) - - -class RPCClient: - def __init__(self, port, marshall_exceptions=True): - """ - Initializes the rpc client. - - :param int port: A port number the client should connect to. - :param bool marshall_exceptions: Whether or not the exceptions should be marshalled. - """ - if marshall_exceptions: - proxy_class = RPCServerProxy - else: - proxy_class = ServerProxy - - server_ip = os.environ.get('RPC_SERVER_IP', '127.0.0.1') - - self.proxy = proxy_class( - f"http://{server_ip}:{port}", - allow_none=True, - ) - self.marshall_exceptions = marshall_exceptions - self.port = port diff --git a/openpype/hosts/unreal/remote/rpc/exceptions.py b/openpype/hosts/unreal/remote/rpc/exceptions.py deleted file mode 100644 index 1a143d04fe5..00000000000 --- a/openpype/hosts/unreal/remote/rpc/exceptions.py +++ /dev/null @@ -1,79 +0,0 @@ - -class BaseRPCException(Exception): - """ - Raised when a rpc class method is not authored as a static method. - """ - def __init__(self, message=None, line_link=''): - self.message = message + line_link - super().__init__(self.message) - - -class InvalidClassMethod(BaseRPCException): - """ - Raised when a rpc class method is not authored as a static method. - """ - def __init__(self, cls, method, message=None, line_link=''): - self.message = message - - if message is None: - self.message = ( - f'\n {cls.__name__}.{method.__name__} is not a static method. Please decorate with @staticmethod.' - ) - BaseRPCException.__init__(self, self.message, line_link) - - -class InvalidTestCasePort(BaseRPCException): - """ - Raised when a rpc test case class does not have a port defined. - """ - def __init__(self, cls, message=None, line_link=''): - self.message = message - - if message is None: - self.message = f'\n You must set {cls.__name__}.port to a supported RPC port.' - BaseRPCException.__init__(self, self.message, line_link) - - -class InvalidKeyWordParameters(BaseRPCException): - """ - Raised when a rpc function has key word arguments in its parameters. - """ - def __init__(self, function, kwargs, message=None, line_link=''): - self.message = message - - if message is None: - self.message = ( - f'\n Keyword arguments "{kwargs}" were found on "{function.__name__}". The RPC client does not ' - f'support key word arguments . Please change your code to use only arguments.' - ) - BaseRPCException.__init__(self, self.message, line_link) - - -class UnsupportedArgumentType(BaseRPCException): - """ - Raised when a rpc function's argument type is not supported. - """ - def __init__(self, function, arg, supported_types, message=None, line_link=''): - self.message = message - - if message is None: - self.message = ( - f'\n "{function.__name__}" has an argument of type "{arg.__class__.__name__}". The only types that are' - f' supported by the RPC client are {[supported_type.__name__ for supported_type in supported_types]}.' - ) - BaseRPCException.__init__(self, self.message, line_link) - - -class FileNotSavedOnDisk(BaseRPCException): - """ - Raised when a rpc function is called in a context where it is not a saved file on disk. - """ - def __init__(self, function, message=None): - self.message = message - - if message is None: - self.message = ( - f'\n "{function.__name__}" is not being called from a saved file. The RPC client does not ' - f'support code that is not saved. Please save your code to a file on disk and re-run it.' - ) - BaseRPCException.__init__(self, self.message) diff --git a/openpype/hosts/unreal/remote/rpc/factory.py b/openpype/hosts/unreal/remote/rpc/factory.py deleted file mode 100644 index cc6ae964ffd..00000000000 --- a/openpype/hosts/unreal/remote/rpc/factory.py +++ /dev/null @@ -1,319 +0,0 @@ -import os -import re -import sys -import logging -import types -import inspect -import textwrap -import unittest -from xmlrpc.client import Fault - -from .client import RPCClient -from .validations import ( - validate_key_word_parameters, - validate_class_method, - get_source_file_path, - get_line_link, - validate_arguments, - validate_file_is_saved, -) - -logger = logging.getLogger(__package__) - - -class RPCFactory: - def __init__(self, rpc_client, remap_pairs=None, default_imports=None): - self.rpc_client = rpc_client - self.file_path = None - self.remap_pairs = remap_pairs - self.default_imports = default_imports or [] - - @staticmethod - def _get_docstring(code, function_name): - """ - Gets the docstring value from the functions code. - - :param list code: A list of code lines. - :param str function_name: The name of the function. - :returns: The docstring text. - :rtype: str - """ - # run the function code - exec('\n'.join(code)) - # get the function from the locals - function_instance = locals().copy().get(function_name) - # get the doc strings from the function - return function_instance.__doc__ - - @staticmethod - def _save_execution_history(code, function, args): - """ - Saves out the executed code to a file. - - :param list code: A list of code lines. - :param callable function: A function. - :param list args: A list of function arguments. - """ - history_file_path = os.environ.get('RPC_EXECUTION_HISTORY_FILE') - - if history_file_path and os.path.exists(os.path.dirname(history_file_path)): - file_size = 0 - if os.path.exists(history_file_path): - file_size = os.path.getsize(history_file_path) - - with open(history_file_path, 'a') as history_file: - # add the import for SourceFileLoader if the file is empty - if file_size == 0: - history_file.write('from importlib.machinery import SourceFileLoader\n') - - # space out the functions - history_file.write(f'\n\n') - - for line in code: - history_file.write(f'{line}\n') - - # convert the args to strings - formatted_args = [] - for arg in args: - if isinstance(arg, str): - formatted_args.append(f'r"{arg}"') - else: - formatted_args.append(str(arg)) - - # write the call with the arg values - params = ", ".join(formatted_args) if formatted_args else '' - history_file.write(f'{function.__name__}({params})\n') - - def _get_callstack_references(self, code, function): - """ - Gets all references for the given code. - - :param list[str] code: The code of the callable. - :param callable function: A callable. - :return str: The new code of the callable with all its references added. - """ - import_code = self.default_imports - - client_module = inspect.getmodule(function) - self.file_path = get_source_file_path(function) - - # if a list of remap pairs have been set, the file path will be remapped to the new server location - # Note: The is useful when the server and client are not on the same machine. - server_module_path = self.file_path - for client_path_root, matching_server_path_root in self.remap_pairs or []: - if self.file_path.startswith(client_path_root): - server_module_path = os.path.join( - matching_server_path_root, - self.file_path.replace(client_path_root, '').replace(os.sep, '/').strip('/') - ) - break - - for key in dir(client_module): - for line_number, line in enumerate(code): - if line.startswith('def '): - continue - - if key in re.split('\.|\(| ', line.strip()): - if os.path.basename(self.file_path) == '__init__.py': - base_name = os.path.basename(os.path.dirname(self.file_path)) - else: - base_name = os.path.basename(self.file_path) - - module_name, file_extension = os.path.splitext(base_name) - - # add the source file to the import code - source_import_code = f'{module_name} = SourceFileLoader("{module_name}", r"{server_module_path}").load_module()' - if source_import_code not in import_code: - import_code.append(source_import_code) - - # relatively import the module from the source file - relative_import_code = f'from {module_name} import {key}' - if relative_import_code not in import_code: - import_code.append(relative_import_code) - - break - - return textwrap.indent('\n'.join(import_code), ' ' * 4) - - def _get_code(self, function): - """ - Gets the code from a callable. - - :param callable function: A callable. - :return str: The code of the callable. - """ - code = textwrap.dedent(inspect.getsource(function)).split('\n') - code = [line for line in code if not line.startswith(('@', '#'))] - - # get the docstring from the code - doc_string = self._get_docstring(code, function.__name__) - - # get import code and insert them inside the function - import_code = self._get_callstack_references(code, function) - code.insert(1, import_code) - - # remove the doc string - if doc_string: - code = '\n'.join(code).replace(doc_string, '') - code = [line for line in code.split('\n') if not all([char == '"' or char == "'" for char in line.strip()])] - - return code - - def _register(self, function): - """ - Registers a given callable with the server. - - :param callable function: A callable. - :return: The code of the function. - :rtype: list - """ - code = self._get_code(function) - try: - # if additional paths are explicitly set, then use them. This is useful with the client is on another - # machine and the python paths are different - additional_paths = list(filter(None, os.environ.get('RPC_ADDITIONAL_PYTHON_PATHS', '').split(','))) - - if not additional_paths: - # otherwise use the current system path - additional_paths = sys.path - - response = self.rpc_client.proxy.add_new_callable( - function.__name__, '\n'.join(code), - additional_paths - ) - if os.environ.get('RPC_DEBUG'): - logger.debug(response) - - except ConnectionRefusedError: - server_name = os.environ.get(f'RPC_SERVER_{self.rpc_client.port}', self.rpc_client.port) - raise ConnectionRefusedError(f'No connection could be made with "{server_name}"') - - return code - - def run_function_remotely(self, function, args): - """ - Handles running the given function on remotely. - - :param callable function: A function reference. - :param tuple(Any) args: The function's arguments. - :return callable: A remote callable. - """ - validate_arguments(function, args) - - # get the remote function instance - code = self._register(function) - remote_function = getattr(self.rpc_client.proxy, function.__name__) - self._save_execution_history(code, function, args) - - current_frame = inspect.currentframe() - outer_frame_info = inspect.getouterframes(current_frame) - # step back 2 frames in the callstack - caller_frame = outer_frame_info[2][0] - # create a trace back that is relevant to the remote code rather than the code transporting it - call_traceback = types.TracebackType(None, caller_frame, caller_frame.f_lasti, caller_frame.f_lineno) - # call the remote function - if not self.rpc_client.marshall_exceptions: - # if exceptions are not marshalled then receive the default Fault - return remote_function(*args) - - # otherwise catch them and add a line link to them - try: - return remote_function(*args) - except Exception as exception: - stack_trace = str(exception) + get_line_link(function) - if isinstance(exception, Fault): - raise Fault(exception.faultCode, exception.faultString) - raise exception.__class__(stack_trace).with_traceback(call_traceback) - - -def remote_call(port, default_imports=None, remap_pairs=None): - """ - A decorator that makes this function run remotely. - - :param Enum port: The name of the port application i.e. maya, blender, unreal. - :param list[str] default_imports: A list of import commands that include modules in every call. - :param list(tuple) remap_pairs: A list of tuples with first value being the client file path root and the - second being the matching server path root. This can be useful if the client and server are on two different file - systems and the root of the import paths need to be dynamically replaced. - """ - def decorator(function): - def wrapper(*args, **kwargs): - validate_file_is_saved(function) - validate_key_word_parameters(function, kwargs) - rpc_factory = RPCFactory( - rpc_client=RPCClient(port), - remap_pairs=remap_pairs, - default_imports=default_imports - ) - return rpc_factory.run_function_remotely(function, args) - return wrapper - return decorator - - -def remote_class(decorator): - """ - A decorator that makes this class run remotely. - - :param remote_call decorator: The remote call decorator. - :return: A decorated class. - """ - def decorate(cls): - for attribute, value in cls.__dict__.items(): - validate_class_method(cls, value) - if callable(getattr(cls, attribute)): - setattr(cls, attribute, decorator(getattr(cls, attribute))) - return cls - return decorate - - -class RPCTestCase(unittest.TestCase): - """ - Subclasses unittest.TestCase to implement a RPC compatible TestCase. - """ - port = None - remap_pairs = None - default_imports = None - - @classmethod - def run_remotely(cls, method, args): - """ - Run the given method remotely. - - :param callable method: A method to wrap. - """ - default_imports = cls.__dict__.get('default_imports', None) - port = cls.__dict__.get('port', None) - remap_pairs = cls.__dict__.get('remap_pairs', None) - rpc_factory = RPCFactory( - rpc_client=RPCClient(port), - default_imports=default_imports, - remap_pairs=remap_pairs - ) - return rpc_factory.run_function_remotely(method, args) - - def _callSetUp(self): - """ - Overrides the TestCase._callSetUp method by passing it to be run remotely. - Notice None is passed as an argument instead of self. This is because only static methods - are allowed by the RPCClient. - """ - self.run_remotely(self.setUp, [None]) - - def _callTearDown(self): - """ - Overrides the TestCase._callTearDown method by passing it to be run remotely. - Notice None is passed as an argument instead of self. This is because only static methods - are allowed by the RPCClient. - """ - # notice None is passed as an argument instead of self so self can't be used - self.run_remotely(self.tearDown, [None]) - - def _callTestMethod(self, method): - """ - Overrides the TestCase._callTestMethod method by capturing the test case method that would be run and then - passing it to be run remotely. Notice no arguments are passed. This is because only static methods - are allowed by the RPCClient. - - :param callable method: A method from the test case. - """ - self.run_remotely(method, []) diff --git a/openpype/hosts/unreal/remote/rpc/unreal_server.py b/openpype/hosts/unreal/remote/rpc/unreal_server.py deleted file mode 100644 index 9ab598c5966..00000000000 --- a/openpype/hosts/unreal/remote/rpc/unreal_server.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright Epic Games, Inc. All Rights Reserved. - -import os - -from . import base_server -from .base_server import BaseRPCServerThread, BaseRPCServerManager - - -class UnrealRPCServerThread(BaseRPCServerThread): - def thread_safe_call(self, callable_instance, *args): - """ - Implementation of a thread safe call in Unreal. - """ - return lambda *args: base_server.run_in_main_thread(callable_instance, *args) - - -class RPCServer(BaseRPCServerManager): - def __init__(self): - """ - Initialize the unreal rpc server, with its name and specific port. - """ - super(RPCServer, self).__init__() - self.name = 'UnrealRPCServer' - self.port = int(os.environ.get('RPC_PORT', 9998)) - self.threaded_server_class = UnrealRPCServerThread - - def start_server_thread(self): - """ - Starts the server thread. - """ - # TODO use a timer exposed from FTSTicker instead of slate tick, less aggressive and safer - # https://docs.unrealengine.com/4.27/en-US/PythonAPI/class/AutomationScheduler.html?highlight=automationscheduler#unreal.AutomationScheduler - import unreal - unreal.register_slate_post_tick_callback(base_server.execute_queued_calls) - super(RPCServer, self).start_server_thread() diff --git a/openpype/hosts/unreal/remote/rpc/validations.py b/openpype/hosts/unreal/remote/rpc/validations.py deleted file mode 100644 index e4a95877005..00000000000 --- a/openpype/hosts/unreal/remote/rpc/validations.py +++ /dev/null @@ -1,105 +0,0 @@ -import inspect - -from .exceptions import ( - InvalidClassMethod, - InvalidTestCasePort, - InvalidKeyWordParameters, - UnsupportedArgumentType, - FileNotSavedOnDisk, -) - - -def get_source_file_path(function): - """ - Gets the full path to the source code. - - :param callable function: A callable. - :return str: A file path. - """ - client_module = inspect.getmodule(function) - return client_module.__file__ - - -def get_line_link(function): - """ - Gets the line number of a function. - - :param callable function: A callable. - :return int: The line number - """ - lines, line_number = inspect.getsourcelines(function) - file_path = get_source_file_path(function) - return f' File "{file_path}", line {line_number}' - - -def validate_arguments(function, args): - """ - Validates arguments to ensure they are a supported type. - - :param callable function: A function reference. - :param tuple(Any) args: A list of arguments. - """ - supported_types = [str, int, float, tuple, list, dict, bool] - line_link = get_line_link(function) - for arg in args: - if arg is None: - continue - - if type(arg) not in supported_types: - raise UnsupportedArgumentType(function, arg, supported_types, line_link=line_link) - - -def validate_test_case_class(cls): - """ - This is use to validate a subclass of RPCTestCase. While building your test - suite you can call this method on each class preemptively to validate that it - was defined correctly. - - :param RPCTestCase cls: A class. - :param str file_path: Optionally, a file path to the test case can be passed to give - further context into where the error is occurring. - """ - line_link = get_line_link(cls) - if not cls.__dict__.get('port'): - raise InvalidTestCasePort(cls, line_link=line_link) - - for attribute, method in cls.__dict__.items(): - if callable(method) and not isinstance(method, staticmethod): - if method.__name__.startswith('test'): - raise InvalidClassMethod(cls, method, line_link=line_link) - - -def validate_class_method(cls, method): - """ - Validates a method on a class. - - :param Any cls: A class. - :param callable method: A callable. - """ - if callable(method) and not isinstance(method, staticmethod): - line_link = get_line_link(method) - raise InvalidClassMethod(cls, method, line_link=line_link) - - -def validate_key_word_parameters(function, kwargs): - """ - Validates a method on a class. - - :param callable function: A callable. - :param dict kwargs: A dictionary of key word arguments. - """ - if kwargs: - line_link = get_line_link(function) - raise InvalidKeyWordParameters(function, kwargs, line_link=line_link) - - -def validate_file_is_saved(function): - """ - Validates that the file that the function is from is saved on disk. - - :param callable function: A callable. - """ - try: - inspect.getsourcelines(function) - except OSError: - raise FileNotSavedOnDisk(function) diff --git a/openpype/hosts/unreal/remote/unreal.py b/openpype/hosts/unreal/remote/unreal.py deleted file mode 100644 index f01c4368e8e..00000000000 --- a/openpype/hosts/unreal/remote/unreal.py +++ /dev/null @@ -1,992 +0,0 @@ -# Copyright Epic Games, Inc. All Rights Reserved. - -import os -import json -import time -import sys -import inspect -from http.client import RemoteDisconnected - -sys.path.append(os.path.dirname(__file__)) -import rpc.factory -import remote_execution - -try: - import unreal -except ModuleNotFoundError: - pass - -REMAP_PAIRS = [] -UNREAL_PORT = int(os.environ.get('UNREAL_PORT', 9998)) - -# use a different remap pairs when inside a container -if os.environ.get('TEST_ENVIRONMENT'): - UNREAL_PORT = int(os.environ.get('UNREAL_PORT', 8998)) - REMAP_PAIRS = [(os.environ.get('HOST_REPO_FOLDER'), os.environ.get('CONTAINER_REPO_FOLDER'))] - -# this defines a the decorator that makes function run as remote call in unreal -remote_unreal_decorator = rpc.factory.remote_call( - port=UNREAL_PORT, - default_imports=['import unreal'], - remap_pairs=REMAP_PAIRS, -) -rpc_client = rpc.client.RPCClient(port=UNREAL_PORT) -unreal_response = '' - - -def get_response(): - """ - Gets the stdout produced by the remote python call. - - :return str: The stdout produced by the remote python command. - """ - if unreal_response: - full_output = [] - output = unreal_response.get('output') - if output: - full_output.append('\n'.join([line['output'] for line in output if line['type'] != 'Warning'])) - - result = unreal_response.get('result') - if result != 'None': - full_output.append(result) - - return '\n'.join(full_output) - return '' - - -def add_indent(commands, indent): - """ - Adds an indent to the list of python commands. - - :param list commands: A list of python commands that will be run by unreal engine. - :param str indent: A str of tab characters. - :return str: A list of python commands that will be run by unreal engine. - """ - indented_line = [] - for command in commands: - for line in command.split('\n'): - indented_line.append(f'{indent}{line}') - - return indented_line - - -def print_python(commands): - """ - Prints the list of commands as formatted output for debugging and development. - - :param list commands: A list of python commands that will be run by unreal engine. - """ - if os.environ.get('REMOTE_EXECUTION_SHOW_PYTHON'): - dashes = '-' * 50 - label = 'Remote Execution' - sys.stdout.write(f'{dashes}{label}{dashes}\n') - - # get the function name - current_frame = inspect.currentframe() - caller_frame = inspect.getouterframes(current_frame, 2) - function_name = caller_frame[3][3] - - kwargs = caller_frame[3][0].f_locals - kwargs.pop('commands', None) - kwargs.pop('result', None) - - # write out the function name and its arguments - sys.stdout.write( - f'{function_name}(kwargs={json.dumps(kwargs, indent=2, default=lambda element: type(element).__name__)})\n') - - # write out the code with the lines numbers - for index, line in enumerate(commands, 1): - sys.stdout.write(f'{index} {line}\n') - - sys.stdout.write(f'{dashes}{"-" * len(label)}{dashes}\n') - - -def run_unreal_python_commands(remote_exec, commands, failed_connection_attempts=0): - """ - Finds the open unreal editor with remote connection enabled, and sends it python commands. - - :param object remote_exec: A RemoteExecution instance. - :param list commands: A list of python commands that will be run by unreal engine. - :param int failed_connection_attempts: A counter that keeps track of how many times an editor connection attempt - was made. - """ - if failed_connection_attempts == 0: - print_python(commands) - - # wait a tenth of a second before attempting to connect - time.sleep(0.1) - try: - # try to connect to an editor - for node in remote_exec.remote_nodes: - remote_exec.open_command_connection(node.get("node_id")) - - # if a connection is made - if remote_exec.has_command_connection(): - # run the import commands and save the response in the global unreal_response variable - global unreal_response - unreal_response = remote_exec.run_command('\n'.join(commands), unattended=False) - - # otherwise make an other attempt to connect to the engine - else: - if failed_connection_attempts < 50: - run_unreal_python_commands(remote_exec, commands, failed_connection_attempts + 1) - else: - remote_exec.stop() - raise ConnectionError("Could not find an open Unreal Editor instance!") - - # catch all errors - except: - raise ConnectionError("Could not find an open Unreal Editor instance!") - - # shutdown the connection - finally: - remote_exec.stop() - - return get_response() - - -def run_commands(commands): - """ - Runs a list of python commands and returns the result of the output. - - :param list commands: A formatted string of python commands that will be run by unreal engine. - :return str: The stdout produced by the remote python command. - """ - # wrap the commands in a try except so that all exceptions can be logged in the output - commands = ['try:'] + add_indent(commands, '\t') + ['except Exception as error:', '\tprint(error)'] - - # start a connection to the engine that lets you send python-commands.md strings - remote_exec = remote_execution.RemoteExecution() - remote_exec.start() - - # send over the python code as a string and run it - return run_unreal_python_commands(remote_exec, commands) - - -def is_connected(): - """ - Checks the rpc server connection - """ - try: - return rpc_client.proxy.is_running() - except (RemoteDisconnected, ConnectionRefusedError): - return False - - -def set_rpc_timeout(seconds): - """ - Sets the response timeout value of the unreal RPC server. - """ - rpc_client.proxy.set_env('RPC_TIME_OUT', seconds) - - -def bootstrap_unreal_with_rpc_server(): - """ - Bootstraps the running unreal editor with the unreal rpc server if it doesn't already exist. - """ - if not os.environ.get('TEST_ENVIRONMENT'): - if not is_connected(): - dependencies_path = os.path.dirname(__file__) - result = run_commands( - [ - 'import sys', - f'sys.path.append(r"{dependencies_path}")', - 'from rpc import unreal_server', - 'rpc_server = unreal_server.RPCServer()', - 'rpc_server.start(threaded=True)', - ] - ) - if result: - raise RuntimeError(result) - - -class Unreal: - @staticmethod - def get_value(value, unreal_type=None): - """ - Gets the value as an unreal type. - - :param Any value: A value that can be any generic python type. - :param str unreal_type: The name of an unreal type. - :return Any: The converted unreal value. - """ - if unreal_type == 'Array': - if isinstance(value, str): - value = value.split(',') - - if value: - array = unreal.Array(type(value[0])) - for element in value: - array.append(element) - return array - - elif unreal_type == 'Int32Interval': - int_32_interval = unreal.Int32Interval() - int_32_interval.set_editor_property("min", value[0]) - int_32_interval.set_editor_property("max", value[1]) - return int_32_interval - - elif unreal_type == 'Vector': - return unreal.Vector(x=value[0], y=value[1], z=value[2]) - - elif unreal_type == 'Rotator': - return unreal.Rotator(roll=value[0], pitch=value[1], yaw=value[2]) - - elif unreal_type == 'Color': - return unreal.Color(r=value[0], g=value[1], b=value[2], a=value[3]) - - elif unreal_type == 'Name': - return unreal.Name(value) - - elif unreal_type == 'SoftObjectPath': - return unreal.SoftObjectPath(path_string=value) - - elif unreal_type == 'Enum': - enum_value = unreal - for attribute in value.split('.')[1:]: - enum_value = getattr(enum_value, attribute) - return enum_value - - elif unreal_type == 'Asset': - if value: - return Unreal.get_asset(value) - else: - return None - else: - return value - - @staticmethod - def get_asset(asset_path): - """ - Adds the commands that load an unreal asset. - - :param str asset_path: The unreal project path of an asset. - :return str: A list of python commands that will be run by unreal engine. - """ - asset = unreal.load_asset(asset_path) - if not asset: - raise RuntimeError(f"The {asset_path} does not exist in the project!") - return asset - - @staticmethod - def set_settings(property_group, data_object): - """ - Sets a group of properties onto an unreal object. - - :param dict property_group: A dictionary of properties and their data. - :param object data_object: A object. - """ - for attribute, data in property_group.items(): - value = Unreal.get_value( - value=data.get('value'), - unreal_type=data.get('unreal_type'), - ) - data_object.set_editor_property(attribute, value) - return data_object - - @staticmethod - def object_attributes_to_dict(object_instance): - """ - Converts the attributes of the given python object to a dictionary. - - :param object object_instance: A object instance. - :return dict: A dictionary of attributes and values. - """ - data = {} - if object_instance: - for attribute in dir(object_instance): - value = getattr(object_instance, attribute) - if isinstance(value, (bool, str, float, int, list)) and not attribute.startswith("_"): - data[attribute] = getattr(object_instance, attribute) - return data - - -class UnrealImportAsset(Unreal): - def __init__(self, file_path, asset_data, property_data): - """ - Initializes the import with asset data and property data. - - :param str file_path: The full path to the file to import. - :param dict asset_data: A dictionary that contains various data about the asset. - :param PropertyData property_data: A property data instance that contains all property values of the tool. - """ - self._file_path = file_path - self._asset_data = asset_data - self._property_data = property_data - self._import_task = unreal.AssetImportTask() - self._options = None - - def set_skeleton(self): - """ - Sets a skeleton to the import options. - """ - skeleton_path = self._asset_data.get('skeleton_asset_path') - if skeleton_path: - self._options.skeleton = self.get_asset(skeleton_path) - - def set_physics_asset(self): - """ - Sets a physics asset to the import options. - """ - asset_path = self._asset_data.get('asset_path') - physics_asset_path = self._property_data.get('unreal_physics_asset_path', {}).get('value', '') - default_physics_asset = f'{asset_path}_PhysicsAsset' - # try to load the provided physics asset - if physics_asset_path: - physics_asset = unreal.load_asset(physics_asset_path) - else: - physics_asset = unreal.load_asset(default_physics_asset) - - if physics_asset: - self._options.create_physics_asset = False - self._options.physics_asset = physics_asset - else: - self._options.create_physics_asset = True - - def set_static_mesh_import_options(self): - """ - Sets the static mesh import options. - """ - if not self._asset_data.get('skeletal_mesh') and not self._asset_data.get('animation'): - self._options.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH - self._options.static_mesh_import_data.import_mesh_lo_ds = False - - import_data = unreal.FbxStaticMeshImportData() - self.set_settings( - self._property_data['unreal']['import_method']['fbx']['static_mesh_import_data'], - import_data - ) - self._options.static_mesh_import_data = import_data - - def set_skeletal_mesh_import_options(self): - """ - Sets the skeletal mesh import options. - """ - if self._asset_data.get('skeletal_mesh'): - self.set_skeleton() - self.set_physics_asset() - self._options.mesh_type_to_import = unreal.FBXImportType.FBXIT_SKELETAL_MESH - self._options.skeletal_mesh_import_data.import_mesh_lo_ds = False - import_data = unreal.FbxSkeletalMeshImportData() - self.set_settings( - self._property_data['unreal']['import_method']['fbx']['skeletal_mesh_import_data'], - import_data - ) - self._options.skeletal_mesh_import_data = import_data - - def set_animation_import_options(self): - """ - Sets the animation import options. - """ - if self._asset_data.get('animation'): - self.set_skeleton() - self.set_physics_asset() - self._options.mesh_type_to_import = unreal.FBXImportType.FBXIT_ANIMATION - import_data = unreal.FbxAnimSequenceImportData() - self.set_settings( - self._property_data['unreal']['import_method']['fbx']['anim_sequence_import_data'], - import_data, - ) - self._options.anim_sequence_import_data = import_data - - def set_texture_import_options(self): - """ - Sets the texture import options. - """ - if self._property_data.get('import_textures', {}).get('value', False): - import_data = unreal.FbxTextureImportData() - self.set_settings( - self._property_data['unreal']['import_method']['fbx']['texture_import_data'], - import_data - ) - self._options.texture_import_data = import_data - - def set_fbx_import_task_options(self): - """ - Sets the FBX import options. - """ - self._import_task.set_editor_property('filename', self._file_path) - self._import_task.set_editor_property('destination_path', self._asset_data.get('asset_folder')) - self._import_task.set_editor_property('replace_existing', True) - self._import_task.set_editor_property('replace_existing_settings', True) - self._import_task.set_editor_property( - 'automated', - not self._property_data.get('advanced_ui_import', {}).get('value', False) - ) - - import_materials_and_textures = self._property_data.get('import_materials_and_textures', {}).get('value', True) - - import_mesh = self._asset_data.get('import_mesh', False) - import_animations = self._asset_data.get('animation', False) - import_as_skeletal = self._asset_data.get('skeletal_mesh', False) - - # set the options - self._options = unreal.FbxImportUI() - self._options.set_editor_property('import_mesh', import_mesh) - self._options.set_editor_property('import_as_skeletal', import_as_skeletal) - self._options.set_editor_property('import_animations', import_animations) - self._options.set_editor_property('import_materials', import_materials_and_textures) - self._options.set_editor_property('import_textures', import_materials_and_textures) - - # set the static mesh import options - self.set_static_mesh_import_options() - - # add the skeletal mesh import options - self.set_skeletal_mesh_import_options() - - # add the animation import options - self.set_animation_import_options() - - # add the texture import options - self.set_texture_import_options() - - def run_import(self): - # assign the options object to the import task and import the asset - self._import_task.options = self._options - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([self._import_task]) - return list(self._import_task.get_editor_property('imported_object_paths')) - - -class UnrealImportSequence(Unreal): - def __init__(self, asset_path, file_path, track_name, start=None, end=None): - """ - Initializes the import with asset data and property data. - - :param str asset_path: The project path to the asset. - :param str file_path: The full file path to the import file. - :param str track_name: The name of the track. - :param int start: The start frame. - :param int end: The end frame. - """ - self._asset_path = asset_path - self._file_path = file_path - self._track_name = track_name - self._control_channel_mappings = [] - self._sequence = self.get_asset(asset_path) - self._control_rig_settings = unreal.MovieSceneUserImportFBXControlRigSettings() - - if start and end: - self._control_rig_settings.start_time_range = int(start) - self._control_rig_settings.end_time_range = int(end) - - @staticmethod - def get_control_rig_mappings(): - """ - Set the control rig mappings. - - :return list[tuple]: A list channels, transforms and negations that define the control rig mappings. - """ - return [ - (unreal.FControlRigChannelEnum.BOOL, unreal.FTransformChannelEnum.TRANSLATE_X, False), - (unreal.FControlRigChannelEnum.FLOAT, unreal.FTransformChannelEnum.TRANSLATE_Y, False), - (unreal.FControlRigChannelEnum.VECTOR2DX, unreal.FTransformChannelEnum.TRANSLATE_X, False), - (unreal.FControlRigChannelEnum.VECTOR2DY, unreal.FTransformChannelEnum.TRANSLATE_Y, False), - (unreal.FControlRigChannelEnum.POSITION_X, unreal.FTransformChannelEnum.TRANSLATE_X, False), - (unreal.FControlRigChannelEnum.POSITION_Y, unreal.FTransformChannelEnum.TRANSLATE_Y, False), - (unreal.FControlRigChannelEnum.POSITION_Z, unreal.FTransformChannelEnum.TRANSLATE_Z, False), - (unreal.FControlRigChannelEnum.ROTATOR_X, unreal.FTransformChannelEnum.ROTATE_X, False), - (unreal.FControlRigChannelEnum.ROTATOR_Y, unreal.FTransformChannelEnum.ROTATE_Y, False), - (unreal.FControlRigChannelEnum.ROTATOR_Z, unreal.FTransformChannelEnum.ROTATE_Z, False), - (unreal.FControlRigChannelEnum.SCALE_X, unreal.FTransformChannelEnum.SCALE_X, False), - (unreal.FControlRigChannelEnum.SCALE_Y, unreal.FTransformChannelEnum.SCALE_Y, False), - (unreal.FControlRigChannelEnum.SCALE_Z, unreal.FTransformChannelEnum.SCALE_Z, False) - - ] - - def set_control_mapping(self, control_channel, fbx_channel, negate): - """ - Sets the control mapping. - - :param str control_channel: The unreal enum of the control channel. - :param str fbx_channel: The unreal enum of the transform channel. - :param bool negate: Whether or not the mapping is negated. - :return str: A list of python commands that will be run by unreal engine. - """ - control_map = unreal.ControlToTransformMappings() - control_map.set_editor_property('control_channel', control_channel) - control_map.set_editor_property('fbx_channel', fbx_channel) - control_map.set_editor_property('negate', negate) - self._control_maps.append(control_map) - - def remove_level_sequence_keyframes(self): - """ - Removes all key frames from the given sequence and track name. - """ - bindings = {binding.get_name(): binding for binding in self._sequence.get_bindings()} - binding = bindings.get(self._track_name) - track = binding.get_tracks()[0] - section = track.get_sections()[0] - for channel in section.get_channels(): - for key in channel.get_keys(): - channel.remove_key(key) - - def run_import(self, import_type='control_rig'): - """ - Imports key frames onto the given sequence track name from a file. - - :param str import_type: What type of sequence import to run. - """ - sequencer_tools = unreal.SequencerTools() - self.remove_level_sequence_keyframes() - - if import_type == 'control_rig': - for control_channel, fbx_channel, negate in self.get_control_rig_mappings(): - self.set_control_mapping(control_channel, fbx_channel, negate) - - self._control_rig_settings.control_channel_mappings = self._control_channel_mappings - self._control_rig_settings.insert_animation = False - self._control_rig_settings.import_onto_selected_controls = False - sequencer_tools.import_fbx_to_control_rig( - world=unreal.EditorLevelLibrary.get_editor_world(), - sequence=self._sequence, - actor_with_control_rig_track=self._track_name, - selected_control_rig_names=[], - import_fbx_control_rig_settings=self._control_rig_settings, - import_filename=self._file_path - ) - - -@rpc.factory.remote_class(remote_unreal_decorator) -class UnrealRemoteCalls: - @staticmethod - def get_lod_count(asset_path): - """ - Gets the number of lods on the given asset. - - :param str asset_path: The path to the unreal asset. - :return int: The number of lods on the asset. - """ - lod_count = 0 - asset = Unreal.get_asset(asset_path) - if asset.__class__.__name__ == 'SkeletalMesh': - lod_count = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem).get_lod_count(asset) - - if asset.__class__.__name__ == 'StaticMesh': - lod_count = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem).get_lod_count(asset) - - return lod_count - - @staticmethod - def asset_exists(asset_path): - """ - Checks to see if an asset exist in unreal. - - :param str asset_path: The path to the unreal asset. - :return bool: Whether or not the asset exists. - """ - return bool(unreal.load_asset(asset_path)) - - @staticmethod - def directory_exists(asset_path): - """ - Checks to see if a directory exist in unreal. - - :param str asset_path: The path to the unreal asset. - :return bool: Whether or not the asset exists. - """ - # TODO fix this when the unreal API is fixed where it queries the registry correctly - # https://jira.it.epicgames.com/browse/UE-142234 - # return unreal.EditorAssetLibrary.does_directory_exist(asset_path) - return True - - @staticmethod - def get_static_mesh_collision_info(asset_path): - """ - Gets the number of convex and simple collisions on a static mesh. - - :param str asset_path: The path to the unreal asset. - :return str: The name of the complex collision. - """ - mesh = Unreal.get_asset(asset_path) - return { - 'simple': unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem).get_simple_collision_count(mesh), - 'convex': unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem).get_convex_collision_count(mesh), - 'customized': mesh.get_editor_property('customized_collision') - } - - @staticmethod - def get_material_index_by_name(asset_path, material_name): - """ - Checks to see if an asset has a complex collision. - - :param str asset_path: The path to the unreal asset. - :param str material_name: The name of the material. - :return str: The name of the complex collision. - """ - mesh = Unreal.get_asset(asset_path) - if mesh.__class__.__name__ == 'SkeletalMesh': - for index, material in enumerate(mesh.materials): - if material.material_slot_name == material_name: - return index - if mesh.__class__.__name__ == 'StaticMesh': - for index, material in enumerate(mesh.static_materials): - if material.material_slot_name == material_name: - return index - - @staticmethod - def has_socket(asset_path, socket_name): - """ - Checks to see if an asset has a socket. - - :param str asset_path: The path to the unreal asset. - :param str socket_name: The name of the socket to look for. - :return bool: Whether or not the asset has the given socket or not. - """ - mesh = Unreal.get_asset(asset_path) - return bool(mesh.find_socket(socket_name)) - - @staticmethod - def delete_asset(asset_path): - """ - Deletes an asset in unreal. - - :param str asset_path: The path to the unreal asset. - :return bool: Whether or not the asset was deleted. - """ - if unreal.EditorAssetLibrary.does_asset_exist(asset_path): - unreal.EditorAssetLibrary.delete_asset(asset_path) - - @staticmethod - def delete_directory(directory_path): - """ - Deletes an folder and its contents in unreal. - - :param str directory_path: The game path to the unreal project folder. - :return bool: Whether or not the directory was deleted. - """ - # API BUG:cant check if exists https://jira.it.epicgames.com/browse/UE-142234 - # if unreal.EditorAssetLibrary.does_directory_exist(directory_path): - unreal.EditorAssetLibrary.delete_directory(directory_path) - - @staticmethod - def import_asset(file_path, asset_data, property_data, file_type='fbx'): - """ - Imports an asset to unreal based on the asset data in the provided dictionary. - - :param str file_path: The full path to the file to import. - :param dict asset_data: A dictionary of import parameters. - :param dict property_data: A dictionary representation of the properties. - :param str file_type: The import file type. - """ - unreal_import_asset = UnrealImportAsset( - file_path=file_path, - asset_data=asset_data, - property_data=property_data - ) - if file_type.lower() == 'fbx': - unreal_import_asset.set_fbx_import_task_options() - - # run the import task - return unreal_import_asset.run_import() - - @staticmethod - def import_sequence_track(asset_path, file_path, track_name, start=None, end=None): - """ - Initializes the import with asset data and property data. - - :param str asset_path: The project path to the asset. - :param str file_path: The full file path to the import file. - :param str track_name: The name of the track. - :param int start: The start frame. - :param int end: The end frame. - """ - unreal_import_sequence = UnrealImportSequence( - asset_path=asset_path, - file_path=file_path, - track_name=track_name, - start=start, - end=start - ) - # run the import task - unreal_import_sequence.run_import() - - @staticmethod - def import_skeletal_mesh_lod(asset_path, file_path, index): - """ - Imports a lod onto a skeletal mesh. - - :param str asset_path: The project path to the skeletal mesh in unreal. - :param str file_path: The path to the file that contains the lods on disk. - :param int index: Which lod index to import the lod on. - """ - skeletal_mesh = Unreal.get_asset(asset_path) - skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) - result = skeletal_mesh_subsystem.import_lod(skeletal_mesh, index, file_path) - if result == -1: - raise RuntimeError(f"{file_path} import failed!") - - @staticmethod - def import_static_mesh_lod(asset_path, file_path, index): - """ - Imports a lod onto a static mesh. - - :param str asset_path: The project path to the skeletal mesh in unreal. - :param str file_path: The path to the file that contains the lods on disk. - :param int index: Which lod index to import the lod on. - """ - static_mesh = Unreal.get_asset(asset_path) - static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) - result = static_mesh_subsystem.import_lod(static_mesh, index, file_path) - if result == -1: - raise RuntimeError(f"{file_path} import failed!") - - @staticmethod - def set_skeletal_mesh_lod_build_settings(asset_path, index, property_data): - """ - Sets the lod build settings for skeletal mesh. - - :param str asset_path: The project path to the skeletal mesh in unreal. - :param int index: Which lod index to import the lod on. - :param dict property_data: A dictionary representation of the properties. - """ - skeletal_mesh = Unreal.get_asset(asset_path) - skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) - options = unreal.SkeletalMeshBuildSettings() - options = Unreal.set_settings( - property_data['unreal']['editor_skeletal_mesh_library']['lod_build_settings'], - options - ) - skeletal_mesh_subsystem.set_lod_build_settings(skeletal_mesh, index, options) - - @staticmethod - def set_static_mesh_lod_build_settings(asset_path, index, property_data): - """ - Sets the lod build settings for static mesh. - - :param str asset_path: The project path to the static mesh in unreal. - :param int index: Which lod index to import the lod on. - :param dict property_data: A dictionary representation of the properties. - """ - static_mesh = Unreal.get_asset(asset_path) - static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) - options = unreal.MeshBuildSettings() - options = Unreal.set_settings( - property_data['unreal']['editor_static_mesh_library']['lod_build_settings'], - options - ) - static_mesh_subsystem.set_lod_build_settings(static_mesh, index, options) - - @staticmethod - def reset_skeletal_mesh_lods(asset_path, property_data): - """ - Removes all lods on the given skeletal mesh. - - :param str asset_path: The project path to the skeletal mesh in unreal. - :param dict property_data: A dictionary representation of the properties. - """ - skeletal_mesh = Unreal.get_asset(asset_path) - skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) - lod_count = skeletal_mesh_subsystem.get_lod_count(skeletal_mesh) - if lod_count > 1: - skeletal_mesh_subsystem.remove_lo_ds(skeletal_mesh, list(range(1, lod_count))) - - lod_settings_path = property_data.get('unreal_skeletal_mesh_lod_settings_path', {}).get('value', '') - if lod_settings_path: - data_asset = Unreal.get_asset(asset_path) - skeletal_mesh.lod_settings = data_asset - skeletal_mesh_subsystem.regenerate_lod(skeletal_mesh, new_lod_count=lod_count) - - @staticmethod - def reset_static_mesh_lods(asset_path): - """ - Removes all lods on the given static mesh. - - :param str asset_path: The project path to the static mesh in unreal. - """ - static_mesh = Unreal.get_asset(asset_path) - static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) - lod_count = static_mesh_subsystem.get_lod_count(static_mesh) - if lod_count > 1: - static_mesh_subsystem.remove_lods(static_mesh) - - @staticmethod - def set_static_mesh_sockets(asset_path, asset_data): - """ - Sets sockets on a static mesh. - - :param str asset_path: The project path to the skeletal mesh in unreal. - :param dict asset_data: A dictionary of import parameters. - """ - static_mesh = Unreal.get_asset(asset_path) - for socket_name, socket_data in asset_data.get('sockets').items(): - socket = unreal.StaticMeshSocket() - - # apply the socket settings - socket.set_editor_property('relative_location', socket_data.get('relative_location')) - socket.set_editor_property('relative_rotation', socket_data.get('relative_rotation')) - socket.set_editor_property('relative_scale', socket_data.get('relative_scale')) - socket.set_editor_property('socket_name', socket_name) - - # if that socket already exists remove it - existing_socket = static_mesh.find_socket(socket_name) - if existing_socket: - static_mesh.remove_socket(existing_socket) - - # create a new socket - static_mesh.add_socket(socket) - - @staticmethod - def get_lod_build_settings(asset_path, index): - """ - Gets the lod build settings from the given asset. - - :param str asset_path: The project path to the asset. - :param int index: The lod index to check. - :return dict: A dictionary of lod build settings. - """ - build_settings = None - mesh = Unreal.get_asset(asset_path) - if not mesh: - raise RuntimeError(f'"{asset_path}" was not found in the unreal project!') - if mesh.__class__.__name__ == 'SkeletalMesh': - skeletal_mesh_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem) - build_settings = skeletal_mesh_subsystem.get_lod_build_settings(mesh, index) - if mesh.__class__.__name__ == 'StaticMesh': - static_mesh_subsystem = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) - build_settings = static_mesh_subsystem.get_lod_build_settings(mesh, index) - - return Unreal.object_attributes_to_dict(build_settings) - - @staticmethod - def get_bone_path_to_root(asset_path, bone_name): - """ - Gets the path to the root bone from the given skeleton. - - :param str asset_path: The project path to the asset. - :param str bone_name: The name of the bone to start from. - :return list: A list of bone name all the way to the root bone. - """ - animation = Unreal.get_asset(asset_path) - path = unreal.AnimationLibrary.find_bone_path_to_root(animation, bone_name) - return [str(i) for i in path] - - @staticmethod - def get_bone_transform_for_frame(asset_path, bone_name, frame): - """ - Gets the transformations of the given bone on the given frame. - - :param str asset_path: The project path to the asset. - :param str bone_name: The name of the bone to get the transforms of. - :param float frame: The frame number. - :return dict: A dictionary of transformation values. - """ - animation = Unreal.get_asset(asset_path) - path = unreal.AnimationLibrary.find_bone_path_to_root(animation, bone_name) - transform = unreal.AnimationLibrary.get_bone_pose_for_frame(animation, bone_name, frame, True) - world_rotation = unreal.Rotator() - world_location = unreal.Transform() - for bone in path: - bone_transform = unreal.AnimationLibrary.get_bone_pose_for_frame(animation, str(bone), frame, True) - world_rotation = world_rotation.combine(bone_transform.rotation.rotator()) - world_location = world_location.multiply(bone_transform) - - return { - 'scale': transform.scale3d.to_tuple(), - 'world_rotation': world_rotation.transform().rotation.euler().to_tuple(), - 'local_rotation': transform.rotation.euler().to_tuple(), - 'world_location': world_location.translation.to_tuple(), - 'local_location': transform.translation.to_tuple() - } - - @staticmethod - def get_bone_count(skeleton_path): - """ - Gets the bone count from the given skeleton. - - :param str skeleton_path: The project path to the skeleton. - :return int: The number of bones. - """ - skeleton = unreal.load_asset(skeleton_path) - return len(skeleton.get_editor_property('bone_tree')) - - @staticmethod - def get_origin(asset_path): - """ - Gets the location of the assets origin. - - :param str asset_path: The project path to the asset. - :return list: A list of bone name all the way to the root bone. - """ - mesh = Unreal.get_asset(asset_path) - return mesh.get_bounds().origin.to_tuple() - - @staticmethod - def get_sequence_track_keyframe(asset_path, track_name, curve_name, frame): - """ - Gets the transformations of the given bone on the given frame. - - :param str asset_path: The project path to the asset. - :param str track_name: The name of the track. - :param str curve_name: The curve name. - :param float frame: The frame number. - :return dict: A dictionary of transformation values. - """ - sequence = unreal.load_asset(asset_path) - bindings = {binding.get_name(): binding for binding in sequence.get_bindings()} - binding = bindings.get(track_name) - track = binding.get_tracks()[0] - section = track.get_sections()[0] - data = {} - for channel in section.get_channels(): - if channel.get_name().startswith(curve_name): - for key in channel.get_keys(): - if key.get_time().frame_number.value == frame: - data[channel.get_name()] = key.get_value() - return data - - @staticmethod - def create_asset(asset_path, asset_class=None, asset_factory=None, unique_name=True): - """ - Creates a new unreal asset. - - :param str asset_path: The project path to the asset. - :param str asset_class: The name of the unreal asset class. - :param str asset_factory: The name of the unreal factory. - :param bool unique_name: Whether or not the check if the name is unique before creating the asset. - """ - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - if unique_name: - asset_path, _ = asset_tools.create_unique_asset_name( - base_package_name=asset_path, - suffix='' - ) - path = asset_path.rsplit("/", 1)[0] - name = asset_path.rsplit("/", 1)[1] - asset_tools.create_asset( - asset_name=name, - package_path=path, - asset_class=asset_class, - factory=asset_factory - ) - - @staticmethod - def create_folder(folder_path): - unreal.EditorAssetLibrary.make_directory(folder_path) - - @staticmethod - def import_animation_fcurves(asset_path, fcurve_file_path): - """ - Imports fcurves from a file onto an animation sequence. - - :param str asset_path: The project path to the skeletal mesh in unreal. - :param str fcurve_file_path: The file path to the fcurve file. - """ - animation_sequence = Unreal.get_asset(asset_path) - with open(fcurve_file_path, 'r') as fcurve_file: - fcurve_data = json.load(fcurve_file) - - for fcurve_name, keys in fcurve_data.items(): - unreal.AnimationLibrary.add_curve(animation_sequence, fcurve_name) - for key in keys: - unreal.AnimationLibrary.add_float_curve_key(animation_sequence, fcurve_name, key[0], key[1]) - - @staticmethod - def does_curve_exist(asset_path, curve_name): - """ - Checks if the fcurve exists on the animation sequence. - - :param str asset_path: The project path to the skeletal mesh in unreal. - :param str curve_name: The fcurve name. - """ - animation_sequence = Unreal.get_asset(asset_path) - return unreal.AnimationLibrary.does_curve_exist(animation_sequence, curve_name, unreal.RawCurveTrackTypes.RCT_FLOAT) From 0304d63054881b658f31dcd943fa2118e7aacaf1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 21 Oct 2022 17:41:42 +0100 Subject: [PATCH 11/55] Comments and code refinement --- .../Private/OpenPypeCommunication.cpp | 231 ++++++++++-------- .../OpenPype/Public/OpenPypeCommunication.h | 12 +- 2 files changed, 134 insertions(+), 109 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp index 17ebe84a1c5..2e11e39a589 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -12,7 +12,7 @@ int32 FOpenPypeCommunication::Id = 0; void FOpenPypeCommunication::CreateSocket() { - UE_LOG(LogTemp, Display, TEXT("Starting web socket...")); + UE_LOG(LogTemp, Display, TEXT("Creating web socket...")); FString url = FWindowsPlatformMisc::GetEnvironmentVariable(*FString("WEBSOCKET_URL")); @@ -21,23 +21,22 @@ void FOpenPypeCommunication::CreateSocket() const FString ServerURL = url; const FString ServerProtocol = TEXT("ws"); - TMap UpgradeHeaders; - UpgradeHeaders.Add(TEXT("upgrade"), TEXT("websocket")); - + // We initialize the Id to 0. This is used during the communication to + // identify the message and the responses. Id = 0; - Socket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol, UpgradeHeaders); + Socket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol); } void FOpenPypeCommunication::ConnectToSocket() { + // Handle delegates for the socket. Socket->OnConnected().AddStatic(&FOpenPypeCommunication::OnConnected); Socket->OnConnectionError().AddStatic(&FOpenPypeCommunication::OnConnectionError); Socket->OnClosed().AddStatic(&FOpenPypeCommunication::OnClosed); Socket->OnMessage().AddStatic(&FOpenPypeCommunication::OnMessage); - Socket->OnRawMessage().AddStatic(&FOpenPypeCommunication::OnRawMessage); Socket->OnMessageSent().AddStatic(&FOpenPypeCommunication::OnMessageSent); - UE_LOG(LogTemp, Display, TEXT("Connecting web socket to server...")); + UE_LOG(LogTemp, Display, TEXT("Attempting to connect to web socket server...")); Socket->Connect(); } @@ -56,7 +55,7 @@ void FOpenPypeCommunication::CallMethod(const FString Method, const TArrayIsConnected()) { - UE_LOG(LogTemp, Display, TEXT("Calling method \"%s\"..."), *Method); + UE_LOG(LogTemp, Verbose, TEXT("Calling method \"%s\"..."), *Method); int32 newId = Id++; @@ -75,13 +74,13 @@ void FOpenPypeCommunication::CallMethod(const FString Method, const TArrayClose(). - UE_LOG(LogTemp, Warning, TEXT("Closed")); + UE_LOG(LogTemp, Warning, TEXT("Closed connection to web socket server.")); } void FOpenPypeCommunication::OnMessage(const FString & Message) { // This code will run when we receive a string message from the server. - UE_LOG(LogTemp, Display, TEXT("Message received: %s"), *Message); + UE_LOG(LogTemp, Verbose, TEXT("Message received: \"%s\"."), *Message); TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(Message); TSharedPtr Root; if (FJsonSerializer::Deserialize(Reader, Root)) { + // Depending on the fields of the received message, we handle the + // message differently. if (Root->HasField(TEXT("method"))) { - FString Method = Root->GetStringField(TEXT("method")); - UE_LOG(LogTemp, Display, TEXT("Method: %s"), *Method); - - if (Method == "ls") - { - FString Result = UOpenPypePythonBridge::Get()->ls(); - - UE_LOG(LogTemp, Display, TEXT("Result: %s"), *Result); - - FString StringResponse; - FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; - FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); - - Socket->Send(StringResponse); - } - else if (Method == "containerise") - { - auto params = Root->GetArrayField(TEXT("params")); - - FString Name = params[0]->AsString(); - FString Namespace = params[1]->AsString(); - FString Nodes = params[2]->AsString(); - FString Context = params[3]->AsString(); - FString Loader = params.Num() >= 5 ? params[4]->AsString() : TEXT(""); - FString Suffix = params.Num() == 6 ? params[5]->AsString() : TEXT("_CON"); - - UE_LOG(LogTemp, Display, TEXT("Name: %s"), *Name); - UE_LOG(LogTemp, Display, TEXT("Namespace: %s"), *Namespace); - UE_LOG(LogTemp, Display, TEXT("Nodes: %s"), *Nodes); - UE_LOG(LogTemp, Display, TEXT("Context: %s"), *Context); - UE_LOG(LogTemp, Display, TEXT("Loader: %s"), *Loader); - UE_LOG(LogTemp, Display, TEXT("Suffix: %s"), *Suffix); - - FString Result = UOpenPypePythonBridge::Get()->containerise(Name, Namespace, Nodes, Context, Loader, Suffix); - - UE_LOG(LogTemp, Display, TEXT("Result: %s"), *Result); - - FString StringResponse; - FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; - FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); - - Socket->Send(StringResponse); - } - else if (Method == "instantiate") - { - auto params = Root->GetArrayField(TEXT("params")); - - FString RootParam = params[0]->AsString(); - FString Name = params[1]->AsString(); - FString Data = params[2]->AsString(); - FString Assets = params.Num() >= 4 ? params[3]->AsString() : TEXT(""); - FString Suffix = params.Num() == 5 ? params[4]->AsString() : TEXT("_INS"); - - UE_LOG(LogTemp, Display, TEXT("Root: %s"), *RootParam); - UE_LOG(LogTemp, Display, TEXT("Name: %s"), *Name); - UE_LOG(LogTemp, Display, TEXT("Data: %s"), *Data); - UE_LOG(LogTemp, Display, TEXT("Assets: %s"), *Assets); - UE_LOG(LogTemp, Display, TEXT("Suffix: %s"), *Suffix); - - UOpenPypePythonBridge::Get()->instantiate(RootParam, Name, Data, Assets, Suffix); - - FString StringResponse; - FRpcResponseResult RpcResponse = { "2.0", "", Root->GetIntegerField(TEXT("id")) }; - FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); - - Socket->Send(StringResponse); - } + RunMethod(Root); } else if (Root->HasField(TEXT("result"))) { - FString OutputMessage; - if (Root->TryGetStringField(TEXT("result"), OutputMessage)) - { - UE_LOG(LogTemp, Display, TEXT("Result: %s"), *OutputMessage); - } - else - { - UE_LOG(LogTemp, Display, TEXT("Function call successful without return value")); - } + HandleResult(Root); } else if (Root->HasField(TEXT("error"))) { - auto Error = Root->GetObjectField(TEXT("error")); - - if (Error->HasField(TEXT("message"))) - { - FString ErrorMessage; - Error->TryGetStringField(TEXT("message"), ErrorMessage); - UE_LOG(LogTemp, Error, TEXT("Error: %s"), *ErrorMessage); - } - else - { - UE_LOG(LogTemp, Error, TEXT("Error during parsing error")); - } + HandleError(Root); } else { @@ -210,14 +126,117 @@ void FOpenPypeCommunication::OnMessage(const FString & Message) } } -void FOpenPypeCommunication::OnRawMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining) +void FOpenPypeCommunication::OnMessageSent(const FString& MessageString) { - // This code will run when we receive a raw (binary) message from the server. - UE_LOG(LogTemp, Display, TEXT("Raw message received")); + // This code is called after we sent a message to the server. + UE_LOG(LogTemp, Verbose, TEXT("Message sent: %s"), *MessageString); } -void FOpenPypeCommunication::OnMessageSent(const FString& MessageString) +void FOpenPypeCommunication::HandleResult(TSharedPtr Root) { - // This code is called after we sent a message to the server. - UE_LOG(LogTemp, Display, TEXT("Message sent: %s"), *MessageString); + // This code is called when we receive a result from the server. + UE_LOG(LogTemp, Verbose, TEXT("Getting a result.")); + + FString OutputMessage; + if (Root->TryGetStringField(TEXT("result"), OutputMessage)) + { + UE_LOG(LogTemp, Verbose, TEXT("Result: %s"), *OutputMessage); + } + else + { + UE_LOG(LogTemp, Verbose, TEXT("Function call successful without return value")); + } +} + +void FOpenPypeCommunication::HandleError(TSharedPtr Root) +{ + // This code is called when we receive an error from the server. + UE_LOG(LogTemp, Verbose, TEXT("Getting an error.")); + + auto Error = Root->GetObjectField(TEXT("error")); + + if (Error->HasField(TEXT("message"))) + { + FString ErrorMessage; + Error->TryGetStringField(TEXT("message"), ErrorMessage); + UE_LOG(LogTemp, Error, TEXT("Error: %s"), *ErrorMessage); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Error during parsing error")); + } +} + +void FOpenPypeCommunication::RunMethod(TSharedPtr Root) +{ + // This code is called when we receive the request to run a method from the server. + FString Method = Root->GetStringField(TEXT("method")); + UE_LOG(LogTemp, Verbose, TEXT("Calling a function: %s"), *Method); + + if (Method == "ls") + { + ls(Root); + } + else if (Method == "containerise") + { + containerise(Root); + } + else if (Method == "instantiate") + { + instantiate(Root); + } +} + +void FOpenPypeCommunication::ls(TSharedPtr Root) +{ + FString Result = UOpenPypePythonBridge::Get()->ls(); + + UE_LOG(LogTemp, Verbose, TEXT("Result: %s"), *Result); + + FString StringResponse; + FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + + Socket->Send(StringResponse); +} + +void FOpenPypeCommunication::containerise(TSharedPtr Root) +{ + auto params = Root->GetArrayField(TEXT("params")); + + FString Name = params[0]->AsString(); + FString Namespace = params[1]->AsString(); + FString Nodes = params[2]->AsString(); + FString Context = params[3]->AsString(); + FString Loader = params.Num() >= 5 ? params[4]->AsString() : TEXT(""); + FString Suffix = params.Num() == 6 ? params[5]->AsString() : TEXT("_CON"); + + FString Result = UOpenPypePythonBridge::Get()->containerise(Name, Namespace, Nodes, Context, Loader, Suffix); + + UE_LOG(LogTemp, Verbose, TEXT("Result: %s"), *Result); + + FString StringResponse; + FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + + Socket->Send(StringResponse); +} + +void FOpenPypeCommunication::instantiate(TSharedPtr Root) +{ + auto params = Root->GetArrayField(TEXT("params")); + + FString RootParam = params[0]->AsString(); + FString Name = params[1]->AsString(); + FString Data = params[2]->AsString(); + FString Assets = params.Num() >= 4 ? params[3]->AsString() : TEXT(""); + FString Suffix = params.Num() == 5 ? params[4]->AsString() : TEXT("_INS"); + + UOpenPypePythonBridge::Get()->instantiate(RootParam, Name, Data, Assets, Suffix); + + FString StringResponse; + FRpcResponseResult RpcResponse = { "2.0", "", Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + + Socket->Send(StringResponse); } diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h index 495eb2bdec2..b4ae1d77a26 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h @@ -99,12 +99,18 @@ class FOpenPypeCommunication UFUNCTION() static void OnMessage(const FString & Message); - UFUNCTION() - static void OnRawMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining); - UFUNCTION() static void OnMessageSent(const FString& MessageString); +private: + static void HandleResult(TSharedPtr Root); + static void HandleError(TSharedPtr Root); + static void RunMethod(TSharedPtr Root); + + static void ls(TSharedPtr Root); + static void containerise(TSharedPtr Root); + static void instantiate(TSharedPtr Root); + private: static TSharedPtr Socket; static int32 Id; From d5ed97b08fe7248f16bdfb10045f0ebb73cd9b3e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Oct 2022 16:03:15 +0100 Subject: [PATCH 12/55] Changed logic for execution of python code from websocket requests --- .../UE_5.0/Content/Python/__init__.py | 0 .../UE_5.0/Content/Python/init_unreal.py | 313 +----------------- .../UE_5.0/Content/Python/pipeline.py | 308 +++++++++++++++++ .../UE_5.0/Source/OpenPype/OpenPype.Build.cs | 1 + .../Source/OpenPype/Private/OpenPype.cpp | 1 - .../Private/OpenPypeCommunication.cpp | 90 ++--- .../OpenPype/Private/OpenPypePythonBridge.cpp | 13 - .../OpenPype/Public/OpenPypeCommunication.h | 4 - .../OpenPype/Public/OpenPypePythonBridge.h | 22 -- 9 files changed, 349 insertions(+), 403 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 24ec4fad013..0cb3ad0886c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -1,308 +1,5 @@ -import ast -from typing import List - -import unreal - - -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 parse_container(container): - """To get data from container, AssetContainer 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 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("OpenPype containerising"): - unreal.EditorAssetLibrary.save_asset(node) - -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("{}/{}".format(root, name)): - name = "{}{}".format(name, index) - index += 1 - else: - eal.make_directory("{}/{}".format(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, "{}/{}/{}".format(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.AssetContainerFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() - - asset = tools.create_asset(container, path, None, factory) - return asset - - -def create_publish_instance(instance: str, path: str) -> unreal.Object: - """Helper function to create OpenPype Publish Instance on given path. - - This behaves similarly as :func:`create_openpype_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.OpenPypePublishInstanceFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(instance, path, None, factory) - return asset - -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 = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - break - if subscene_track is not None and subscene_track.get_sections(): - return subscene_track.get_sections() - return [] - - -@unreal.uclass() -class OpenPypeIntegration(unreal.OpenPypePythonBridge): - """OpenPype integration for Unreal Engine 5.0.""" - - @unreal.ufunction(override=True) - def ls(self): - """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() - openpype_containers = ar.get_assets_by_class("AssetContainer", True) - - containers = [] - - # 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 openpype_containers: - asset = asset_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset_data.asset_name - data = cast_map_to_str_dict(data) - - containers.append(data) - - return str(containers) - - @unreal.ufunction(override=True) - def containerise(self, name, namespc, str_nodes, str_context, loader="", 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. OpenPype 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/OpenPype/Test/TestMaterial.TestMaterial` - - """ - namespace = namespc - nodes = ast.literal_eval(str_nodes) - context = ast.literal_eval(str_context) - if loader == "": - loader = None - # 1 - create directory for container - root = "/Game" - container_name = "{}{}".format(name, suffix) - new_name = move_assets_to_path(root, container_name, nodes) - - # 2 - create Asset Container there - path = "{}/{}".format(root, new_name) - create_container(container=container_name, path=path) - - namespace = path - - data = { - "schema": "openpype:container-2.0", - "id": "pyblish.avalon.container", - "name": new_name, - "namespace": namespace, - "loader": str(loader), - "representation": context["representation"]["_id"], - } - # 3 - imprint data - imprint("{}/{}".format(path, container_name), data) - return path - - @unreal.ufunction(override=True) - def instantiate(self, root, name, str_data, str_assets="", suffix="_INS"): - """Bundles *nodes* into *container*. - - Marking it with metadata as publishable instance. If assets are provided, - they are moved to new path where `OpenPypePublishInstance` 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 - - """ - data = ast.literal_eval(str_data) - assets = ast.literal_eval(str_assets) - container_name = "{}{}".format(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 = "{}/{}".format(root, new_name) - create_publish_instance(instance=container_name, path=path) - - imprint("{}/{}".format(path, container_name), data) +from pipeline import ( + ls, + containerise, + instantiate, +) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py new file mode 100644 index 00000000000..64086f429de --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py @@ -0,0 +1,308 @@ +import ast +from typing import List + +import unreal + + +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 parse_container(container): + """To get data from container, AssetContainer 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 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("OpenPype containerising"): + unreal.EditorAssetLibrary.save_asset(node) + + +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("{}/{}".format(root, name)): + name = "{}{}".format(name, index) + index += 1 + else: + eal.make_directory("{}/{}".format(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, "{}/{}/{}".format(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.AssetContainerFactory() + tools = unreal.AssetToolsHelpers().get_asset_tools() + + asset = tools.create_asset(container, path, None, factory) + return asset + + +def create_publish_instance(instance: str, path: str) -> unreal.Object: + """Helper function to create OpenPype Publish Instance on given path. + + This behaves similarly as :func:`create_openpype_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.OpenPypePublishInstanceFactory() + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset = tools.create_asset(instance, path, None, factory) + return asset + + +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 = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + break + if subscene_track is not None and subscene_track.get_sections(): + return subscene_track.get_sections() + return [] + + +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() + openpype_containers = ar.get_assets_by_class("AssetContainer", True) + + containers = [] + + # 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 openpype_containers: + asset = asset_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = asset_data.asset_name + data = cast_map_to_str_dict(data) + + containers.append(data) + + return containers + + +def containerise(name, namespc, str_nodes, str_context, loader="", 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. OpenPype 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/OpenPype/Test/TestMaterial.TestMaterial` + + """ + namespace = namespc + nodes = ast.literal_eval(str_nodes) + context = ast.literal_eval(str_context) + if loader == "": + loader = None + # 1 - create directory for container + root = "/Game" + container_name = "{}{}".format(name, suffix) + new_name = move_assets_to_path(root, container_name, nodes) + + # 2 - create Asset Container there + path = "{}/{}".format(root, new_name) + create_container(container=container_name, path=path) + + namespace = path + + data = { + "schema": "openpype:container-2.0", + "id": "pyblish.avalon.container", + "name": new_name, + "namespace": namespace, + "loader": str(loader), + "representation": context["representation"]["_id"], + } + # 3 - imprint data + imprint("{}/{}".format(path, container_name), data) + return path + + +def instantiate(root, name, str_data, str_assets="", suffix="_INS"): + """Bundles *nodes* into *container*. + + Marking it with metadata as publishable instance. If assets are provided, + they are moved to new path where `OpenPypePublishInstance` 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 + + """ + data = ast.literal_eval(str_data) + assets = ast.literal_eval(str_assets) + container_name = "{}{}".format(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 = "{}/{}".format(root, new_name) + create_publish_instance(instance=container_name, path=path) + + imprint("{}/{}".format(path, container_name), data) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs index c26f2713b5f..f4f91ed4be8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs @@ -47,6 +47,7 @@ public OpenPype(ReadOnlyTargetRules Target) : base(Target) "SlateCore", "Json", "JsonUtilities", + "PythonScriptPlugin", // ... add private dependencies that you statically link with here ... } ); diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp index 5b4e6a52740..662e7b938dd 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp @@ -2,7 +2,6 @@ #include "OpenPypeStyle.h" #include "OpenPypeCommands.h" #include "OpenPypeCommunication.h" -#include "OpenPypePythonBridge.h" #include "LevelEditor.h" #include "Misc/MessageDialog.h" #include "LevelEditorMenuContext.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp index 2e11e39a589..142a8059cdf 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -4,6 +4,8 @@ #include "WebSocketsModule.h" #include "Json.h" #include "JsonObjectConverter.h" +#include "IPythonScriptPlugin.h" +#include "PythonScriptTypes.h" // Initialize static attributes @@ -170,73 +172,51 @@ void FOpenPypeCommunication::HandleError(TSharedPtr Root) void FOpenPypeCommunication::RunMethod(TSharedPtr Root) { // This code is called when we receive the request to run a method from the server. - FString Method = Root->GetStringField(TEXT("method")); - UE_LOG(LogTemp, Verbose, TEXT("Calling a function: %s"), *Method); + IPythonScriptPlugin* PythonPlugin = IPythonScriptPlugin::Get(); - if (Method == "ls") + if ( !PythonPlugin || !PythonPlugin->IsPythonAvailable() ) { - ls(Root); + UE_LOG(LogTemp, Error, TEXT("Python Plugin not loaded!")); + return; } - else if (Method == "containerise") - { - containerise(Root); - } - else if (Method == "instantiate") - { - instantiate(Root); - } -} - -void FOpenPypeCommunication::ls(TSharedPtr Root) -{ - FString Result = UOpenPypePythonBridge::Get()->ls(); - UE_LOG(LogTemp, Verbose, TEXT("Result: %s"), *Result); - - FString StringResponse; - FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; - FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); - - Socket->Send(StringResponse); -} + FString Method = Root->GetStringField(TEXT("method")); + UE_LOG(LogTemp, Verbose, TEXT("Calling a function: %s"), *Method); -void FOpenPypeCommunication::containerise(TSharedPtr Root) -{ + FPythonCommandEx Command; + Command.ExecutionMode = EPythonCommandExecutionMode::EvaluateStatement; + Command.Command = Method + "("; auto params = Root->GetArrayField(TEXT("params")); + for (auto param : params) + { + Command.Command += " " + param->AsString(); + } + Command.Command += ")"; - FString Name = params[0]->AsString(); - FString Namespace = params[1]->AsString(); - FString Nodes = params[2]->AsString(); - FString Context = params[3]->AsString(); - FString Loader = params.Num() >= 5 ? params[4]->AsString() : TEXT(""); - FString Suffix = params.Num() == 6 ? params[5]->AsString() : TEXT("_CON"); - - FString Result = UOpenPypePythonBridge::Get()->containerise(Name, Namespace, Nodes, Context, Loader, Suffix); - - UE_LOG(LogTemp, Verbose, TEXT("Result: %s"), *Result); + UE_LOG(LogTemp, Verbose, TEXT("Full command: %s"), *Command.Command); FString StringResponse; - FRpcResponseResult RpcResponse = { "2.0", Result, Root->GetIntegerField(TEXT("id")) }; - FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); - - Socket->Send(StringResponse); -} - -void FOpenPypeCommunication::instantiate(TSharedPtr Root) -{ - auto params = Root->GetArrayField(TEXT("params")); - FString RootParam = params[0]->AsString(); - FString Name = params[1]->AsString(); - FString Data = params[2]->AsString(); - FString Assets = params.Num() >= 4 ? params[3]->AsString() : TEXT(""); - FString Suffix = params.Num() == 5 ? params[4]->AsString() : TEXT("_INS"); + if ( !PythonPlugin->ExecPythonCommandEx(Command) ) + { + UE_LOG(LogTemp, Error, TEXT("Python Execution Failed!")); + for ( FPythonLogOutputEntry& LogEntry : Command.LogOutput ) + { + UE_LOG(LogTemp, Error, TEXT("%s"), *LogEntry.Message); + } - UOpenPypePythonBridge::Get()->instantiate(RootParam, Name, Data, Assets, Suffix); + FRpcError RpcError = { -32000, "Python Execution in Unreal Failed!", "" }; + FRpcResponseError RpcResponse = { "2.0", RpcError, Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + } + else + { + UE_LOG(LogTemp, Verbose, TEXT("Python Execution Success!")); + UE_LOG(LogTemp, Verbose, TEXT("Result: %s"), *Command.CommandResult); - FString StringResponse; - FRpcResponseResult RpcResponse = { "2.0", "", Root->GetIntegerField(TEXT("id")) }; - FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + FRpcResponseResult RpcResponse = { "2.0", Command.CommandResult, Root->GetIntegerField(TEXT("id")) }; + FJsonObjectConverter::UStructToJsonObjectString(RpcResponse, StringResponse); + } Socket->Send(StringResponse); } diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp deleted file mode 100644 index 81132315030..00000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "OpenPypePythonBridge.h" - -UOpenPypePythonBridge* UOpenPypePythonBridge::Get() -{ - TArray OpenPypePythonBridgeClasses; - GetDerivedClasses(UOpenPypePythonBridge::StaticClass(), OpenPypePythonBridgeClasses); - int32 NumClasses = OpenPypePythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(OpenPypePythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h index b4ae1d77a26..5e9b58f7f37 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h @@ -107,10 +107,6 @@ class FOpenPypeCommunication static void HandleError(TSharedPtr Root); static void RunMethod(TSharedPtr Root); - static void ls(TSharedPtr Root); - static void containerise(TSharedPtr Root); - static void instantiate(TSharedPtr Root); - private: static TSharedPtr Socket; static int32 Id; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h deleted file mode 100644 index f70928d8e8e..00000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include "Engine.h" -#include "OpenPypePythonBridge.generated.h" - -UCLASS(Blueprintable) -class UOpenPypePythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UOpenPypePythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - FString ls() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - FString containerise(const FString & name, const FString & namespc, const FString & str_nodes, const FString & str_context, const FString & loader, const FString & suffix) const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - FString instantiate(const FString & root, const FString & name, const FString & str_data, const FString & str_assets, const FString & suffix) const; -}; From 90d01388b5cc06df9a862af8bb0b5b6da3f9a246 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 12:03:42 +0100 Subject: [PATCH 13/55] Fixed execution of python commands from the OP plugin --- .../UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp index 142a8059cdf..36e732bf50b 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -189,7 +189,7 @@ void FOpenPypeCommunication::RunMethod(TSharedPtr Root) auto params = Root->GetArrayField(TEXT("params")); for (auto param : params) { - Command.Command += " " + param->AsString(); + Command.Command += " " + param->AsString() + ","; } Command.Command += ")"; @@ -202,7 +202,7 @@ void FOpenPypeCommunication::RunMethod(TSharedPtr Root) UE_LOG(LogTemp, Error, TEXT("Python Execution Failed!")); for ( FPythonLogOutputEntry& LogEntry : Command.LogOutput ) { - UE_LOG(LogTemp, Error, TEXT("%s"), *LogEntry.Message); + UE_LOG(LogTemp, Error, TEXT("%s"), *LogEntry.Output); } FRpcError RpcError = { -32000, "Python Execution in Unreal Failed!", "" }; From de7ae567f6561657f1601c19b0a0c21d506efcde Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 12:09:24 +0100 Subject: [PATCH 14/55] Basic functionality for loading assets through websockets --- openpype/hosts/unreal/api/pipeline.py | 31 ++++++++--- .../UE_5.0/Content/Python/init_unreal.py | 11 ++++ .../UE_5.0/Content/Python/pipeline.py | 1 + .../UE_5.0/Content/Python/plugins/__init__.py | 0 .../UE_5.0/Content/Python/plugins/load.py | 52 +++++++++++++++++++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/__init__.py create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index fe7e64d5923..a5bec314dd2 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -95,6 +95,28 @@ def _register_events(): pass +def format_string(input): + string = input.replace('\\', '/') + string = string.replace('"', '\\"') + string = string.replace("'", "\\'") + return '"' + string + '"' + + +def send_request(request, params=None): + communicator = CommunicationWrapper.communicator + formatted_params = [] + if params: + for p in params: + if isinstance(p, str): + p = format_string(p) + formatted_params.append(p) + return communicator.send_request(request, formatted_params) + + +def send_request_literal(request, params=None): + return ast.literal_eval(send_request(request, params)) + + def ls(): """List all containers. @@ -102,8 +124,7 @@ def ls(): metadata from them. Adding `objectName` to set. """ - communicator = CommunicationWrapper.communicator - return ast.literal_eval(communicator.send_request("ls")) + return send_request_literal("ls") def publish(): @@ -131,8 +152,7 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): `Material /Game/OpenPype/Test/TestMaterial.TestMaterial` """ - communicator = CommunicationWrapper.communicator - return communicator.send_request( + return send_request( "containerise", [name, namespace, nodes, context, loader, suffix]) @@ -154,8 +174,7 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): suffix (str): suffix string to append to instance name """ - communicator = CommunicationWrapper.communicator - return communicator.send_request( + return send_request( "instantiate", params=[root, name, data, assets, suffix]) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 0cb3ad0886c..25e91b44751 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -2,4 +2,15 @@ ls, containerise, instantiate, + create_container, + imprint, +) + +from plugins.load import ( + create_unique_asset_name, + does_directory_exist, + make_directory, + import_task, + list_assets, + save_listed_assets, ) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py index 64086f429de..cedffb2dfde 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py @@ -40,6 +40,7 @@ def parse_container(container): def imprint(node, data): loaded_asset = unreal.EditorAssetLibrary.load_asset(node) + data = ast.literal_eval(data) for key, value in data.items(): # Support values evaluated at imprint if callable(value): diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/__init__.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py new file mode 100644 index 00000000000..b3974055af3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -0,0 +1,52 @@ +import ast + +import unreal + + +def create_unique_asset_name(root, asset, name, version, suffix=""): + tools = unreal.AssetToolsHelpers().get_asset_tools() + return tools.create_unique_asset_name( + f"{root}/{asset}/{name}_v{version:03d}", suffix) + + +def does_directory_exist(directory_path): + return unreal.EditorAssetLibrary.does_directory_exist(directory_path) + + +def make_directory(directory_path): + unreal.EditorAssetLibrary.make_directory(directory_path) + + +def import_task(task_properties, options_properties, options_extra_properties): + task = unreal.AssetImportTask() + options = unreal.FbxImportUI() + + task_properties = ast.literal_eval(task_properties) + for prop in task_properties: + task.set_editor_property(prop[0], eval(prop[1])) + + options_properties = ast.literal_eval(options_properties) + for prop in options_properties: + options.set_editor_property(prop[0], eval(prop[1])) + + options_extra_properties = ast.literal_eval(options_extra_properties) + for prop in options_extra_properties: + options.get_editor_property(prop[0]).set_editor_property( + prop[1], eval(prop[2])) + + task.options = options + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + +def list_assets(directory_path, recursive, include_folder): + recursive = ast.literal_eval(recursive) + include_folder = ast.literal_eval(include_folder) + return str(unreal.EditorAssetLibrary.list_assets( + directory_path, recursive, include_folder)) + + +def save_listed_assets(asset_list): + asset_list = ast.literal_eval(asset_list) + for asset in asset_list: + unreal.EditorAssetLibrary.save_asset(asset) From dd7364045ae20b4bbbc4016b44b30c619d1f0b68 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 12:09:59 +0100 Subject: [PATCH 15/55] Load rigs through websocket request --- .../hosts/unreal/plugins/load/load_rig.py | 250 +++++++++--------- 1 file changed, 127 insertions(+), 123 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_rig.py b/openpype/hosts/unreal/plugins/load/load_rig.py index 227c5c9292a..be25653874d 100644 --- a/openpype/hosts/unreal/plugins/load/load_rig.py +++ b/openpype/hosts/unreal/plugins/load/load_rig.py @@ -7,8 +7,7 @@ AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api import pipeline as up class SkeletalMeshFBXLoader(plugin.Loader): @@ -54,53 +53,59 @@ def load(self, context, name, namespace, options): asset_name = "{}".format(name) version = context.get('version').get('name') - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", params=[root, asset, name, version]) container_name += suffix - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = unreal.AssetImportTask() - - task.set_editor_property('filename', self.fname) - 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', True) - task.set_editor_property('save', False) - - # set import options here - options = unreal.FbxImportUI() - 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) - # set to import normals, otherwise Unreal will compute them - # and it will take a long time, depending on the size of the mesh - options.skeletal_mesh_import_data.set_editor_property( - 'normal_import_method', - unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS) - - task.options = options - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + if not up.send_request_literal( + "does_directory_exist", params=[asset_dir]): + up.send_request("make_directory", params=[asset_dir]) + + task_properties = [ + ("filename", up.format_string(self.fname)), + ("destination_path", up.format_string(asset_dir)), + ("destination_name", up.format_string(asset_name)), + ("replace_existing", "False"), + ("automated", "True"), + ("save", "False") + ] + + options_properties = [ + ("import_as_skeletal", "True"), + ("import_animations", "False"), + ("import_mesh", "True"), + ("import_materials", "False"), + ("import_textures", "False"), + ("skeleton", "None"), + ("create_physics_asset", "False"), + ("mesh_type_to_import", "unreal.FBXImportType.FBXIT_SKELETAL_MESH") + ] + + options_extra_properties = [ + ( + "skeletal_mesh_import_data", + "import_content_type", + "unreal.FBXImportContentType.FBXICT_ALL" + ), + ( + "skeletal_mesh_import_data", + "normal_import_method", + "unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS" + ) + ] + + up.send_request( + "import_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -110,89 +115,88 @@ def load(self, context, name, namespace, options): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) return asset_content - def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] - - task = unreal.AssetImportTask() - - task.set_editor_property('filename', source_path) - task.set_editor_property('destination_path', destination_path) - task.set_editor_property('destination_name', name) - task.set_editor_property('replace_existing', True) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - options = unreal.FbxImportUI() - 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', True) - options.set_editor_property('import_textures', True) - 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 - ) - # set to import normals, otherwise Unreal will compute them - # and it will take a long time, depending on the size of the mesh - options.skeletal_mesh_import_data.set_editor_property( - 'normal_import_method', - unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS - ) - - task.options = options - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - 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) + # def update(self, container, representation): + # name = container["asset_name"] + # source_path = get_representation_path(representation) + # destination_path = container["namespace"] + + # task = unreal.AssetImportTask() + + # task.set_editor_property('filename', source_path) + # task.set_editor_property('destination_path', destination_path) + # task.set_editor_property('destination_name', name) + # task.set_editor_property('replace_existing', True) + # task.set_editor_property('automated', True) + # task.set_editor_property('save', True) + + # # set import options here + # options = unreal.FbxImportUI() + # 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', True) + # options.set_editor_property('import_textures', True) + # 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 + # ) + # # set to import normals, otherwise Unreal will compute them + # # and it will take a long time, depending on the size of the mesh + # options.skeletal_mesh_import_data.set_editor_property( + # 'normal_import_method', + # unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS + # ) + + # task.options = options + # # do import fbx and replace existing data + # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + # container_path = "{}/{}".format(container["namespace"], + # container["objectName"]) + # # update metadata + # unreal_pipeline.imprint( + # container_path, + # { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]) + # }) + + # 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) From ea853b3e55382fa4f18069df974c353dab7450e0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 12:10:19 +0100 Subject: [PATCH 16/55] Load staticmeshes through websocket request --- .../unreal/plugins/load/load_staticmeshfbx.py | 155 +++++++++--------- 1 file changed, 76 insertions(+), 79 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index 351c6860951..d64a74f9c5c 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -7,8 +7,7 @@ AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api import pipeline as up class StaticMeshFBXLoader(plugin.Loader): @@ -20,32 +19,6 @@ class StaticMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" - @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 load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -78,22 +51,47 @@ def load(self, context, name, namespace, options): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) + version = context.get('version').get('name') - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", params=[root, asset, name, version]) container_name += suffix - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task(self.fname, 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) + if not up.send_request_literal( + "does_directory_exist", params=[asset_dir]): + up.send_request("make_directory", params=[asset_dir]) + + task_properties = [ + ("filename", up.format_string(self.fname)), + ("destination_path", up.format_string(asset_dir)), + ("destination_name", up.format_string(asset_name)), + ("replace_existing", "False"), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("automated_import_should_detect_type", "False"), + ("import_animations", "False") + ] + + options_extra_properties = [ + ("static_mesh_import_data", "combine_meshes", "True"), + ("static_mesh_import_data", "remove_degenerates", "False") + ] + + up.send_request( + "import_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + + # Create Asset Container + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -103,58 +101,57 @@ def load(self, context, name, namespace, options): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) return asset_content - def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + # def update(self, container, representation): + # name = container["asset_name"] + # source_path = get_representation_path(representation) + # destination_path = container["namespace"] - task = self.get_task(source_path, destination_path, name, True) + # 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]) + # # do import fbx and replace existing data + # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + # container_path = "{}/{}".format(container["namespace"], + # container["objectName"]) + # # update metadata + # up.imprint( + # container_path, + # { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]) + # }) - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # destination_path, recursive=True, include_folder=True + # ) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + # for a in asset_content: + # unreal.EditorAssetLibrary.save_asset(a) - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) + # def remove(self, container): + # path = container["namespace"] + # parent_path = os.path.dirname(path) - unreal.EditorAssetLibrary.delete_directory(path) + # unreal.EditorAssetLibrary.delete_directory(path) - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # parent_path, recursive=False + # ) - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + # if len(asset_content) == 0: + # unreal.EditorAssetLibrary.delete_directory(parent_path) From 32fdfd9bdc6e2defb948b4b4681d5231102e5cbb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 15:32:34 +0100 Subject: [PATCH 17/55] Updated loading to differentiate between fbx and abc --- .../UE_5.0/Content/Python/init_unreal.py | 3 ++- .../UE_5.0/Content/Python/plugins/load.py | 26 ++++++++++++++++--- .../hosts/unreal/plugins/load/load_rig.py | 2 +- .../unreal/plugins/load/load_staticmeshfbx.py | 2 +- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 25e91b44751..0bf52159bf6 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -10,7 +10,8 @@ create_unique_asset_name, does_directory_exist, make_directory, - import_task, + import_abc_task, + import_fbx_task, list_assets, save_listed_assets, ) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index b3974055af3..5f8db5db9b1 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -17,9 +17,12 @@ def make_directory(directory_path): unreal.EditorAssetLibrary.make_directory(directory_path) -def import_task(task_properties, options_properties, options_extra_properties): - task = unreal.AssetImportTask() - options = unreal.FbxImportUI() +def _import( + task_arg, options_arg, + task_properties, options_properties, options_extra_properties +): + task = task_arg + options = options_arg task_properties = ast.literal_eval(task_properties) for prop in task_properties: @@ -34,6 +37,23 @@ def import_task(task_properties, options_properties, options_extra_properties): options.get_editor_property(prop[0]).set_editor_property( prop[1], eval(prop[2])) + return task, options + + +def import_abc_task(task_properties, options_properties, options_extra_properties): + task, options = _import( + unreal.AssetImportTask(), unreal.AbcImportSettings(), + task_properties, options_properties, options_extra_properties) + + task.options = options + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + +def import_fbx_task(task_properties, options_properties, options_extra_properties): + task, options = _import( + unreal.AssetImportTask(), unreal.FbxImportUI(), + task_properties, options_properties, options_extra_properties) + task.options = options unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) diff --git a/openpype/hosts/unreal/plugins/load/load_rig.py b/openpype/hosts/unreal/plugins/load/load_rig.py index be25653874d..d61a0f7cd83 100644 --- a/openpype/hosts/unreal/plugins/load/load_rig.py +++ b/openpype/hosts/unreal/plugins/load/load_rig.py @@ -96,7 +96,7 @@ def load(self, context, name, namespace, options): ] up.send_request( - "import_task", + "import_fbx_task", params=[ str(task_properties), str(options_properties), diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index d64a74f9c5c..e535f700425 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -82,7 +82,7 @@ def load(self, context, name, namespace, options): ] up.send_request( - "import_task", + "import_fbx_task", params=[ str(task_properties), str(options_properties), From 188eb3879e2312d238d3822a5d6ad9a7f8c47f73 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 15:33:15 +0100 Subject: [PATCH 18/55] Load alembic staticmeshes through websocket request --- .../plugins/load/load_alembic_staticmesh.py | 162 ++++++++---------- 1 file changed, 76 insertions(+), 86 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index a5b9cbd1fc5..c0ac411a223 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -7,8 +7,7 @@ AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api import pipeline as up class StaticMeshAlembicLoader(plugin.Loader): @@ -20,39 +19,6 @@ class StaticMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - @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 load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -89,23 +55,48 @@ def load(self, context, name, namespace, options): 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"{root}/{asset}/{name}_v{version:03d}", suffix="") + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", params=[root, asset, name, version]) container_name += suffix - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - self.fname, asset_dir, asset_name, False, default_conversion) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + if not up.send_request_literal( + "does_directory_exist", params=[asset_dir]): + up.send_request("make_directory", params=[asset_dir]) + + task_properties = [ + ("filename", up.format_string(self.fname)), + ("destination_path", up.format_string(asset_dir)), + ("destination_name", up.format_string(asset_name)), + ("replace_existing", "False"), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("import_type", "unreal.AlembicImportType.STATIC_MESH") + ] + + options_extra_properties = [ + ("static_mesh_settings", "merge_meshes", "True"), + ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "flip_u", "False"), + ("conversion_settings", "flip_v", "False"), + ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), + ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ] + + up.send_request( + "import_abc_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -115,58 +106,57 @@ def load(self, context, name, namespace, options): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) return asset_content - def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + # def update(self, container, representation): + # name = container["asset_name"] + # source_path = get_representation_path(representation) + # destination_path = container["namespace"] - task = self.get_task(source_path, destination_path, name, True) + # 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]) + # # do import fbx and replace existing data + # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + # container_path = "{}/{}".format(container["namespace"], + # container["objectName"]) + # # update metadata + # up.imprint( + # container_path, + # { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]) + # }) - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # destination_path, recursive=True, include_folder=True + # ) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + # for a in asset_content: + # unreal.EditorAssetLibrary.save_asset(a) - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) + # def remove(self, container): + # path = container["namespace"] + # parent_path = os.path.dirname(path) - unreal.EditorAssetLibrary.delete_directory(path) + # unreal.EditorAssetLibrary.delete_directory(path) - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # parent_path, recursive=False + # ) - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + # if len(asset_content) == 0: + # unreal.EditorAssetLibrary.delete_directory(parent_path) From 37e0bc2cebb8423284668694e4accbc7f566f4f1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 15:59:18 +0100 Subject: [PATCH 19/55] Check options before applying conversion settings --- .../plugins/load/load_alembic_staticmesh.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index c0ac411a223..43247620981 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -78,14 +78,18 @@ def load(self, context, name, namespace, options): ] options_extra_properties = [ - ("static_mesh_settings", "merge_meshes", "True"), - ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), - ("conversion_settings", "flip_u", "False"), - ("conversion_settings", "flip_v", "False"), - ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), - ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ("static_mesh_settings", "merge_meshes", "True") ] + if not default_conversion: + options_extra_properties.extend([ + ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "flip_u", "False"), + ("conversion_settings", "flip_v", "False"), + ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), + ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ]) + up.send_request( "import_abc_task", params=[ From 4dd20e24fb9cb0a226adbf60abc34349051bf18b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 16:01:58 +0100 Subject: [PATCH 20/55] Load alembic skeletalmeshes through websocket request --- .../plugins/load/load_alembic_skeletalmesh.py | 164 +++++++++--------- 1 file changed, 83 insertions(+), 81 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py index 9fe5f3ab4bb..4ab242e035d 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py @@ -7,8 +7,7 @@ AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api import pipeline as up class SkeletalMeshAlembicLoader(plugin.Loader): @@ -20,35 +19,7 @@ class SkeletalMeshAlembicLoader(plugin.Loader): 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) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - 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): + 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 @@ -80,22 +51,54 @@ def load(self, context, name, namespace, data): asset_name = "{}".format(name) version = context.get('version').get('name') - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") + default_conversion = False + if options.get("default_conversion"): + default_conversion = options.get("default_conversion") - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", params=[root, asset, name, version]) - task = self.get_task(self.fname, asset_dir, asset_name, False) + container_name += suffix - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + if not up.send_request_literal( + "does_directory_exist", params=[asset_dir]): + up.send_request("make_directory", params=[asset_dir]) + + task_properties = [ + ("filename", up.format_string(self.fname)), + ("destination_path", up.format_string(asset_dir)), + ("destination_name", up.format_string(asset_name)), + ("replace_existing", "False"), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("import_type", "unreal.AlembicImportType.SKELETAL") + ] + + options_extra_properties = [] + + if not default_conversion: + options_extra_properties.extend([ + ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "flip_u", "False"), + ("conversion_settings", "flip_v", "False"), + ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), + ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ]) + + up.send_request( + "import_abc_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -105,57 +108,56 @@ def load(self, context, name, namespace, data): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) return asset_content - def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + # def update(self, container, representation): + # name = container["asset_name"] + # source_path = get_representation_path(representation) + # destination_path = container["namespace"] - task = self.get_task(source_path, destination_path, name, True) + # 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 = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + # # do import fbx and replace existing data + # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + # container_path = "{}/{}".format(container["namespace"], + # container["objectName"]) + # # update metadata + # up.imprint( + # container_path, + # { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]) + # }) - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # destination_path, recursive=True, include_folder=True + # ) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + # for a in asset_content: + # unreal.EditorAssetLibrary.save_asset(a) - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) + # def remove(self, container): + # path = container["namespace"] + # parent_path = os.path.dirname(path) - unreal.EditorAssetLibrary.delete_directory(path) + # unreal.EditorAssetLibrary.delete_directory(path) - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # parent_path, recursive=False + # ) - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + # if len(asset_content) == 0: + # unreal.EditorAssetLibrary.delete_directory(parent_path) From 148de1b7ba67668de30f5b3cad04c974204311c5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 26 Oct 2022 16:12:16 +0100 Subject: [PATCH 21/55] Load alembic geometry cache through websocket request --- .../load/load_alembic_geometrycache.py | 202 +++++++++--------- 1 file changed, 98 insertions(+), 104 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py index 6ac3531b405..2ecf712b8ba 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py @@ -7,9 +7,7 @@ AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline - -import unreal # noqa +from openpype.hosts.unreal.api import pipeline as up class PointCacheAlembicLoader(plugin.Loader): @@ -21,47 +19,7 @@ class PointCacheAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - def get_task( - self, filename, asset_dir, asset_name, replace, frame_start, frame_end - ): - 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) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - 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)) - - sampling_settings.set_editor_property('frame_start', frame_start) - 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 load(self, context, name, namespace, data): + 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 @@ -91,31 +49,68 @@ def load(self, context, name, namespace, data): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) + version = context.get('version').get('name') - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) + default_conversion = False + if options.get("default_conversion"): + default_conversion = options.get("default_conversion") - frame_start = context.get('asset').get('data').get('frameStart') - frame_end = context.get('asset').get('data').get('frameEnd') + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", params=[root, asset, name, version]) - # 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 - - task = self.get_task( - self.fname, asset_dir, asset_name, False, frame_start, frame_end) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + container_name += suffix - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + if not up.send_request_literal( + "does_directory_exist", params=[asset_dir]): + up.send_request("make_directory", params=[asset_dir]) + + frame_start = context.get('asset').get('data').get('frameStart') + frame_end = context.get('asset').get('data').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 + + task_properties = [ + ("filename", up.format_string(self.fname)), + ("destination_path", up.format_string(asset_dir)), + ("destination_name", up.format_string(asset_name)), + ("replace_existing", "False"), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("import_type", "unreal.AlembicImportType.GEOMETRY_CACHE") + ] + + options_extra_properties = [ + ("geometry_cache_settings", "flatten_tracks", "False"), + ("sampling_settings", "frame_start", str(frame_start)), + ("sampling_settings", "frame_end", str(frame_end)) + ] + + if not default_conversion: + options_extra_properties.extend([ + ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "flip_u", "False"), + ("conversion_settings", "flip_v", "True"), + ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), + ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ]) + + up.send_request( + "import_abc_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + + # Create Asset Container + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -125,58 +120,57 @@ def load(self, context, name, namespace, data): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) return asset_content - def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + # def update(self, container, representation): + # name = container["asset_name"] + # source_path = get_representation_path(representation) + # destination_path = container["namespace"] - task = self.get_task(source_path, destination_path, name, True) + # 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]) + # # do import fbx and replace existing data + # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + # container_path = "{}/{}".format(container["namespace"], + # container["objectName"]) + # # update metadata + # up.imprint( + # container_path, + # { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]) + # }) - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # destination_path, recursive=True, include_folder=True + # ) - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + # for a in asset_content: + # unreal.EditorAssetLibrary.save_asset(a) - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) + # def remove(self, container): + # path = container["namespace"] + # parent_path = os.path.dirname(path) - unreal.EditorAssetLibrary.delete_directory(path) + # unreal.EditorAssetLibrary.delete_directory(path) - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + # asset_content = unreal.EditorAssetLibrary.list_assets( + # parent_path, recursive=False + # ) - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + # if len(asset_content) == 0: + # unreal.EditorAssetLibrary.delete_directory(parent_path) From 5b46e83caca500eb79703e432f748e7fd1ccc55e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 28 Oct 2022 13:25:53 +0100 Subject: [PATCH 22/55] Load layouts through websocket request --- .../UE_5.0/Content/Python/init_unreal.py | 18 +- .../UE_5.0/Content/Python/plugins/load.py | 300 ++++- .../hosts/unreal/plugins/load/load_layout.py | 1022 +++++++---------- 3 files changed, 696 insertions(+), 644 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 0bf52159bf6..94a816ea458 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -8,10 +8,24 @@ from plugins.load import ( create_unique_asset_name, + does_asset_exist, does_directory_exist, make_directory, - import_abc_task, - import_fbx_task, + new_level, + load_level, + save_current_level, + save_all_dirty_levels, + add_level_to_world, list_assets, + get_assets_of_class, save_listed_assets, + import_abc_task, + import_fbx_task, + get_sequence_frame_range, + generate_sequence, + generate_master_sequence, + set_sequence_hierarchy, + process_family, + apply_animation_to_actor, + add_animation_to_sequencer, ) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index 5f8db5db9b1..346fd6dff03 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -1,12 +1,23 @@ import ast +from pathlib import Path import unreal -def create_unique_asset_name(root, asset, name, version, suffix=""): +def get_asset(path): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + return ar.get_asset_by_object_path(path).get_asset() + + +def create_unique_asset_name(root, asset, name, version="", suffix=""): tools = unreal.AssetToolsHelpers().get_asset_tools() + subset = f"{name}_v{version:03d}" if version else name return tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix) + f"{root}/{asset}/{subset}", suffix) + + +def does_asset_exist(asset_path): + return unreal.EditorAssetLibrary.does_asset_exist(asset_path) def does_directory_exist(directory_path): @@ -17,8 +28,56 @@ def make_directory(directory_path): unreal.EditorAssetLibrary.make_directory(directory_path) +def new_level(level_path): + unreal.EditorLevelLibrary.new_level(level_path) + + +def load_level(level_path): + unreal.EditorLevelLibrary.load_level(level_path) + + +def save_current_level(): + unreal.EditorLevelLibrary.save_current_level() + + +def save_all_dirty_levels(): + unreal.EditorLevelLibrary.save_all_dirty_levels() + + +def add_level_to_world(level_path): + unreal.EditorLevelUtils.add_level_to_world( + unreal.EditorLevelLibrary.get_editor_world(), + level_path, + unreal.LevelStreamingDynamic + ) + + +def list_assets(directory_path, recursive, include_folder): + recursive = ast.literal_eval(recursive) + include_folder = ast.literal_eval(include_folder) + return str(unreal.EditorAssetLibrary.list_assets( + directory_path, recursive, include_folder)) + + +def get_assets_of_class(asset_list, class_name): + asset_list = ast.literal_eval(asset_list) + assets = [] + for asset in asset_list: + if unreal.EditorAssetLibrary.does_asset_exist(asset): + asset_object = unreal.EditorAssetLibrary.load_asset(asset) + if asset_object.get_class().get_name() == class_name: + assets.append(asset) + return assets + + +def save_listed_assets(asset_list): + asset_list = ast.literal_eval(asset_list) + for asset in asset_list: + unreal.EditorAssetLibrary.save_asset(asset) + + def _import( - task_arg, options_arg, + task_arg, options_arg, task_properties, options_properties, options_extra_properties ): task = task_arg @@ -49,6 +108,7 @@ def import_abc_task(task_properties, options_properties, options_extra_propertie unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + def import_fbx_task(task_properties, options_properties, options_extra_properties): task, options = _import( unreal.AssetImportTask(), unreal.FbxImportUI(), @@ -59,14 +119,230 @@ def import_fbx_task(task_properties, options_properties, options_extra_propertie unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) -def list_assets(directory_path, recursive, include_folder): - recursive = ast.literal_eval(recursive) - include_folder = ast.literal_eval(include_folder) - return str(unreal.EditorAssetLibrary.list_assets( - directory_path, recursive, include_folder)) +def get_sequence_frame_range(sequence_path): + sequence = get_asset(sequence_path) + return str((sequence.get_playback_start(), sequence.get_playback_end())) -def save_listed_assets(asset_list): - asset_list = ast.literal_eval(asset_list) - for asset in asset_list: - unreal.EditorAssetLibrary.save_asset(asset) +def generate_sequence(asset_name, asset_path, start_frame, end_frame, fps): + tools = unreal.AssetToolsHelpers().get_asset_tools() + + sequence = tools.create_asset( + asset_name=asset_name, + package_path=asset_path, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + sequence.set_display_rate(unreal.FrameRate(fps, 1.0)) + sequence.set_playback_start(start_frame) + sequence.set_playback_end(end_frame) + + return sequence.get_path_name() + + +def generate_master_sequence( + asset_name, asset_path, start_frame, end_frame, fps +): + sequence_path = generate_sequence( + asset_name, asset_path, start_frame, end_frame, fps) + sequence = get_asset(sequence_path) + + 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.get_path_name() + +def set_sequence_hierarchy( + parent_path, child_path, + parent_end_frame, child_start_frame, child_end_frame, map_paths_str +): + map_paths = ast.literal_eval(map_paths_str) + + parent = get_asset(parent_path) + child = get_asset(child_path) + + # Get existing sequencer tracks or create them if they don't exist + tracks = parent.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 = parent.add_master_track( + unreal.MovieSceneSubTrack) + if not visibility_track: + visibility_track = parent.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') == child: + 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', child) + subscene.set_range(child_start_frame, child_end_frame + 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(child_start_frame, child_end_frame + 1) + vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) + vis_section.set_row_index(index) + vis_section.set_level_names(maps) + + if child_start_frame > 1: + hid_section = visibility_track.add_section() + hid_section.set_range(1, child_start_frame) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + if child_end_frame < parent_end_frame: + hid_section = visibility_track.add_section() + hid_section.set_range(child_end_frame + 1, parent_end_frame + 1) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + + +def process_family(assets_str, class_name, transform_str, basis_str, sequence_path): + assets = ast.literal_eval(assets_str) + basis = ast.literal_eval(transform_str) + transform = ast.literal_eval(basis_str) + + actors = [] + bindings = [] + + sequence = get_asset(sequence_path) if sequence_path else None + + for asset in assets: + print(asset) + obj = get_asset(asset) + if obj and obj.get_class().get_name() == class_name: + 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 + ).transform() + actor = unreal.EditorLevelLibrary.spawn_actor_from_object( + obj, new_transform.translation) + actor.set_actor_rotation(new_transform.rotation.rotator(), False) + actor.set_actor_scale3d(new_transform.scale3d) + + if class_name == 'SkeletalMesh': + skm_comp = actor.get_editor_property('skeletal_mesh_component') + skm_comp.set_bounds_scale(10.0) + + actors.append(actor.get_path_name()) + + 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.get_id().to_string()) + + return (actors, bindings) + + +def apply_animation_to_actor(actor_path, animation_path): + actor = get_asset(actor_path) + animation = get_asset(animation_path) + + 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) + +def add_animation_to_sequencer(sequence_path, binding_guid, animation_path): + sequence = get_asset(sequence_path) + animation = get_asset(animation_path) + + binding = None + for b in sequence.get_possessables(): + if b.get_id().to_string() == binding_guid: + binding = b + break + + 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('\\', '/') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + _filter = unreal.ARFilter( + class_names=["AssetContainer"], + 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) + diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index c1d66ddf2a8..97d347dfe68 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -3,15 +3,6 @@ import json from pathlib import Path -import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils -from unreal import AssetToolsHelpers -from unreal import FBXImportType -from unreal import MovieSceneLevelVisibilityTrack -from unreal import MovieSceneSubTrack - from bson.objectid import ObjectId from openpype.client import get_asset_by_name, get_assets @@ -26,7 +17,7 @@ from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api import pipeline as up class LayoutLoader(plugin.Loader): @@ -40,21 +31,21 @@ class LayoutLoader(plugin.Loader): color = "orange" ASSET_ROOT = "/Game/OpenPype" - def _get_asset_containers(self, path): - ar = unreal.AssetRegistryHelpers.get_asset_registry() + # def _get_asset_containers(self, path): + # ar = unreal.AssetRegistryHelpers.get_asset_registry() - asset_content = EditorAssetLibrary.list_assets( - path, recursive=True) + # asset_content = EditorAssetLibrary.list_assets( + # path, recursive=True) - asset_containers = [] + # 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() == 'AssetContainer': - asset_containers.append(obj) + # # 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() == 'AssetContainer': + # asset_containers.append(obj) - return asset_containers + # return asset_containers @staticmethod def _get_fbx_loader(loaders, family): @@ -92,137 +83,6 @@ def _get_abc_loader(loaders, family): return None - @staticmethod - 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 - EditorLevelLibrary.save_all_dirty_levels() - 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 _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 @@ -233,137 +93,91 @@ def _import_animation( anim_path = f"{asset_dir}/animations/{anim_file_name}" asset_doc = get_current_project_asset() - # 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', asset_doc.get("data", {}).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 - ) + fps = asset_doc.get("data", {}).get("fps") + + task_properties = [ + ("filename", up.format_string(str( + path.with_suffix(f".{animation_file}")))), + ("destination_path", up.format_string(anim_path)), + ("destination_name", up.format_string( + f"{instance_name}_animation")), + ("replace_existing", "False"), + ("automated", "True"), + ("save", "False") + ] + + options_properties = [ + ("automated_import_should_detect_type", "False"), + ("original_import_type", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH"), + ("mesh_type_to_import", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH"), + ("original_import_type", "unreal.FBXImportType.FBXIT_ANIMATION"), + ("import_mesh", "False"), + ("import_animations", ""), + ("override_full_name", ""), + ("skeleton", f"get_asset({skeleton})") + ] + + options_extra_properties = [ + ("anim_sequence_import_data", "animation_length", + "unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME"), + ("anim_sequence_import_data", + "import_meshes_in_bone_hierarchy", "False"), + ("anim_sequence_import_data", "use_default_sample_rate", "False"), + ("anim_sequence_import_data", "custom_sample_rate", fps), + ("anim_sequence_import_data", "import_custom_attribute", "True"), + ("anim_sequence_import_data", "import_bone_tracks", "True"), + ("anim_sequence_import_data", "remove_redundant_keys", "False"), + ("anim_sequence_import_data", "convert_scene", "False") + ] + + up.send_request( + "import_fbx_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + + asset_content = up.send_request_literal( + "list_assets", params=[anim_path, "False", "False"]) + + up.send_request( + "save_listed_assets", params=[str(asset_content)]) 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 + animations = up.send_request_literal( + "get_assets_of_class", + params=[asset_content, "AnimSequence"]) + if animations: + animation = animations[0] 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 + actors = up.send_request_literal( + "get_assets_of_class", + params=[ + actors_dict.get(instance_name), "SkeletalMeshActor"]) + assert len(actors) == 1, ("There should be only one " + "skeleton in the loaded assets.") + actor = actors[0] - 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) + up.send_request( + "apply_animation_to_actor", params=[actor, 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=["AssetContainer"], - 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) - - @staticmethod - 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() - ) + up.send_request( + "add_animation_to_sequencer", + params=[sequence, binding, animation]) + def _get_frame_info(self, h_dir): project_name = legacy_io.active_project() asset_data = get_asset_by_name( project_name, @@ -392,27 +206,9 @@ def _generate_sequence(h, h_dir): min_frame = min(start_frames) max_frame = max(end_frames) - sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) - - 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) + return min_frame, max_frame, asset_data.get('data').get("fps") 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) @@ -453,7 +249,6 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): continue representation = str(repr_data.get('_id')) - print(representation) # This is to keep compatibility with old versions of the # json format. elif element.get('reference_fbx'): @@ -502,16 +297,26 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): options=options ) + print(assets) + container = None - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == 'AssetContainer': - container = obj - if obj.get_class().get_name() == 'Skeleton': - skeleton = obj + asset_containers = up.send_request_literal( + "get_assets_of_class", + params=[assets, "AssetContainer"]) + assert len(asset_containers) == 1, ("There should be only one " + "AssetContainer in the loaded assets.") + container = asset_containers[0] - loaded_assets.append(container.get_path_name()) + skeletons = up.send_request_literal( + "get_assets_of_class", + params=[assets, "Skeleton"]) + assert len(skeletons) <= 1, ("There should be one " + "skeleton in the loaded assets at most.") + if skeletons: + skeleton = skeletons[0] + + loaded_assets.append(container) instances = [ item for item in data @@ -522,24 +327,25 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): for instance in instances: # transform = instance.get('transform') - transform = instance.get('transform_matrix') - basis = instance.get('basis') - inst = instance.get('instance_name') + transform = str(instance.get('transform_matrix')) + basis = str(instance.get('basis')) + instance_name = instance.get('instance_name') actors = [] if family == 'model': - actors, _ = self._process_family( - assets, 'StaticMesh', transform, basis, - sequence, inst - ) + (actors, _) = up.send_request_literal( + "process_family", params=[ + assets, 'StaticMesh', + transform, basis, sequence]) elif family == 'rig': - actors, bindings = self._process_family( - assets, 'SkeletalMesh', transform, basis, - sequence, inst - ) - actors_dict[inst] = actors - bindings_dict[inst] = bindings + (actors, bindings) = up.send_request_literal( + "process_family", params=[ + assets, 'SkeletalMesh', + transform, basis, sequence]) + + actors_dict[instance_name] = actors + bindings_dict[instance_name] = bindings if skeleton: skeleton_dict[representation] = skeleton @@ -555,48 +361,6 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): 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. @@ -633,20 +397,19 @@ def load(self, context, name, namespace, options): suffix = "_CON" asset_name = f"{asset}_{name}" if asset else name - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(hierarchy_dir, asset, name), suffix="") + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", params=[root, asset, name]) container_name += suffix - EditorAssetLibrary.make_directory(asset_dir) + up.send_request("make_directory", params=[asset_dir]) master_level = None - shot = None + shot = "" sequences = [] level = f"{asset_dir}/{asset}_map.{asset}_map" - EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map") + up.send_request("new_level", params=[f"{asset_dir}/{asset}_map"]) if create_sequences: # Create map for the shot, and create hierarchy of map. If the @@ -655,85 +418,84 @@ def load(self, context, name, namespace, options): 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 not up.send_request_literal( + "does_asset_exist", params=[master_level]): + up.send_request( + "new_level", params=[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) + up.send_request("load_level", params=[master_level]) + up.send_request("add_level_to_world", params=[level]) + up.send_request("save_all_dirty_levels") + up.send_request("load_level", params=[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) + root_content = up.send_request_literal( + "list_assets", params=[asset_dir, "False", "False"]) - existing_sequences = [ - EditorAssetLibrary.find_asset_data(asset) - for asset in root_content - if EditorAssetLibrary.find_asset_data( - asset).get_class().get_name() == 'LevelSequence' - ] + existing_sequences = up.send_request_literal( + "get_assets_of_class", + params=[root_content, "LevelSequence"]) if not existing_sequences: - sequence, frame_range = self._generate_sequence(h, h_dir) + start_frame, end_frame, fps = self._get_frame_info(h_dir) + sequence = up.send_request( + "generate_master_sequence", + params=[h, h_dir, start_frame, end_frame, fps]) sequences.append(sequence) - frame_ranges.append(frame_range) + frame_ranges.append((start_frame, end_frame)) 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=asset, - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) + for sequence in existing_sequences: + sequences.append(sequence) + frame_ranges.append( + up.send_request_literal( + "get_sequence_frame_range", + params=[sequence])) + + project_name = legacy_io.active_project() + data = get_asset_by_name(project_name, asset)["data"] + shot_start_frame = 0 + shot_end_frame = data.get('clipOut') - data.get('clipIn') + 1 + fps = data.get("fps") + + shot = up.send_request( + "generate_sequence", + params=[ + asset, asset_dir, shot_start_frame, shot_end_frame, fps]) # sequences and frame_ranges have the same length for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( - sequences[i], sequences[i + 1], - frame_ranges[i][1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1], - [level]) + up.send_request( + "set_sequence_hierarchy", + params=[ + sequences[i], sequences[i + 1], frame_ranges[i][1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1], + str([level])]) - project_name = legacy_io.active_project() - data = get_asset_by_name(project_name, asset)["data"] - shot.set_display_rate( - unreal.FrameRate(data.get("fps"), 1.0)) - shot.set_playback_start(0) - shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) if sequences: - self._set_sequence_hierarchy( - sequences[-1], shot, - frame_ranges[-1][1], - data.get('clipIn'), data.get('clipOut'), - [level]) + up.send_request( + "set_sequence_hierarchy", + params=[ + sequences[-1], shot, + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + str([level])]) - EditorLevelLibrary.load_level(level) + up.send_request("load_level", params=[level]) loaded_assets = self._process(self.fname, asset_dir, shot) - for s in sequences: - EditorAssetLibrary.save_asset(s.get_full_name()) + up.send_request("save_listed_assets", params=[str(sequences)]) - EditorLevelLibrary.save_current_level() + up.send_request("save_current_level") # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -743,238 +505,238 @@ def load(self, context, name, namespace, options): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "False"]) - for a in asset_content: - EditorAssetLibrary.save_asset(a) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) if master_level: - EditorLevelLibrary.load_level(master_level) + up.send_request("load_level", params=[master_level]) return asset_content - def update(self, container, representation): - data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - root = "/Game/OpenPype" - - asset_dir = container.get('namespace') - context = representation.get("context") - - sequence = None - master_level = None - - if create_sequences: - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - 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_editor_property('object_path') - - 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(representation) - - loaded_assets = self._process(source_path, asset_dir, sequence) - - data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]), - "loaded_assets": loaded_assets - } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) - - EditorLevelLibrary.save_current_level() - - asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + # def update(self, container, representation): + # data = get_current_project_settings() + # create_sequences = data["unreal"]["level_sequences_for_layouts"] - 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) + # ar = unreal.AssetRegistryHelpers.get_asset_registry() - 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/OpenPype" - root = "/Game/OpenPype" - path = Path(container.get("namespace")) - - containers = unreal_pipeline.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 - ) + # asset_dir = container.get('namespace') + # context = representation.get("context") - 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_editor_property('object_path') - - 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 - ) + # sequence = None + # master_level = None - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(str(path.parent)) + # if create_sequences: + # hierarchy = context.get('hierarchy').split("/") + # h_dir = f"{root}/{hierarchy[0]}" + # h_asset = hierarchy[0] + # 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_editor_property('object_path') + + # 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(representation) + + # loaded_assets = self._process(source_path, asset_dir, sequence) + + # data = { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]), + # "loaded_assets": loaded_assets + # } + # up.imprint( + # "{}/{}".format(asset_dir, container.get('container_name')), data) + + # EditorLevelLibrary.save_current_level() + + # asset_content = EditorAssetLibrary.list_assets( + # asset_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) + + # 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/OpenPype" + # path = Path(container.get("namespace")) + + # containers = up.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_editor_property('object_path') + + # 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)) From 7c28cf4a651265a71b3159b7d86e1aeae9d2bf63 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 31 Oct 2022 11:54:33 +0000 Subject: [PATCH 23/55] Fix layout path --- openpype/hosts/unreal/plugins/load/load_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 97d347dfe68..cf8e6479f98 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -398,7 +398,7 @@ def load(self, context, name, namespace, options): asset_name = f"{asset}_{name}" if asset else name asset_dir, container_name = up.send_request_literal( - "create_unique_asset_name", params=[root, asset, name]) + "create_unique_asset_name", params=[hierarchy_dir, asset, name]) container_name += suffix From 879974f496aefa9dc586d0ff51244cfe433795bd Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 31 Oct 2022 12:41:54 +0000 Subject: [PATCH 24/55] More fixes to layout loader --- .../UE_5.0/Content/Python/init_unreal.py | 1 + .../UE_5.0/Content/Python/plugins/load.py | 36 ++++++++++----- .../hosts/unreal/plugins/load/load_layout.py | 45 +++++++++++++------ 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 94a816ea458..8eff0d67d26 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -25,6 +25,7 @@ generate_sequence, generate_master_sequence, set_sequence_hierarchy, + set_sequence_visibility, process_family, apply_animation_to_actor, add_animation_to_sequencer, diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index 346fd6dff03..b923ee68d91 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -121,7 +121,7 @@ def import_fbx_task(task_properties, options_properties, options_extra_propertie def get_sequence_frame_range(sequence_path): sequence = get_asset(sequence_path) - return str((sequence.get_playback_start(), sequence.get_playback_end())) + return (sequence.get_playback_start(), sequence.get_playback_end()) def generate_sequence(asset_name, asset_path, start_frame, end_frame, fps): @@ -160,31 +160,22 @@ def generate_master_sequence( return sequence.get_path_name() def set_sequence_hierarchy( - parent_path, child_path, - parent_end_frame, child_start_frame, child_end_frame, map_paths_str + parent_path, child_path, child_start_frame, child_end_frame ): - map_paths = ast.literal_eval(map_paths_str) - parent = get_asset(parent_path) child = get_asset(child_path) # Get existing sequencer tracks or create them if they don't exist tracks = parent.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 + break if not subscene_track: subscene_track = parent.add_master_track( unreal.MovieSceneSubTrack) - if not visibility_track: - visibility_track = parent.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) # Create the sub-scene section subscenes = subscene_track.get_sections() @@ -199,6 +190,27 @@ def set_sequence_hierarchy( subscene.set_editor_property('sub_sequence', child) subscene.set_range(child_start_frame, child_end_frame + 1) + +def set_sequence_visibility( + parent_path, parent_end_frame, child_start_frame, child_end_frame, + map_paths_str +): + map_paths = ast.literal_eval(map_paths_str) + + parent = get_asset(parent_path) + + # Get existing sequencer tracks or create them if they don't exist + tracks = parent.get_master_tracks() + visibility_track = None + for t in tracks: + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t + break + if not visibility_track: + visibility_track = parent.add_master_track( + unreal.MovieSceneLevelVisibilityTrack) + # Create the visibility section ar = unreal.AssetRegistryHelpers.get_asset_registry() maps = [] diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index cf8e6479f98..0429e93340c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -161,8 +161,8 @@ def _import_animation( "get_assets_of_class", params=[ actors_dict.get(instance_name), "SkeletalMeshActor"]) - assert len(actors) == 1, ("There should be only one " - "skeleton in the loaded assets.") + assert len(actors) == 1, ( + "There should be only one skeleton in the loaded assets.") actor = actors[0] up.send_request( @@ -304,15 +304,17 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): asset_containers = up.send_request_literal( "get_assets_of_class", params=[assets, "AssetContainer"]) - assert len(asset_containers) == 1, ("There should be only one " - "AssetContainer in the loaded assets.") + assert len(asset_containers) == 1, ( + "There should be only one AssetContainer in " + "the loaded assets.") container = asset_containers[0] skeletons = up.send_request_literal( "get_assets_of_class", params=[assets, "Skeleton"]) - assert len(skeletons) <= 1, ("There should be one " - "skeleton in the loaded assets at most.") + assert len(skeletons) <= 1, ( + "There should be one skeleton at most in " + "the loaded assets.") if skeletons: skeleton = skeletons[0] @@ -400,6 +402,9 @@ def load(self, context, name, namespace, options): asset_dir, container_name = up.send_request_literal( "create_unique_asset_name", params=[hierarchy_dir, asset, name]) + asset_path = Path(asset_dir) + asset_path_parent = str(asset_path.parent.as_posix()) + container_name += suffix up.send_request("make_directory", params=[asset_dir]) @@ -408,8 +413,11 @@ def load(self, context, name, namespace, options): shot = "" sequences = [] - level = f"{asset_dir}/{asset}_map.{asset}_map" - up.send_request("new_level", params=[f"{asset_dir}/{asset}_map"]) + level = f"{asset_path_parent}/{asset}_map.{asset}_map" + if not up.send_request_literal( + "does_asset_exist", params=[level]): + up.send_request( + "new_level", params=[f"{asset_path_parent}/{asset}_map"]) if create_sequences: # Create map for the shot, and create hierarchy of map. If the @@ -434,7 +442,7 @@ def load(self, context, name, namespace, options): frame_ranges = [] for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): root_content = up.send_request_literal( - "list_assets", params=[asset_dir, "False", "False"]) + "list_assets", params=[h_dir, "False", "False"]) existing_sequences = up.send_request_literal( "get_assets_of_class", @@ -451,10 +459,10 @@ def load(self, context, name, namespace, options): else: for sequence in existing_sequences: sequences.append(sequence) - frame_ranges.append( - up.send_request_literal( + frame_range = up.send_request_literal( "get_sequence_frame_range", - params=[sequence])) + params=[sequence]) + frame_ranges.append(frame_range) project_name = legacy_io.active_project() data = get_asset_by_name(project_name, asset)["data"] @@ -472,7 +480,12 @@ def load(self, context, name, namespace, options): up.send_request( "set_sequence_hierarchy", params=[ - sequences[i], sequences[i + 1], frame_ranges[i][1], + sequences[i], sequences[i + 1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1]]) + up.send_request( + "set_sequence_visibility", + params=[ + sequences[i], frame_ranges[i][1], frame_ranges[i + 1][0], frame_ranges[i + 1][1], str([level])]) @@ -481,7 +494,11 @@ def load(self, context, name, namespace, options): "set_sequence_hierarchy", params=[ sequences[-1], shot, - frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut')]) + up.send_request( + "set_sequence_visibility", + params=[ + sequences[-1], frame_ranges[-1][1], data.get('clipIn'), data.get('clipOut'), str([level])]) From 2a80e6621e7596064ce133195f74f6bede868aae Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 31 Oct 2022 13:10:20 +0000 Subject: [PATCH 25/55] Load cameras through websocket request --- .../UE_5.0/Content/Python/init_unreal.py | 1 + .../UE_5.0/Content/Python/plugins/load.py | 34 + .../hosts/unreal/plugins/load/load_camera.py | 785 ++++++++---------- 3 files changed, 392 insertions(+), 428 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 8eff0d67d26..78631fd892a 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -29,4 +29,5 @@ process_family, apply_animation_to_actor, add_animation_to_sequencer, + import_camera, ) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index b923ee68d91..0d2c54db574 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -358,3 +358,37 @@ def add_animation_to_sequencer(sequence_path, binding_guid, animation_path): sec_params = section.get_editor_property('params') sec_params.set_editor_property('animation', animation) + +def import_camera(sequence_path, import_filename): + sequence = get_asset(sequence_path) + + world = unreal.EditorLevelLibrary.get_editor_world() + + settings = unreal.MovieSceneUserImportFBXSettings() + settings.set_editor_property('reduce_keys', False) + + ue_version = unreal.SystemLibrary.get_engine_version().split('.') + ue_major = int(ue_version[0]) + ue_minor = int(ue_version[1]) + + print(import_filename) + + if ue_major == 4 and ue_minor <= 26: + unreal.SequencerTools.import_fbx( + world, + sequence, + sequence.get_bindings(), + settings, + import_filename + ) + elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5: + unreal.SequencerTools.import_level_sequence_fbx( + world, + sequence, + sequence.get_bindings(), + settings, + import_filename + ) + else: + raise NotImplementedError( + f"Unreal version {ue_major} not supported") diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index ca6b0ce7368..52a5591a1d7 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -2,17 +2,13 @@ """Load camera from FBX.""" from pathlib import Path -import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils from openpype.client import get_assets, get_asset_by_name from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api import pipeline as up class CameraLoader(plugin.Loader): @@ -24,60 +20,38 @@ class CameraLoader(plugin.Loader): icon = "cube" color = "orange" - def _set_sequence_hierarchy( - self, seq_i, seq_j, min_frame_j, max_frame_j - ): - tracks = seq_i.get_master_tracks() - track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - track = t - break - if not track: - track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - - subscenes = 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 = track.add_section() - subscene.set_row_index(len(track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - - 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 _get_frame_info(self, h_dir): + project_name = legacy_io.active_project() + asset_data = get_asset_by_name( + project_name, + h_dir.split('/')[-1], + fields=["_id", "data.fps"] + ) + + start_frames = [] + end_frames = [] + + elements = list(get_assets( + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(get_assets( + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + + min_frame = min(start_frames) + max_frame = max(end_frames) + + return min_frame, max_frame, asset_data.get('data').get("fps") - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, options): """ Load and containerise representation into Content Browser. @@ -115,14 +89,12 @@ def load(self, context, name, namespace, data): else: asset_name = "{}".format(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}/{asset}"): - asset_content = EditorAssetLibrary.list_assets( - f"{root}/{asset}", recursive=False, include_folder=True - ) + if up.send_request_literal("does_directory_exist", + params=[f"{hierarchy_dir}/{asset}"]): + asset_content = up.send_request_literal( + "list_assets", params=[f"{root}/{asset}", "False", "True"]) # Get highest number to make a unique name folders = [a for a in asset_content @@ -138,38 +110,38 @@ def load(self, context, name, namespace, data): else: unique_number = f_numbers[-1] + 1 - asset_dir, container_name = tools.create_unique_asset_name( - f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", params=[ + hierarchy_dir, asset, name, unique_number]) asset_path = Path(asset_dir) asset_path_parent = str(asset_path.parent.as_posix()) container_name += suffix - EditorAssetLibrary.make_directory(asset_dir) + up.send_request("make_directory", params=[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[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 not up.send_request_literal( + "does_asset_exist", params=[master_level]): + up.send_request( + "new_level", params=[f"{h_dir}/{h_asset}_map"]) level = f"{asset_path_parent}/{asset}_map.{asset}_map" - if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map") - - 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) + if not up.send_request_literal( + "does_asset_exist", params=[level]): + up.send_request( + "new_level", params=[f"{asset_path_parent}/{asset}_map"]) + + up.send_request("load_level", params=[master_level]) + up.send_request("add_level_to_world", params=[level]) + up.send_request("save_all_dirty_levels") + up.send_request("load_level", params=[level]) - project_name = legacy_io.active_project() # TODO refactor # - Creationg of hierarchy should be a function in unreal integration # - it's used in multiple loaders but must not be loader's logic @@ -186,109 +158,67 @@ def load(self, context, name, namespace, data): # they don't exist. sequences = [] frame_ranges = [] - i = 0 - for h in hierarchy_dir_list: - root_content = EditorAssetLibrary.list_assets( - h, 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' - ] + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): + root_content = up.send_request_literal( + "list_assets", params=[h_dir, "False", "False"]) + + print(root_content) + + existing_sequences = up.send_request_literal( + "get_assets_of_class", + params=[root_content, "LevelSequence"]) + + print(existing_sequences) if not existing_sequences: - scene = tools.create_asset( - asset_name=hierarchy[i], - package_path=h, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - asset_data = get_asset_by_name( - project_name, - h.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - scene.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min_frame) - scene.set_playback_end(max_frame) - - sequences.append(scene) - frame_ranges.append((min_frame, max_frame)) - 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())) + start_frame, end_frame, fps = self._get_frame_info(h_dir) + sequence = up.send_request( + "generate_master_sequence", + params=[h, h_dir, start_frame, end_frame, fps]) - i += 1 + sequences.append(sequence) + frame_ranges.append((start_frame, end_frame)) + else: + for sequence in existing_sequences: + sequences.append(sequence) + frame_ranges.append( + up.send_request_literal( + "get_sequence_frame_range", + params=[sequence])) - EditorAssetLibrary.make_directory(asset_dir) + project_name = legacy_io.active_project() + data = get_asset_by_name(project_name, asset)["data"] + start_frame = 0 + end_frame = data.get('clipOut') - data.get('clipIn') + 1 + fps = data.get("fps") - cam_seq = tools.create_asset( - asset_name=f"{asset}_camera", - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) + cam_sequence = up.send_request( + "generate_sequence", + params=[ + f"{asset}_camera", asset_dir, start_frame, end_frame, fps]) # Add sequences data to hierarchy for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( - sequences[i], sequences[i + 1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1]) - - data = get_asset_by_name(project_name, asset)["data"] - cam_seq.set_display_rate( - unreal.FrameRate(data.get("fps"), 1.0)) - cam_seq.set_playback_start(0) - cam_seq.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) - self._set_sequence_hierarchy( - sequences[-1], cam_seq, - data.get('clipIn'), data.get('clipOut')) - - settings = unreal.MovieSceneUserImportFBXSettings() - settings.set_editor_property('reduce_keys', False) - - if cam_seq: - self._import_camera( - EditorLevelLibrary.get_editor_world(), - cam_seq, - cam_seq.get_bindings(), - settings, - self.fname - ) + up.send_request( + "set_sequence_hierarchy", + params=[ + sequences[i], sequences[i + 1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1]]) + + up.send_request( + "set_sequence_hierarchy", + params=[ + sequences[-1], cam_sequence, + data.get('clipIn'), data.get('clipOut')]) + + up.send_request( + "import_camera", + params=[ + cam_sequence, self.fname]) # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -298,265 +228,264 @@ def load(self, context, name, namespace, data): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(master_level) + up.send_request("save_all_dirty_levels") + up.send_request("load_level", params=[master_level]) - asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - for a in asset_content: - EditorAssetLibrary.save_asset(a) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) return asset_content - def update(self, container, representation): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - root = "/Game/OpenPype" - - asset_dir = container.get('namespace') - - context = representation.get("context") - - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - - 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=[str(Path(asset_dir).parent.as_posix())], - recursive_paths=True) - maps = ar.get_assets(filter) - - # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_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/OpenPype" - 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() - - 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 - break - 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 - # 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 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) - - self._import_camera( - EditorLevelLibrary.get_editor_world(), - new_sequence, - new_sequence.get_bindings(), - settings, - str(representation["data"]["path"]) - ) - - data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) - - EditorLevelLibrary.save_current_level() - - asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - EditorLevelLibrary.load_level(master_level) - - def remove(self, container): - path = Path(container.get("namespace")) - parent_path = str(path.parent.as_posix()) - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{str(path.as_posix())}"], - 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=[f"{parent_path}"], - 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_full_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_full_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/OpenPype" - 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() - - sequences = [master_sequence] - - parent = 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 - break - 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. - i = 0 - for ss in sections: - ss.set_row_index(i) - i += 1 - - if parent: - break - - assert parent, "Could not find the parent sequence" - - EditorAssetLibrary.delete_directory(str(path.as_posix())) - - # Check if there isn't any more assets in the parent folder, and - # delete it if not. - asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) + # def update(self, container, representation): + # ar = unreal.AssetRegistryHelpers.get_asset_registry() + + # root = "/Game/OpenPype" + + # asset_dir = container.get('namespace') + + # context = representation.get("context") + + # hierarchy = context.get('hierarchy').split("/") + # h_dir = f"{root}/{hierarchy[0]}" + # h_asset = hierarchy[0] + # master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + + # 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=[str(Path(asset_dir).parent.as_posix())], + # recursive_paths=True) + # maps = ar.get_assets(filter) + + # # There should be only one map in the list + # EditorLevelLibrary.load_level(maps[0].get_full_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/OpenPype" + # 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() + + # 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 + # break + # 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 + # # 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 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) + + # self._import_camera( + # EditorLevelLibrary.get_editor_world(), + # new_sequence, + # new_sequence.get_bindings(), + # settings, + # str(representation["data"]["path"]) + # ) + + # data = { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]) + # } + # up.imprint( + # "{}/{}".format(asset_dir, container.get('container_name')), data) + + # EditorLevelLibrary.save_current_level() + + # asset_content = EditorAssetLibrary.list_assets( + # asset_dir, recursive=True, include_folder=False) + + # for a in asset_content: + # EditorAssetLibrary.save_asset(a) + + # EditorLevelLibrary.load_level(master_level) + + # def remove(self, container): + # path = Path(container.get("namespace")) + # parent_path = str(path.parent.as_posix()) + + # ar = unreal.AssetRegistryHelpers.get_asset_registry() + # filter = unreal.ARFilter( + # class_names=["LevelSequence"], + # package_paths=[f"{str(path.as_posix())}"], + # 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=[f"{parent_path}"], + # 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_full_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_full_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/OpenPype" + # 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() + + # sequences = [master_sequence] + + # parent = 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 + # break + # 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. + # i = 0 + # for ss in sections: + # ss.set_row_index(i) + # i += 1 + + # if parent: + # break + + # assert parent, "Could not find the parent sequence" + + # EditorAssetLibrary.delete_directory(str(path.as_posix())) + + # # Check if there isn't any more assets in the parent folder, and + # # delete it if not. + # asset_content = EditorAssetLibrary.list_assets( + # parent_path, recursive=False, include_folder=True + # ) + + # if len(asset_content) == 0: + # EditorAssetLibrary.delete_directory(parent_path) From 0d2ad6ad3b4edfadae35f752205ef3fe97181d5c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 1 Nov 2022 10:27:48 +0000 Subject: [PATCH 26/55] Fixed layout problems with some parameters --- openpype/hosts/unreal/plugins/load/load_layout.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 0429e93340c..90f299b226d 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -114,9 +114,9 @@ def _import_animation( "unreal.FBXImportType.FBXIT_SKELETAL_MESH"), ("original_import_type", "unreal.FBXImportType.FBXIT_ANIMATION"), ("import_mesh", "False"), - ("import_animations", ""), - ("override_full_name", ""), - ("skeleton", f"get_asset({skeleton})") + ("import_animations", "True"), + ("override_full_name", "True"), + ("skeleton", f"get_asset({up.format_string(skeleton)})") ] options_extra_properties = [ @@ -125,7 +125,7 @@ def _import_animation( ("anim_sequence_import_data", "import_meshes_in_bone_hierarchy", "False"), ("anim_sequence_import_data", "use_default_sample_rate", "False"), - ("anim_sequence_import_data", "custom_sample_rate", fps), + ("anim_sequence_import_data", "custom_sample_rate", str(fps)), ("anim_sequence_import_data", "import_custom_attribute", "True"), ("anim_sequence_import_data", "import_bone_tracks", "True"), ("anim_sequence_import_data", "remove_redundant_keys", "False"), From 38510c1e67a7b5e144278e4e194fffd2d48060cb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 1 Nov 2022 10:54:36 +0000 Subject: [PATCH 27/55] Fix name of instanced assets when loading layouts --- .../integration/UE_5.0/Content/Python/plugins/load.py | 10 +++++++++- openpype/hosts/unreal/plugins/load/load_layout.py | 6 ++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index 0d2c54db574..b53b781ee26 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -242,7 +242,10 @@ def set_sequence_visibility( hid_section.set_level_names(maps) -def process_family(assets_str, class_name, transform_str, basis_str, sequence_path): +def process_family( + assets_str, class_name, instance_name, + transform_str, basis_str, sequence_path +): assets = ast.literal_eval(assets_str) basis = ast.literal_eval(transform_str) transform = ast.literal_eval(basis_str) @@ -273,6 +276,11 @@ def process_family(assets_str, class_name, transform_str, basis_str, sequence_pa ).transform() actor = unreal.EditorLevelLibrary.spawn_actor_from_object( obj, new_transform.translation) + if instance_name: + try: + actor.set_actor_label(instance_name) + except Exception as e: + print(e) actor.set_actor_rotation(new_transform.rotation.rotator(), False) actor.set_actor_scale3d(new_transform.scale3d) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 90f299b226d..0a87cd71320 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -297,8 +297,6 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): options=options ) - print(assets) - container = None asset_containers = up.send_request_literal( @@ -338,12 +336,12 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): if family == 'model': (actors, _) = up.send_request_literal( "process_family", params=[ - assets, 'StaticMesh', + assets, 'StaticMesh', instance_name, transform, basis, sequence]) elif family == 'rig': (actors, bindings) = up.send_request_literal( "process_family", params=[ - assets, 'SkeletalMesh', + assets, 'SkeletalMesh', instance_name, transform, basis, sequence]) actors_dict[instance_name] = actors From 45c9051a39e2397af994b6be5f5439f5c0e0aa22 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 1 Nov 2022 14:44:51 +0000 Subject: [PATCH 28/55] Load animations through websocket request --- .../UE_5.0/Content/Python/init_unreal.py | 4 + .../UE_5.0/Content/Python/plugins/load.py | 92 +++- .../unreal/plugins/load/load_animation.py | 404 ++++++++---------- 3 files changed, 271 insertions(+), 229 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 78631fd892a..666613d0a4d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -18,6 +18,8 @@ add_level_to_world, list_assets, get_assets_of_class, + get_all_assets_of_class, + get_first_asset_of_class, save_listed_assets, import_abc_task, import_fbx_task, @@ -28,6 +30,8 @@ set_sequence_visibility, process_family, apply_animation_to_actor, + apply_animation, add_animation_to_sequencer, import_camera, + get_actor_and_skeleton, ) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index b53b781ee26..deb41662115 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -70,6 +70,25 @@ def get_assets_of_class(asset_list, class_name): return assets +def get_all_assets_of_class(class_name, path, recursive): + recursive = ast.literal_eval(recursive) + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + filter = unreal.ARFilter( + class_names=[class_name], + package_paths=[path], + recursive_paths=recursive) + + assets = ar.get_assets(filter) + + return [asset.get_editor_property('object_path') for asset in assets] + + +def get_first_asset_of_class(class_name, path, recursive): + return get_all_assets_of_class(class_name, path, recursive)[0] + + def save_listed_assets(asset_list): asset_list = ast.literal_eval(asset_list) for asset in asset_list: @@ -99,7 +118,9 @@ def _import( return task, options -def import_abc_task(task_properties, options_properties, options_extra_properties): +def import_abc_task( + task_properties, options_properties, options_extra_properties +): task, options = _import( unreal.AssetImportTask(), unreal.AbcImportSettings(), task_properties, options_properties, options_extra_properties) @@ -109,7 +130,9 @@ def import_abc_task(task_properties, options_properties, options_extra_propertie unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) -def import_fbx_task(task_properties, options_properties, options_extra_properties): +def import_fbx_task( + task_properties, options_properties, options_extra_properties +): task, options = _import( unreal.AssetImportTask(), unreal.FbxImportUI(), task_properties, options_properties, options_extra_properties) @@ -159,6 +182,7 @@ def generate_master_sequence( return sequence.get_path_name() + def set_sequence_hierarchy( parent_path, child_path, child_start_frame, child_end_frame ): @@ -169,7 +193,7 @@ def set_sequence_hierarchy( tracks = parent.get_master_tracks() subscene_track = None for t in tracks: - if (t.get_class() == + if (t.get_class() == unreal.MovieSceneSubTrack.static_class()): subscene_track = t break @@ -256,7 +280,6 @@ def process_family( sequence = get_asset(sequence_path) if sequence_path else None for asset in assets: - print(asset) obj = get_asset(asset) if obj and obj.get_class().get_name() == class_name: basis_matrix = unreal.Matrix( @@ -316,6 +339,47 @@ def apply_animation_to_actor(actor_path, animation_path): actor.skeletal_mesh_component.animation_data.set_editor_property( 'anim_to_play', animation) + +def apply_animation(animation_path, instance_name, sequences): + animation = get_asset(animation_path) + sequences = ast.literal_eval(sequences) + + anim_track_class = "MovieSceneSkeletalAnimationTrack" + anim_section_class = "MovieSceneSkeletalAnimationSection" + + for sequence_path in sequences: + sequence = get_asset(sequence_path) + possessables = [ + possessable for possessable in sequence.get_possessables() + if possessable.get_display_name() == instance_name] + + for possessable in possessables: + tracks = [ + track for track in possessable.get_tracks() + if (track.get_class().get_name() == anim_track_class)] + + if not tracks: + track = possessable.add_track( + unreal.MovieSceneSkeletalAnimationTrack) + tracks.append(track) + + for track in tracks: + sections = [ + section for section in track.get_sections() + if (section.get_class().get_name == anim_section_class)] + + if not sections: + sections.append(track.add_section()) + + for section in sections: + section.params.set_editor_property('animation', animation) + section.set_range( + sequence.get_playback_start(), + sequence.get_playback_end() - 1) + section.set_completion_mode( + unreal.MovieSceneCompletionMode.KEEP_STATE) + + def add_animation_to_sequencer(sequence_path, binding_guid, animation_path): sequence = get_asset(sequence_path) animation = get_asset(animation_path) @@ -379,8 +443,6 @@ def import_camera(sequence_path, import_filename): ue_major = int(ue_version[0]) ue_minor = int(ue_version[1]) - print(import_filename) - if ue_major == 4 and ue_minor <= 26: unreal.SequencerTools.import_fbx( world, @@ -400,3 +462,21 @@ def import_camera(sequence_path, import_filename): else: raise NotImplementedError( f"Unreal version {ue_major} not supported") + + +def get_actor_and_skeleton(instance_name): + actor_subsystem = unreal.EditorActorSubsystem() + actors = actor_subsystem.get_all_level_actors() + actor = None + 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 + + return (actor.get_path_name(), skeleton.get_path_name()) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 1fe0bef4623..e09b0d82e5c 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -3,18 +3,13 @@ import os import json -import unreal -from unreal import EditorAssetLibrary -from unreal import MovieSceneSkeletalAnimationTrack -from unreal import MovieSceneSkeletalAnimationSection - from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api import pipeline as up class AnimationFBXLoader(plugin.Loader): @@ -29,91 +24,75 @@ class AnimationFBXLoader(plugin.Loader): def _process(self, asset_dir, asset_name, instance_name): automated = False actor = None - - task = unreal.AssetImportTask() - task.options = unreal.FbxImportUI() + skeleton = None 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) + actor, skeleton = up.send_request_literal( + "get_actor_and_skeleton", params=[instance_name]) if not actor: return None asset_doc = get_current_project_asset(fields=["data.fps"]) - - task.set_editor_property('filename', self.fname) - 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', asset_doc.get("data", {}).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 - ) + fps = asset_doc.get("data", {}).get("fps") + + task_properties = [ + ("filename", up.format_string(self.fname)), + ("destination_path", up.format_string(asset_dir)), + ("destination_name", up.format_string(asset_name)), + ("replace_existing", "False"), + ("automated", str(automated)), + ("save", "False") + ] + + options_properties = [ + ("automated_import_should_detect_type", "False"), + ("original_import_type", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH"), + ("mesh_type_to_import", + "unreal.FBXImportType.FBXIT_ANIMATION"), + ("import_mesh", "False"), + ("import_animations", "True"), + ("override_full_name", "True"), + ("skeleton", f"get_asset({up.format_string(skeleton)})") + ] + + options_extra_properties = [ + ("anim_sequence_import_data", "animation_length", + "unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME"), + ("anim_sequence_import_data", + "import_meshes_in_bone_hierarchy", "False"), + ("anim_sequence_import_data", "use_default_sample_rate", "False"), + ("anim_sequence_import_data", "custom_sample_rate", str(fps)), + ("anim_sequence_import_data", "import_custom_attribute", "True"), + ("anim_sequence_import_data", "import_bone_tracks", "True"), + ("anim_sequence_import_data", "remove_redundant_keys", "False"), + ("anim_sequence_import_data", "convert_scene", "True") + ] + + up.send_request( + "import_fbx_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "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 + animations = up.send_request_literal( + "get_assets_of_class", + params=[asset_content, "AnimSequence"]) + if animations: + animation = animations[0] 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) + up.send_request( + "apply_animation_to_actor", params=[actor, animation]) return animation @@ -145,37 +124,30 @@ def load(self, context, name, namespace, options=None): asset = context.get('asset').get('name') suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/Animations/{asset}/{name}", suffix="") - ar = unreal.AssetRegistryHelpers.get_asset_registry() + asset_dir, container_name = up.send_request_literal( + "create_unique_asset_name", + params=[f"{root}/Animations", asset, name]) - _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_editor_property('object_path') + master_level = up.send_request( + "get_first_asset_of_class", + params=["World", f"{root}/{hierarchy[0]}", "False"]) hierarchy_dir = root for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_dir = f"{hierarchy_dir}/{asset}" - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{hierarchy_dir}/"], - recursive_paths=True) - levels = ar.get_assets(_filter) - level = levels[0].get_editor_property('object_path') + level = up.send_request( + "get_first_asset_of_class", + params=["World", f"{hierarchy_dir}/", "False"]) - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorLevelLibrary.load_level(level) + up.send_request("save_all_dirty_levels") + up.send_request("load_level", params=[level]) container_name += suffix - EditorAssetLibrary.make_directory(asset_dir) + up.send_request("make_directory", params=[asset_dir]) libpath = self.fname.replace("fbx", "json") @@ -186,41 +158,24 @@ def load(self, context, name, namespace, options=None): animation = self._process(asset_dir, asset_name, instance_name) - asset_content = EditorAssetLibrary.list_assets( - hierarchy_dir, recursive=True, include_folder=False) + asset_content = up.send_request_literal( + "list_assets", params=[hierarchy_dir, "True", "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) + all_sequences = up.send_request_literal( + "get_assets_of_class", + params=[asset_content, "LevelSequence"]) + sequences = [ + a for a in all_sequences + if "_camera" not in a.split("/")[-1]] + + up.send_request( + "apply_animation", + params=[animation, instance_name, str(sequences)]) # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + up.send_request( + "create_container", params=[container_name, asset_dir]) data = { "schema": "openpype:container-2.0", @@ -230,100 +185,103 @@ def load(self, context, name, namespace, options=None): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - 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, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - asset_doc = get_current_project_asset(fields=["data.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', 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', asset_doc.get("data", {}).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": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - 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) + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) + + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "False"]) + + up.send_request( + "save_listed_assets", params=[str(asset_content)]) + + up.send_request("save_current_level") + up.send_request("load_level", params=[master_level]) + + return asset_content + + # def update(self, container, representation): + # name = container["asset_name"] + # source_path = get_representation_path(representation) + # asset_doc = get_current_project_asset(fields=["data.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', 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', asset_doc.get("data", {}).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 + # up.imprint( + # container_path, + # { + # "representation": str(representation["_id"]), + # "parent": str(representation["parent"]) + # }) + + # 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) From 7bf315ed358e20ab66c69bdefb14bd6c97ff01a1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 7 Nov 2022 12:12:10 +0000 Subject: [PATCH 29/55] Fixed some transform problems --- .../UE_5.0/Content/Python/plugins/load.py | 100 +++++++++++++----- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index deb41662115..eb955bc18d8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -266,46 +266,96 @@ def set_sequence_visibility( hid_section.set_level_names(maps) +def get_transform(actor, import_data, basis_data, transform_data): + filename = import_data.get_first_filename() + path = Path(filename) + + conversion = unreal.Matrix.IDENTITY.transform() + tuning = unreal.Matrix.IDENTITY.transform() + + basis = unreal.Matrix( + basis_data[0], + basis_data[1], + basis_data[2], + basis_data[3] + ).transform() + transform = unreal.Matrix( + transform_data[0], + transform_data[1], + transform_data[2], + transform_data[3] + ).transform() + + # Check for the conversion settings. We cannot access + # the alembic conversion settings, so we assume that + # the maya ones have been applied. + if path.suffix == '.fbx': + loc = import_data.import_translation + rot = import_data.import_rotation.to_vector() + scale = import_data.import_uniform_scale + conversion = unreal.Transform( + location=[loc.x, loc.y, loc.z], + rotation=[rot.x, -rot.y, -rot.z], + scale=[scale, scale, scale] + ) + tuning = unreal.Transform( + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0] + ) + elif path.suffix == '.abc': + # This is the standard conversion settings for + # alembic files from Maya. + conversion = unreal.Transform( + location=[0.0, 0.0, 0.0], + rotation=[90.0, 0.0, 0.0], + scale=[1.0, -1.0, 1.0] + ) + tuning = unreal.Transform( + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0] + ) + + new_transform = basis.inverse() * transform * basis + return tuning * conversion.inverse() * new_transform + def process_family( assets_str, class_name, instance_name, transform_str, basis_str, sequence_path ): assets = ast.literal_eval(assets_str) - basis = ast.literal_eval(transform_str) - transform = ast.literal_eval(basis_str) + basis_data = ast.literal_eval(basis_str) + transform_data = ast.literal_eval(transform_str) actors = [] bindings = [] + component_property = '' + mesh_property = '' + + if class_name == 'StaticMesh': + component_property = 'static_mesh_component' + mesh_property = 'static_mesh' + elif class_name == 'SkeletalMesh': + component_property = 'skeletal_mesh_component' + mesh_property = 'skeletal_mesh' + sequence = get_asset(sequence_path) if sequence_path else None for asset in assets: obj = get_asset(asset) if obj and obj.get_class().get_name() == class_name: - 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 - ).transform() actor = unreal.EditorLevelLibrary.spawn_actor_from_object( - obj, new_transform.translation) - if instance_name: - try: - actor.set_actor_label(instance_name) - except Exception as e: - print(e) - actor.set_actor_rotation(new_transform.rotation.rotator(), False) - actor.set_actor_scale3d(new_transform.scale3d) + obj, unreal.Vector(0.0, 0.0, 0.0)) + actor.set_actor_label(instance_name) + + component = actor.get_editor_property(component_property) + mesh = component.get_editor_property(mesh_property) + import_data = mesh.get_editor_property('asset_import_data') + + transform = get_transform( + actor, import_data, basis_data, transform_data) + + actor.set_actor_transform(transform, False, True) if class_name == 'SkeletalMesh': skm_comp = actor.get_editor_property('skeletal_mesh_component') From 12d8ea886064d34b161a3d46668cfa43eb080a30 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 7 Nov 2022 12:24:31 +0000 Subject: [PATCH 30/55] Hound fixes --- openpype/hosts/unreal/api/communication_server.py | 4 ---- openpype/hosts/unreal/api/launch_script.py | 12 +++++++----- .../integration/UE_5.0/Content/Python/pipeline.py | 4 +++- .../UE_5.0/Content/Python/plugins/load.py | 1 + 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/api/communication_server.py b/openpype/hosts/unreal/api/communication_server.py index 54f4ce9ed7d..b452b5c1f7b 100644 --- a/openpype/hosts/unreal/api/communication_server.py +++ b/openpype/hosts/unreal/api/communication_server.py @@ -6,11 +6,7 @@ import asyncio import logging import socket -import platform -import filecmp -import tempfile import threading -import shutil from queue import Queue from contextlib import closing import aiohttp diff --git a/openpype/hosts/unreal/api/launch_script.py b/openpype/hosts/unreal/api/launch_script.py index a40a530b573..5dd301f5612 100644 --- a/openpype/hosts/unreal/api/launch_script.py +++ b/openpype/hosts/unreal/api/launch_script.py @@ -7,21 +7,22 @@ import platform import logging -logging.basicConfig(level=logging.DEBUG) - from Qt import QtWidgets, QtCore, QtGui -from openpype import style -from openpype.pipeline import install_host -from openpype.hosts.unreal.api import UnrealHost from openpype.hosts.unreal.api.communication_server import ( CommunicationWrapper ) +from openpype.hosts.unreal.api import UnrealHost +from openpype.pipeline import install_host +from openpype import style + +logging.basicConfig(level=logging.DEBUG) def safe_excepthook(*args): traceback.print_exception(*args) + def main(launch_args): # Be sure server won't crash at any moment but just print traceback @@ -78,6 +79,7 @@ def signal_handler(*_args): # Run Qt application event processing sys.exit(qt_app.exec_()) + if __name__ == "__main__": args = list(sys.argv) if os.path.abspath(__file__) == os.path.normpath(args[0]): diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py index cedffb2dfde..cccc79da204 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py @@ -227,7 +227,9 @@ def ls(): return containers -def containerise(name, namespc, str_nodes, str_context, loader="", suffix="_CON"): +def containerise( + name, namespc, str_nodes, str_context, loader="", 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. diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index eb955bc18d8..5e26e9ca3ce 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -318,6 +318,7 @@ def get_transform(actor, import_data, basis_data, transform_data): new_transform = basis.inverse() * transform * basis return tuning * conversion.inverse() * new_transform + def process_family( assets_str, class_name, instance_name, transform_str, basis_str, sequence_path From 3fda52987aff073bcdf3205fc26135e99d248669 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 7 Nov 2022 12:29:13 +0000 Subject: [PATCH 31/55] More hound fixes --- openpype/hosts/unreal/api/launch_script.py | 1 - .../hosts/unreal/plugins/load/load_alembic_geometrycache.py | 3 ++- .../hosts/unreal/plugins/load/load_alembic_skeletalmesh.py | 3 ++- openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py | 3 ++- openpype/hosts/unreal/plugins/load/load_layout.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_rig.py | 3 ++- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/unreal/api/launch_script.py b/openpype/hosts/unreal/api/launch_script.py index 5dd301f5612..173929c3bf3 100644 --- a/openpype/hosts/unreal/api/launch_script.py +++ b/openpype/hosts/unreal/api/launch_script.py @@ -2,7 +2,6 @@ import sys import signal import traceback -import subprocess import ctypes import platform import logging diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py index 2ecf712b8ba..9e1e19469e1 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py @@ -93,7 +93,8 @@ def load(self, context, name, namespace, options): if not default_conversion: options_extra_properties.extend([ - ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "preset", + "unreal.AbcConversionPreset.CUSTOM"), ("conversion_settings", "flip_u", "False"), ("conversion_settings", "flip_v", "True"), ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py index 4ab242e035d..c22dfd17314 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py @@ -81,7 +81,8 @@ def load(self, context, name, namespace, options): if not default_conversion: options_extra_properties.extend([ - ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "preset", + "unreal.AbcConversionPreset.CUSTOM"), ("conversion_settings", "flip_u", "False"), ("conversion_settings", "flip_v", "False"), ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index 43247620981..c59d85bc208 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -83,7 +83,8 @@ def load(self, context, name, namespace, options): if not default_conversion: options_extra_properties.extend([ - ("conversion_settings", "preset", "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "preset", + "unreal.AbcConversionPreset.CUSTOM"), ("conversion_settings", "flip_u", "False"), ("conversion_settings", "flip_v", "False"), ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 0a87cd71320..7c6c2da60d5 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -458,8 +458,8 @@ def load(self, context, name, namespace, options): for sequence in existing_sequences: sequences.append(sequence) frame_range = up.send_request_literal( - "get_sequence_frame_range", - params=[sequence]) + "get_sequence_frame_range", + params=[sequence]) frame_ranges.append(frame_range) project_name = legacy_io.active_project() diff --git a/openpype/hosts/unreal/plugins/load/load_rig.py b/openpype/hosts/unreal/plugins/load/load_rig.py index d61a0f7cd83..44595cbdf6b 100644 --- a/openpype/hosts/unreal/plugins/load/load_rig.py +++ b/openpype/hosts/unreal/plugins/load/load_rig.py @@ -79,7 +79,8 @@ def load(self, context, name, namespace, options): ("import_textures", "False"), ("skeleton", "None"), ("create_physics_asset", "False"), - ("mesh_type_to_import", "unreal.FBXImportType.FBXIT_SKELETAL_MESH") + ("mesh_type_to_import", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH") ] options_extra_properties = [ From 712bf1e0d79fa7222423fc633d797b8b39230886 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 16 Nov 2022 10:52:14 +0000 Subject: [PATCH 32/55] Implemented update and remove for all the assets --- .../UE_5.0/Content/Python/init_unreal.py | 4 + .../UE_5.0/Content/Python/plugins/load.py | 253 ++++++++++++++- .../load/load_alembic_geometrycache.py | 142 ++++---- .../plugins/load/load_alembic_skeletalmesh.py | 128 ++++---- .../plugins/load/load_alembic_staticmesh.py | 133 ++++---- .../hosts/unreal/plugins/load/load_camera.py | 303 ++++-------------- .../hosts/unreal/plugins/load/load_layout.py | 258 ++++----------- .../hosts/unreal/plugins/load/load_rig.py | 182 ++++------- .../unreal/plugins/load/load_staticmeshfbx.py | 109 +++---- 9 files changed, 695 insertions(+), 817 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py index 666613d0a4d..6d59ea93711 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -34,4 +34,8 @@ add_animation_to_sequencer, import_camera, get_actor_and_skeleton, + remove_asset, + delete_all_bound_assets, + remove_camera, + remove_layout, ) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py index 5e26e9ca3ce..41fc95846b9 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py @@ -1,3 +1,5 @@ +from pipeline import ls + import ast from pathlib import Path @@ -44,6 +46,17 @@ def save_all_dirty_levels(): unreal.EditorLevelLibrary.save_all_dirty_levels() +def get_current_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/"): + return curr_level_path + + return "" + + def add_level_to_world(level_path): unreal.EditorLevelUtils.add_level_to_world( unreal.EditorLevelLibrary.get_editor_world(), @@ -82,7 +95,7 @@ def get_all_assets_of_class(class_name, path, recursive): assets = ar.get_assets(filter) - return [asset.get_editor_property('object_path') for asset in assets] + return [str(asset.get_editor_property('object_path')) for asset in assets] def get_first_asset_of_class(class_name, path, recursive): @@ -174,7 +187,7 @@ def generate_master_sequence( tracks = sequence.get_master_tracks() track = None for t in tracks: - if (t.get_class() == unreal.MovieSceneCameraCutTrack.static_class()): + if t.get_class().get_name() == "MovieSceneCameraCutTrack": track = t break if not track: @@ -193,8 +206,7 @@ def set_sequence_hierarchy( tracks = parent.get_master_tracks() subscene_track = None for t in tracks: - if (t.get_class() == - unreal.MovieSceneSubTrack.static_class()): + if t.get_class().get_name() == "MovieSceneSubTrack": subscene_track = t break if not subscene_track: @@ -227,8 +239,7 @@ def set_sequence_visibility( tracks = parent.get_master_tracks() visibility_track = None for t in tracks: - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): + if t.get_class().get_name() == "MovieSceneLevelVisibilityTrack": visibility_track = t break if not visibility_track: @@ -531,3 +542,233 @@ def get_actor_and_skeleton(instance_name): skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton return (actor.get_path_name(), skeleton.get_path_name()) + + +def remove_asset(path): + parent_path = Path(path).parent.as_posix() + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False, include_folder=True + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) + + +def delete_all_bound_assets(level_sequence_str): + level_sequence = get_asset(level_sequence_str) + + # Delete from the current level all the assets that are bound to the + # level sequence. + + # Get the actors in the level sequence. + bound_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 obj in bound_objs: + actor_path = obj.bound_objects[0].get_path_name().split(":")[-1] + actor = unreal.EditorLevelLibrary.get_actor_reference(actor_path) + unreal.EditorLevelLibrary.destroy_actor(actor) + + +def remove_camera(root, asset_dir): + path = Path(asset_dir) + parent_path = path.parent.as_posix() + + old_sequence = get_first_asset_of_class( + "LevelSequence", path.as_posix(), "False") + level = get_first_asset_of_class("World", parent_path, "True") + + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(level) + + # There should be only one sequence in the path. + level_sequence = get_asset(old_sequence) + sequence_name = level_sequence.get_fname() + + delete_all_bound_assets(level_sequence.get_path_name()) + + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to find + # the level sequence. + namespace = asset_dir.replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + master_sequence = get_asset(get_first_asset_of_class( + "LevelSequence", f"{root}/{ms_asset}", "False")) + + sequences = [master_sequence] + + parent_sequence = None + for sequence in sequences: + tracks = sequence.get_master_tracks() + subscene_track = None + for track in tracks: + if track.get_class().get_name() == "MovieSceneSubTrack": + subscene_track = track + break + if subscene_track: + sections = subscene_track.get_sections() + for section in sections: + if section.get_sequence().get_name() == sequence_name: + parent_sequence = sequence + subscene_track.remove_section(section) + break + sequences.append(section.get_sequence()) + # Update subscenes indexes. + for i, section in enumerate(sections): + section.set_row_index(i) + + if parent_sequence: + break + + assert parent_sequence, "Could not find the parent sequence" + + unreal.EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) + + # Check if there isn't any more assets in the parent folder, and + # delete it if not. + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False, include_folder=True + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) + + return parent_sequence.get_path_name() + + +def remove_layout( + root, asset, asset_dir, asset_name, loaded_assets, create_sequences +): + path = Path(asset_dir) + parent_path = path.parent.as_posix() + + level_sequence = get_asset(get_first_asset_of_class( + "LevelSequence", path.as_posix(), "False")) + level = get_first_asset_of_class("World", parent_path, "True") + + unreal.EditorLevelLibrary.load_level(level) + + containers = ls() + layout_containers = [ + c for c in containers + if c.get('asset_name') != 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 loaded_asset in eval(loaded_assets): + layouts = [ + lc for lc in layout_containers + if loaded_asset in lc.get('loaded_assets')] + + if not layouts: + unreal.EditorAssetLibrary.delete_directory( + Path(loaded_asset).parent.as_posix()) + + # Delete the parent folder if there aren't any more + # layouts in it. + asset_content = unreal.EditorAssetLibrary.list_assets( + Path(loaded_asset).parent.parent.as_posix(), recursive=False, + include_folder=True + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory( + str(Path(loaded_asset).parent.parent)) + + master_sequence = None + master_level = None + sequences = [] + + if create_sequences: + delete_all_bound_assets(level_sequence.get_path_name()) + + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to + # find the level sequence. + namespace = asset_dir.replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + master_sequence = get_asset(get_first_asset_of_class( + "LevelSequence", f"{root}/{ms_asset}", "False")) + master_level = get_first_asset_of_class( + "World", f"{root}/{ms_asset}", "False") + + sequences = [master_sequence] + + parent = None + for sequence in sequences: + tracks = sequence.get_master_tracks() + subscene_track = None + visibility_track = None + for track in tracks: + if track.get_class().get_name() == "MovieSceneSubTrack": + subscene_track = track + if (track.get_class().get_name() == + "MovieSceneLevelVisibilityTrack"): + visibility_track = track + if subscene_track: + sections = subscene_track.get_sections() + for section in sections: + if section.get_sequence().get_name() == asset: + parent = sequence + subscene_track.remove_section(section) + break + sequences.append(section.get_sequence()) + # Update subscenes indexes. + for i, section in enumerate(sections): + section.set_row_index(i) + + + if visibility_track: + sections = visibility_track.get_sections() + for section in sections: + if (unreal.Name(f"{asset}_map") + in section.get_level_names()): + visibility_track.remove_section(section) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for section in sections: + if prev_name != section.get_level_names(): + i += 1 + section.set_row_index(i) + prev_name = section.get_level_names() + if parent: + break + + assert parent, "Could not find the parent sequence" + + actors = unreal.EditorLevelLibrary.get_all_level_actors() + + if not actors: + # Delete the level if it's empty. + # Create a temporary level to delete the layout level. + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorAssetLibrary.make_directory(f"{root}/tmp") + tmp_level = f"{root}/tmp/temp_map" + if not unreal.EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + unreal.EditorLevelLibrary.new_level(tmp_level) + else: + unreal.EditorLevelLibrary.load_level(tmp_level) + + # Delete the layout directory. + unreal.EditorAssetLibrary.delete_directory(path.as_posix()) + + if not actors: + unreal.EditorAssetLibrary.delete_directory(path.parent.as_posix()) + + if create_sequences: + unreal.EditorLevelLibrary.load_level(master_level) + unreal.EditorAssetLibrary.delete_directory(f"{root}/tmp") diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py index 9e1e19469e1..a135051e6ca 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py @@ -19,6 +19,47 @@ class PointCacheAlembicLoader(plugin.Loader): icon = "cube" color = "orange" + def _import_fbx_task( + self, filename, destination_path, destination_name, replace, + frame_start, frame_end, default_conversion + ): + task_properties = [ + ("filename", up.format_string(filename)), + ("destination_path", up.format_string(destination_path)), + ("destination_name", up.format_string(destination_name)), + ("replace_existing", str(replace)), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("import_type", "unreal.AlembicImportType.GEOMETRY_CACHE") + ] + + options_extra_properties = [ + ("geometry_cache_settings", "flatten_tracks", "False"), + ("sampling_settings", "frame_start", str(frame_start)), + ("sampling_settings", "frame_end", str(frame_end)) + ] + + if not default_conversion: + options_extra_properties.extend([ + ("conversion_settings", "preset", + "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "flip_u", "False"), + ("conversion_settings", "flip_v", "True"), + ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), + ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ]) + + up.send_request( + "import_abc_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -72,42 +113,9 @@ def load(self, context, name, namespace, options): if frame_start == frame_end: frame_end += 1 - task_properties = [ - ("filename", up.format_string(self.fname)), - ("destination_path", up.format_string(asset_dir)), - ("destination_name", up.format_string(asset_name)), - ("replace_existing", "False"), - ("automated", "True"), - ("save", "True") - ] - - options_properties = [ - ("import_type", "unreal.AlembicImportType.GEOMETRY_CACHE") - ] - - options_extra_properties = [ - ("geometry_cache_settings", "flatten_tracks", "False"), - ("sampling_settings", "frame_start", str(frame_start)), - ("sampling_settings", "frame_end", str(frame_end)) - ] - - if not default_conversion: - options_extra_properties.extend([ - ("conversion_settings", "preset", - "unreal.AbcConversionPreset.CUSTOM"), - ("conversion_settings", "flip_u", "False"), - ("conversion_settings", "flip_v", "True"), - ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), - ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") - ]) - - up.send_request( - "import_abc_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) + self._import_fbx_task( + self.fname, asset_dir, asset_name, False, + frame_start, frame_end, default_conversion) # Create Asset Container up.send_request( @@ -123,7 +131,10 @@ def load(self, context, name, namespace, options): "loader": str(self.__class__.__name__), "representation": str(context["representation"]["_id"]), "parent": str(context["representation"]["parent"]), - "family": context["representation"]["context"]["family"] + "family": context["representation"]["context"]["family"], + "frame_start": context["asset"]["data"]["frameStart"], + "frame_end": context["asset"]["data"]["frameEnd"], + "default_conversion": default_conversion } up.send_request( "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) @@ -136,42 +147,35 @@ def load(self, context, name, namespace, options): return asset_content - # def update(self, container, representation): - # name = container["asset_name"] - # source_path = get_representation_path(representation) - # destination_path = container["namespace"] - - # task = self.get_task(source_path, destination_path, name, True) + def update(self, container, representation): + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] + container_name = container['objectName'] - # # do import fbx and replace existing data - # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + frame_start = container["frameStart"] + frame_end = container["frameStart"] + default_conversion = container["default_conversion"] - # container_path = "{}/{}".format(container["namespace"], - # container["objectName"]) - # # update metadata - # up.imprint( - # container_path, - # { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]) - # }) + self._import_fbx_task( + filename, asset_dir, asset_name, True, + frame_start, frame_end, default_conversion) - # asset_content = unreal.EditorAssetLibrary.list_assets( - # destination_path, recursive=True, include_folder=True - # ) - - # for a in asset_content: - # unreal.EditorAssetLibrary.save_asset(a) + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - # def remove(self, container): - # path = container["namespace"] - # parent_path = os.path.dirname(path) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - # unreal.EditorAssetLibrary.delete_directory(path) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) - # asset_content = unreal.EditorAssetLibrary.list_assets( - # parent_path, recursive=False - # ) + def remove(self, container): + path = container["namespace"] - # if len(asset_content) == 0: - # unreal.EditorAssetLibrary.delete_directory(parent_path) + up.send_request( + "remove_asset", params=[path]) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py index c22dfd17314..97f3453e8cf 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_skeletalmesh.py @@ -19,6 +19,43 @@ class SkeletalMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" + def _import_fbx_task( + self, filename, destination_path, destination_name, replace, + default_conversion + ): + task_properties = [ + ("filename", up.format_string(filename)), + ("destination_path", up.format_string(destination_path)), + ("destination_name", up.format_string(destination_name)), + ("replace_existing", str(replace)), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("import_type", "unreal.AlembicImportType.SKELETAL") + ] + + options_extra_properties = [] + + if not default_conversion: + options_extra_properties.extend([ + ("conversion_settings", "preset", + "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "flip_u", "False"), + ("conversion_settings", "flip_v", "False"), + ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), + ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ]) + + up.send_request( + "import_abc_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -64,38 +101,8 @@ def load(self, context, name, namespace, options): "does_directory_exist", params=[asset_dir]): up.send_request("make_directory", params=[asset_dir]) - task_properties = [ - ("filename", up.format_string(self.fname)), - ("destination_path", up.format_string(asset_dir)), - ("destination_name", up.format_string(asset_name)), - ("replace_existing", "False"), - ("automated", "True"), - ("save", "True") - ] - - options_properties = [ - ("import_type", "unreal.AlembicImportType.SKELETAL") - ] - - options_extra_properties = [] - - if not default_conversion: - options_extra_properties.extend([ - ("conversion_settings", "preset", - "unreal.AbcConversionPreset.CUSTOM"), - ("conversion_settings", "flip_u", "False"), - ("conversion_settings", "flip_v", "False"), - ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), - ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") - ]) - - up.send_request( - "import_abc_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) + self._import_fbx_task( + self.fname, asset_dir, asset_name, False, default_conversion) # Create Asset Container up.send_request( @@ -111,7 +118,8 @@ def load(self, context, name, namespace, options): "loader": str(self.__class__.__name__), "representation": str(context["representation"]["_id"]), "parent": str(context["representation"]["parent"]), - "family": context["representation"]["context"]["family"] + "family": context["representation"]["context"]["family"], + "default_conversion": default_conversion } up.send_request( "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) @@ -124,41 +132,31 @@ def load(self, context, name, namespace, options): return asset_content - # def update(self, container, representation): - # name = container["asset_name"] - # source_path = get_representation_path(representation) - # destination_path = container["namespace"] - - # task = self.get_task(source_path, destination_path, name, True) + def update(self, container, representation): + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] + container_name = container['objectName'] + default_conversion = container["default_conversion"] - # # do import fbx and replace existing data - # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - # container_path = "{}/{}".format(container["namespace"], - # container["objectName"]) - # # update metadata - # up.imprint( - # container_path, - # { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]) - # }) + self._import_fbx_task( + filename, asset_dir, asset_name, True, default_conversion) - # asset_content = unreal.EditorAssetLibrary.list_assets( - # destination_path, recursive=True, include_folder=True - # ) - - # for a in asset_content: - # unreal.EditorAssetLibrary.save_asset(a) + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - # def remove(self, container): - # path = container["namespace"] - # parent_path = os.path.dirname(path) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - # unreal.EditorAssetLibrary.delete_directory(path) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) - # asset_content = unreal.EditorAssetLibrary.list_assets( - # parent_path, recursive=False - # ) + def remove(self, container): + path = container["namespace"] - # if len(asset_content) == 0: - # unreal.EditorAssetLibrary.delete_directory(parent_path) + up.send_request( + "remove_asset", params=[path]) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index c59d85bc208..eed78930a6c 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -19,6 +19,45 @@ class StaticMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" + def _import_fbx_task( + self, filename, destination_path, destination_name, replace, + default_conversion + ): + task_properties = [ + ("filename", up.format_string(filename)), + ("destination_path", up.format_string(destination_path)), + ("destination_name", up.format_string(destination_name)), + ("replace_existing", str(replace)), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("import_type", "unreal.AlembicImportType.STATIC_MESH") + ] + + options_extra_properties = [ + ("static_mesh_settings", "merge_meshes", "True") + ] + + if not default_conversion: + options_extra_properties.extend([ + ("conversion_settings", "preset", + "unreal.AbcConversionPreset.CUSTOM"), + ("conversion_settings", "flip_u", "False"), + ("conversion_settings", "flip_v", "False"), + ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), + ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") + ]) + + up.send_request( + "import_abc_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -64,40 +103,8 @@ def load(self, context, name, namespace, options): "does_directory_exist", params=[asset_dir]): up.send_request("make_directory", params=[asset_dir]) - task_properties = [ - ("filename", up.format_string(self.fname)), - ("destination_path", up.format_string(asset_dir)), - ("destination_name", up.format_string(asset_name)), - ("replace_existing", "False"), - ("automated", "True"), - ("save", "True") - ] - - options_properties = [ - ("import_type", "unreal.AlembicImportType.STATIC_MESH") - ] - - options_extra_properties = [ - ("static_mesh_settings", "merge_meshes", "True") - ] - - if not default_conversion: - options_extra_properties.extend([ - ("conversion_settings", "preset", - "unreal.AbcConversionPreset.CUSTOM"), - ("conversion_settings", "flip_u", "False"), - ("conversion_settings", "flip_v", "False"), - ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), - ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") - ]) - - up.send_request( - "import_abc_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) + self._import_fbx_task( + self.fname, asset_dir, asset_name, False, default_conversion) # Create Asset Container up.send_request( @@ -113,7 +120,8 @@ def load(self, context, name, namespace, options): "loader": str(self.__class__.__name__), "representation": str(context["representation"]["_id"]), "parent": str(context["representation"]["parent"]), - "family": context["representation"]["context"]["family"] + "family": context["representation"]["context"]["family"], + "default_conversion": default_conversion } up.send_request( "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) @@ -126,42 +134,31 @@ def load(self, context, name, namespace, options): return asset_content - # def update(self, container, representation): - # name = container["asset_name"] - # source_path = get_representation_path(representation) - # destination_path = container["namespace"] + def update(self, container, representation): + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] + container_name = container['objectName'] + default_conversion = container["default_conversion"] - # task = self.get_task(source_path, destination_path, name, True) + self._import_fbx_task( + filename, asset_dir, asset_name, True, default_conversion) - # # do import fbx and replace existing data - # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # container_path = "{}/{}".format(container["namespace"], - # container["objectName"]) - # # update metadata - # up.imprint( - # container_path, - # { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]) - # }) - - # asset_content = unreal.EditorAssetLibrary.list_assets( - # destination_path, recursive=True, include_folder=True - # ) - - # for a in asset_content: - # unreal.EditorAssetLibrary.save_asset(a) + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - # def remove(self, container): - # path = container["namespace"] - # parent_path = os.path.dirname(path) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - # unreal.EditorAssetLibrary.delete_directory(path) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) - # asset_content = unreal.EditorAssetLibrary.list_assets( - # parent_path, recursive=False - # ) + def remove(self, container): + path = container["namespace"] - # if len(asset_content) == 0: - # unreal.EditorAssetLibrary.delete_directory(parent_path) + up.send_request( + "remove_asset", params=[path]) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 52a5591a1d7..801eb4ac2ec 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -246,246 +246,63 @@ def load(self, context, name, namespace, options): return asset_content - # def update(self, container, representation): - # ar = unreal.AssetRegistryHelpers.get_asset_registry() - - # root = "/Game/OpenPype" - - # asset_dir = container.get('namespace') - - # context = representation.get("context") - - # hierarchy = context.get('hierarchy').split("/") - # h_dir = f"{root}/{hierarchy[0]}" - # h_asset = hierarchy[0] - # master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - - # 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=[str(Path(asset_dir).parent.as_posix())], - # recursive_paths=True) - # maps = ar.get_assets(filter) - - # # There should be only one map in the list - # EditorLevelLibrary.load_level(maps[0].get_full_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/OpenPype" - # 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() - - # 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 - # break - # 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 - # # 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 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) - - # self._import_camera( - # EditorLevelLibrary.get_editor_world(), - # new_sequence, - # new_sequence.get_bindings(), - # settings, - # str(representation["data"]["path"]) - # ) - - # data = { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]) - # } - # up.imprint( - # "{}/{}".format(asset_dir, container.get('container_name')), data) - - # EditorLevelLibrary.save_current_level() - - # asset_content = EditorAssetLibrary.list_assets( - # asset_dir, recursive=True, include_folder=False) - - # for a in asset_content: - # EditorAssetLibrary.save_asset(a) - - # EditorLevelLibrary.load_level(master_level) - - # def remove(self, container): - # path = Path(container.get("namespace")) - # parent_path = str(path.parent.as_posix()) - - # ar = unreal.AssetRegistryHelpers.get_asset_registry() - # filter = unreal.ARFilter( - # class_names=["LevelSequence"], - # package_paths=[f"{str(path.as_posix())}"], - # 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=[f"{parent_path}"], - # 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_full_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_full_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/OpenPype" - # 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() - - # sequences = [master_sequence] - - # parent = 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 - # break - # 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. - # i = 0 - # for ss in sections: - # ss.set_row_index(i) - # i += 1 - - # if parent: - # break - - # assert parent, "Could not find the parent sequence" - - # EditorAssetLibrary.delete_directory(str(path.as_posix())) - - # # Check if there isn't any more assets in the parent folder, and - # # delete it if not. - # asset_content = EditorAssetLibrary.list_assets( - # parent_path, recursive=False, include_folder=True - # ) - - # if len(asset_content) == 0: - # EditorAssetLibrary.delete_directory(parent_path) + def update(self, container, representation): + asset_dir = container.get("namespace") + context = representation.get("context") + asset = container.get('asset') + + root = "/Game/OpenPype" + hierarchy = context.get('hierarchy').split("/") + h_dir = f"{root}/{hierarchy[0]}" + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + + parent_sequence = up.send_request( + "remove_camera", params=[root, asset_dir]) + + project_name = legacy_io.active_project() + data = get_asset_by_name(project_name, asset)["data"] + start_frame = 0 + end_frame = data.get('clipOut') - data.get('clipIn') + 1 + fps = data.get("fps") + + cam_sequence = up.send_request( + "generate_sequence", + params=[ + f"{asset}_camera", asset_dir, start_frame, end_frame, fps]) + + up.send_request( + "set_sequence_hierarchy", + params=[ + parent_sequence, cam_sequence, + data.get('clipIn'), data.get('clipOut')]) + + up.send_request( + "import_camera", + params=[ + cam_sequence, str(representation["data"]["path"])]) + + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + up.send_request( + "imprint", params=[f"{asset_dir}/{container.get('container_name')}", str(data)]) + + up.send_request("save_all_dirty_levels") + up.send_request("load_level", params=[master_level]) + + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) + + up.send_request( + "save_listed_assets", params=[str(asset_content)]) + + def remove(self, container): + root = "/Game/OpenPype" + + up.send_request( + "remove_camera", params=[root, container.get("namespace")]) + + up.send_request( + "remove_asset", params=[container.get("namespace")]) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 7c6c2da60d5..3638f85f364 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -539,219 +539,85 @@ def load(self, context, name, namespace, options): return asset_content - # def update(self, container, representation): - # data = get_current_project_settings() - # create_sequences = data["unreal"]["level_sequences_for_layouts"] + def update(self, container, representation): + root = "/Game/OpenPype" + asset_dir = container.get('namespace') + context = representation.get("context") - # ar = unreal.AssetRegistryHelpers.get_asset_registry() - - # root = "/Game/OpenPype" + data = get_current_project_settings() + create_sequences = data["unreal"]["level_sequences_for_layouts"] - # asset_dir = container.get('namespace') - # context = representation.get("context") + master_level = None + prev_level = None + layout_sequence = "" - # sequence = None - # master_level = None + if create_sequences: + hierarchy = context.get('hierarchy').split("/") + h_dir = f"{root}/{hierarchy[0]}" + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - # if create_sequences: - # hierarchy = context.get('hierarchy').split("/") - # h_dir = f"{root}/{hierarchy[0]}" - # h_asset = hierarchy[0] - # master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + parent_path = Path(asset_dir).parent.as_posix() - # filter = unreal.ARFilter( - # class_names=["LevelSequence"], - # package_paths=[asset_dir], - # recursive_paths=False) - # sequences = ar.get_assets(filter) - # sequence = sequences[0].get_asset() + layout_level = up.send_request( + "get_first_asset_of_class", + params=["World", parent_path, "False"]) - # prev_level = None + up.send_request("load_level", params=[layout_level]) - # 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 + layout_sequence = up.send_request( + "get_first_asset_of_class", + params=["LevelSequence", asset_dir, "False"]) - # # Get layout level - # filter = unreal.ARFilter( - # class_names=["World"], - # package_paths=[asset_dir], - # recursive_paths=False) - # levels = ar.get_assets(filter) + up.send_request( + "delete_all_bound_assets", params=[layout_sequence]) - # layout_level = levels[0].get_editor_property('object_path') + if not master_level: + prev_level = up.send_request("get_current_level") - # EditorLevelLibrary.save_all_dirty_levels() - # EditorLevelLibrary.load_level(layout_level) + source_path = get_representation_path(representation) - # # Delete all the actors in the level - # actors = unreal.EditorLevelLibrary.get_all_level_actors() - # for actor in actors: - # unreal.EditorLevelLibrary.destroy_actor(actor) + loaded_assets = self._process(source_path, asset_dir, layout_sequence) - # if create_sequences: - # EditorLevelLibrary.save_current_level() + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]), + "loaded_assets": loaded_assets + } + up.send_request( + "imprint", params=[ + f"{asset_dir}/{container.get('container_name')}", str(data)]) - # EditorAssetLibrary.delete_directory(f"{asset_dir}/animations/") + up.send_request("save_current_level") - # source_path = get_representation_path(representation) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "False"]) - # loaded_assets = self._process(source_path, asset_dir, sequence) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) - # data = { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]), - # "loaded_assets": loaded_assets - # } - # up.imprint( - # "{}/{}".format(asset_dir, container.get('container_name')), data) + if master_level: + up.send_request("load_level", params=[master_level]) + elif prev_level: + up.send_request("load_level", params=[prev_level]) - # EditorLevelLibrary.save_current_level() - # asset_content = EditorAssetLibrary.list_assets( - # asset_dir, recursive=True, include_folder=False) + 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. + """ + root = "/Game/OpenPype" + asset = container.get('asset') + asset_dir = container.get('namespace') + asset_name = container.get('asset_name') + loaded_assets = container.get('loaded_assets') - # 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) - - # 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/OpenPype" - # path = Path(container.get("namespace")) - - # containers = up.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_editor_property('object_path') - - # 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 - # ) + data = get_current_project_settings() + create_sequences = data["unreal"]["level_sequences_for_layouts"] - # if len(asset_content) == 0: - # EditorAssetLibrary.delete_directory(str(path.parent)) + up.send_request( + "remove_layout", + params=[ + root, asset, asset_dir, asset_name, loaded_assets, + "True" if create_sequences else "False"]) diff --git a/openpype/hosts/unreal/plugins/load/load_rig.py b/openpype/hosts/unreal/plugins/load/load_rig.py index 44595cbdf6b..27e9419e89c 100644 --- a/openpype/hosts/unreal/plugins/load/load_rig.py +++ b/openpype/hosts/unreal/plugins/load/load_rig.py @@ -19,6 +19,50 @@ class SkeletalMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" + def _import_fbx_task( + self, filename, destination_path, destination_name, replace): + task_properties = [ + ("filename", up.format_string(filename)), + ("destination_path", up.format_string(destination_path)), + ("destination_name", up.format_string(destination_name)), + ("replace_existing", str(replace)), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("import_as_skeletal", "True"), + ("import_animations", "False"), + ("import_mesh", "True"), + ("import_materials", "False"), + ("import_textures", "False"), + ("skeleton", "None"), + ("create_physics_asset", "False"), + ("mesh_type_to_import", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH") + ] + + options_extra_properties = [ + ( + "skeletal_mesh_import_data", + "import_content_type", + "unreal.FBXImportContentType.FBXICT_ALL" + ), + ( + "skeletal_mesh_import_data", + "normal_import_method", + "unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS" + ) + ] + + up.send_request( + "import_fbx_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -62,47 +106,7 @@ def load(self, context, name, namespace, options): "does_directory_exist", params=[asset_dir]): up.send_request("make_directory", params=[asset_dir]) - task_properties = [ - ("filename", up.format_string(self.fname)), - ("destination_path", up.format_string(asset_dir)), - ("destination_name", up.format_string(asset_name)), - ("replace_existing", "False"), - ("automated", "True"), - ("save", "False") - ] - - options_properties = [ - ("import_as_skeletal", "True"), - ("import_animations", "False"), - ("import_mesh", "True"), - ("import_materials", "False"), - ("import_textures", "False"), - ("skeleton", "None"), - ("create_physics_asset", "False"), - ("mesh_type_to_import", - "unreal.FBXImportType.FBXIT_SKELETAL_MESH") - ] - - options_extra_properties = [ - ( - "skeletal_mesh_import_data", - "import_content_type", - "unreal.FBXImportContentType.FBXICT_ALL" - ), - ( - "skeletal_mesh_import_data", - "normal_import_method", - "unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS" - ) - ] - - up.send_request( - "import_fbx_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) + self._import_fbx_task(self.fname, asset_dir, asset_name, False) # Create Asset Container up.send_request( @@ -131,73 +135,29 @@ def load(self, context, name, namespace, options): return asset_content - # def update(self, container, representation): - # name = container["asset_name"] - # source_path = get_representation_path(representation) - # destination_path = container["namespace"] - - # task = unreal.AssetImportTask() - - # task.set_editor_property('filename', source_path) - # task.set_editor_property('destination_path', destination_path) - # task.set_editor_property('destination_name', name) - # task.set_editor_property('replace_existing', True) - # task.set_editor_property('automated', True) - # task.set_editor_property('save', True) - - # # set import options here - # options = unreal.FbxImportUI() - # 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', True) - # options.set_editor_property('import_textures', True) - # 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 - # ) - # # set to import normals, otherwise Unreal will compute them - # # and it will take a long time, depending on the size of the mesh - # options.skeletal_mesh_import_data.set_editor_property( - # 'normal_import_method', - # unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS - # ) - - # task.options = options - # # do import fbx and replace existing data - # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - # container_path = "{}/{}".format(container["namespace"], - # container["objectName"]) - # # update metadata - # unreal_pipeline.imprint( - # container_path, - # { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]) - # }) - - # 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) + def update(self, container, representation): + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] + container_name = container['objectName'] + + self._import_fbx_task(filename, asset_dir, asset_name, True) + + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) + + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) + + up.send_request( + "save_listed_assets", params=[str(asset_content)]) + + def remove(self, container): + path = container["namespace"] + + up.send_request( + "remove_asset", params=[path]) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index e535f700425..ed6fabef5e7 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -19,6 +19,35 @@ class StaticMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" + def _import_fbx_task( + self, filename, destination_path, destination_name, replace): + task_properties = [ + ("filename", up.format_string(filename)), + ("destination_path", up.format_string(destination_path)), + ("destination_name", up.format_string(destination_name)), + ("replace_existing", str(replace)), + ("automated", "True"), + ("save", "True") + ] + + options_properties = [ + ("automated_import_should_detect_type", "False"), + ("import_animations", "False") + ] + + options_extra_properties = [ + ("static_mesh_import_data", "combine_meshes", "True"), + ("static_mesh_import_data", "remove_degenerates", "False") + ] + + up.send_request( + "import_fbx_task", + params=[ + str(task_properties), + str(options_properties), + str(options_extra_properties) + ]) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -62,32 +91,7 @@ def load(self, context, name, namespace, options): "does_directory_exist", params=[asset_dir]): up.send_request("make_directory", params=[asset_dir]) - task_properties = [ - ("filename", up.format_string(self.fname)), - ("destination_path", up.format_string(asset_dir)), - ("destination_name", up.format_string(asset_name)), - ("replace_existing", "False"), - ("automated", "True"), - ("save", "True") - ] - - options_properties = [ - ("automated_import_should_detect_type", "False"), - ("import_animations", "False") - ] - - options_extra_properties = [ - ("static_mesh_import_data", "combine_meshes", "True"), - ("static_mesh_import_data", "remove_degenerates", "False") - ] - - up.send_request( - "import_fbx_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) + self._import_fbx_task(self.fname, asset_dir, asset_name, False) # Create Asset Container up.send_request( @@ -116,42 +120,29 @@ def load(self, context, name, namespace, options): return asset_content - # def update(self, container, representation): - # name = container["asset_name"] - # source_path = get_representation_path(representation) - # destination_path = container["namespace"] + def update(self, container, representation): + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] + container_name = container['objectName'] - # task = self.get_task(source_path, destination_path, name, True) + self._import_fbx_task(filename, asset_dir, asset_name, True) - # # do import fbx and replace existing data - # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # container_path = "{}/{}".format(container["namespace"], - # container["objectName"]) - # # update metadata - # up.imprint( - # container_path, - # { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]) - # }) - - # asset_content = unreal.EditorAssetLibrary.list_assets( - # destination_path, recursive=True, include_folder=True - # ) - - # for a in asset_content: - # unreal.EditorAssetLibrary.save_asset(a) + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + up.send_request( + "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - # def remove(self, container): - # path = container["namespace"] - # parent_path = os.path.dirname(path) + asset_content = up.send_request_literal( + "list_assets", params=[asset_dir, "True", "True"]) - # unreal.EditorAssetLibrary.delete_directory(path) + up.send_request( + "save_listed_assets", params=[str(asset_content)]) - # asset_content = unreal.EditorAssetLibrary.list_assets( - # parent_path, recursive=False - # ) + def remove(self, container): + path = container["namespace"] - # if len(asset_content) == 0: - # unreal.EditorAssetLibrary.delete_directory(parent_path) + up.send_request( + "remove_asset", params=[path]) From 34f2fb9d8c879645bb8efcf3cb737fdd72e0524a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Mar 2023 10:42:50 +0000 Subject: [PATCH 33/55] Preparation for merge --- .../integration/UE_4.7/{ => OpenPype}/.gitignore | 0 .../{ => OpenPype}/Content/Python/init_unreal.py | 0 .../UE_4.7/{ => OpenPype}/OpenPype.uplugin | 0 .../integration/UE_4.7/{ => OpenPype}/README.md | 0 .../UE_4.7/{ => OpenPype}/Resources/openpype128.png | Bin .../UE_4.7/{ => OpenPype}/Resources/openpype40.png | Bin .../UE_4.7/{ => OpenPype}/Resources/openpype512.png | Bin .../Source/OpenPype/OpenPype.Build.cs | 0 .../Source/OpenPype/Private/AssetContainer.cpp | 0 .../OpenPype/Private/AssetContainerFactory.cpp | 0 .../Source/OpenPype/Private/OpenPype.cpp | 0 .../Source/OpenPype/Private/OpenPypeLib.cpp | 0 .../OpenPype/Private/OpenPypePublishInstance.cpp | 0 .../Private/OpenPypePublishInstanceFactory.cpp | 0 .../OpenPype/Private/OpenPypePythonBridge.cpp | 0 .../Source/OpenPype/Private/OpenPypeStyle.cpp | 0 .../Source/OpenPype/Public/AssetContainer.h | 0 .../Source/OpenPype/Public/AssetContainerFactory.h | 0 .../Source/OpenPype/Public/OpenPype.h | 0 .../Source/OpenPype/Public/OpenPypeLib.h | 0 .../OpenPype/Public/OpenPypePublishInstance.h | 0 .../Public/OpenPypePublishInstanceFactory.h | 0 .../Source/OpenPype/Public/OpenPypePythonBridge.h | 0 .../Source/OpenPype/Public/OpenPypeStyle.h | 0 .../integration/UE_5.0/{ => OpenPype}/.gitignore | 0 .../{ => OpenPype}/Content/Python/__init__.py | 0 .../{ => OpenPype}/Content/Python/init_unreal.py | 0 .../{ => OpenPype}/Content/Python/pipeline.py | 0 .../Content/Python/plugins/__init__.py | 0 .../{ => OpenPype}/Content/Python/plugins/load.py | 0 .../UE_5.0/{ => OpenPype}/OpenPype.uplugin | 0 .../integration/UE_5.0/{ => OpenPype}/README.md | 0 .../UE_5.0/{ => OpenPype}/Resources/openpype128.png | Bin .../UE_5.0/{ => OpenPype}/Resources/openpype40.png | Bin .../UE_5.0/{ => OpenPype}/Resources/openpype512.png | Bin .../Source/OpenPype/OpenPype.Build.cs | 0 .../Source/OpenPype/Private/AssetContainer.cpp | 0 .../OpenPype/Private/AssetContainerFactory.cpp | 0 .../Source/OpenPype/Private/OpenPype.cpp | 0 .../Source/OpenPype/Private/OpenPypeCommands.cpp | 0 .../OpenPype/Private/OpenPypeCommunication.cpp | 0 .../Source/OpenPype/Private/OpenPypeLib.cpp | 0 .../OpenPype/Private/OpenPypePublishInstance.cpp | 0 .../Private/OpenPypePublishInstanceFactory.cpp | 0 .../Source/OpenPype/Private/OpenPypeStyle.cpp | 0 .../Source/OpenPype/Public/AssetContainer.h | 0 .../Source/OpenPype/Public/AssetContainerFactory.h | 0 .../Source/OpenPype/Public/OpenPype.h | 0 .../Source/OpenPype/Public/OpenPypeCommands.h | 0 .../Source/OpenPype/Public/OpenPypeCommunication.h | 0 .../Source/OpenPype/Public/OpenPypeLib.h | 0 .../OpenPype/Public/OpenPypePublishInstance.h | 0 .../Public/OpenPypePublishInstanceFactory.h | 0 .../Source/OpenPype/Public/OpenPypeStyle.h | 0 54 files changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/.gitignore (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Content/Python/init_unreal.py (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/OpenPype.uplugin (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/README.md (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Resources/openpype128.png (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Resources/openpype40.png (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Resources/openpype512.png (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/OpenPype.Build.cs (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/AssetContainer.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/AssetContainerFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPype.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypeLib.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstance.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypePythonBridge.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypeStyle.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/AssetContainer.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/AssetContainerFactory.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPype.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypeLib.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstance.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypePythonBridge.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypeStyle.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/.gitignore (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Content/Python/__init__.py (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Content/Python/init_unreal.py (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Content/Python/pipeline.py (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Content/Python/plugins/__init__.py (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Content/Python/plugins/load.py (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/OpenPype.uplugin (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/README.md (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Resources/openpype128.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Resources/openpype40.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Resources/openpype512.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/OpenPype.Build.cs (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/AssetContainer.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/AssetContainerFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPype.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeCommands.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeCommunication.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeLib.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstance.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeStyle.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/AssetContainer.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/AssetContainerFactory.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPype.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeCommands.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeCommunication.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeLib.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstance.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeStyle.h (100%) diff --git a/openpype/hosts/unreal/integration/UE_4.7/.gitignore b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/.gitignore rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin diff --git a/openpype/hosts/unreal/integration/UE_4.7/README.md b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/README.md rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md diff --git a/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Resources/openpype128.png rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype40.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Resources/openpype40.png rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype40.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Resources/openpype512.png rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeLib.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeLib.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePythonBridge.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/.gitignore rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/__init__.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/__init__.py diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Content/Python/pipeline.py rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/__init__.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/__init__.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/__init__.py rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/__init__.py diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Content/Python/plugins/load.py rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin diff --git a/openpype/hosts/unreal/integration/UE_5.0/README.md b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/README.md rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Resources/openpype128.png rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype40.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Resources/openpype40.png rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype40.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Resources/openpype512.png rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommunication.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommunication.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommunication.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommunication.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeStyle.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h From 717cca5003a738e20334f9ab6f1e5d0ea194022e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Mar 2023 14:50:51 +0000 Subject: [PATCH 34/55] Fixed Unreal not starting after merge --- openpype/hosts/unreal/api/pipeline.py | 43 +++++++---- openpype/hosts/unreal/api/plugin.py | 57 ++++++--------- .../OpenPype/Content/Python/functions.py | 56 +++++++++++++++ .../OpenPype/Content/Python/init_unreal.py | 25 +++++-- .../OpenPype/Content/Python/pipeline.py | 72 ++++++++++++++++++- .../OpenPype/Content/Python/plugins/create.py | 28 ++++++++ .../OpenPype/Content/Python/plugins/load.py | 32 +-------- 7 files changed, 223 insertions(+), 90 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 9e0d4513ac2..0448c8c0079 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -24,7 +24,6 @@ CommunicationWrapper ) - logger = logging.getLogger("openpype.hosts.unreal") OPENPYPE_CONTAINERS = "OpenPypeContainers" @@ -56,18 +55,8 @@ def install(self): def get_containers(self): return ls() - def show_tools_popup(self): - """Show tools popup with actions leading to show other tools.""" - - show_tools_popup() - - def show_tools_dialog(self): - """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() + content_path = send_request("project_content_dir") op_ctx = content_path + CONTEXT_CONTAINER attempts = 3 for i in range(attempts): @@ -78,13 +67,17 @@ def update_context_data(self, data, changes): except IOError: if i == attempts - 1: raise Exception("Failed to write context data. Aborting.") - unreal.log_warning("Failed to write context data. Retrying...") + send_request( + "log", + params=[ + "Failed to write context data. Retrying..." + "warning"]) i += 1 time.sleep(3) continue def get_context_data(self): - content_path = unreal.Paths.project_content_dir() + content_path = send_request("project_content_dir") op_ctx = content_path + CONTEXT_CONTAINER if not os.path.isfile(op_ctx): return {} @@ -172,6 +165,16 @@ def ls(): return send_request_literal("ls") +def ls_inst(): + """List all containers. + + List all found in *Content Manager* of Unreal and return + metadata from them. Adding `objectName` to set. + + """ + return send_request_literal("ls_inst") + + def publish(): """Shorthand to publish from within host.""" import pyblish.util @@ -241,3 +244,15 @@ def show_manager(): def show_experimental_tools(): host_tools.show_experimental_tools_dialog() + + +@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 diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index d60050a6964..5c7c57a2fe3 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -8,11 +8,7 @@ ABCMeta, ) -import unreal - from .pipeline import ( - create_publish_instance, - imprint, ls_inst, UNREAL_VERSION ) @@ -26,6 +22,7 @@ CreatorError, CreatedInstance ) +from openpype.hosts.unreal.api import pipeline as up @six.add_metaclass(ABCMeta) @@ -73,7 +70,6 @@ def cache_subsets(shared_data): def create(self, subset_name, instance_data, pre_create_data): try: instance_name = f"{subset_name}{self.suffix}" - pub_instance = create_publish_instance(instance_name, self.root) instance_data["subset"] = subset_name instance_data["instance_path"] = f"{self.root}/{instance_name}" @@ -85,16 +81,11 @@ def create(self, subset_name, instance_data, pre_create_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()) + up.send_request( + "new_publish_instance", + params=[ + instance_name, self.root, instance.data_to_store(), + pre_create_data.get("members", [])]) return instance @@ -122,24 +113,24 @@ def update_instances(self, update_list): instance_node = created_inst.get("instance_path", "") if not instance_node: - unreal.log_warning( - f"Instance node not found for {created_inst}") + up.send_request( + "log", + params=[ + f"Instance node not found for {created_inst}", + "warning"]) continue new_values = { key: changes[key].new_value for key in changes.changed_keys } - imprint( - instance_node, - new_values - ) + up.send_request("imprint", params=[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) + up.send_request("delete_asset", params=[instance_node]) self._remove_instance_from_context(instance) @@ -166,10 +157,8 @@ def create(self, subset_name, instance_data, pre_create_data): 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] + pre_create_data["members"] = up.send_request( + "get_selected_assets") super(UnrealAssetCreator, self).create( subset_name, @@ -204,26 +193,20 @@ def create(self, subset_name, instance_data, pre_create_data): CreatedInstance: Created instance. """ try: - if UNREAL_VERSION.major == 5: - world = unreal.UnrealEditorSubsystem().get_editor_world() - else: - world = unreal.EditorLevelLibrary.get_editor_world() + world = up.send_request("get_editor_world") # Check if the level is saved - if world.get_path_name().startswith("/Temp/"): + if world.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["members"] = up.send_request( + "get_selected_actors") - instance_data["level"] = world.get_path_name() + instance_data["level"] = world super(UnrealActorCreator, self).create( subset_name, diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py new file mode 100644 index 00000000000..03045aee82d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py @@ -0,0 +1,56 @@ +from pipeline import UNREAL_VERSION + +import unreal + + +def delete_asset(asset_path): + unreal.EditorAssetLibrary.delete_asset(asset_path) + + +def does_asset_exist(asset_path): + return unreal.EditorAssetLibrary.does_asset_exist(asset_path) + + +def does_directory_exist(directory_path): + return unreal.EditorAssetLibrary.does_directory_exist(directory_path) + + +def make_directory(directory_path): + unreal.EditorAssetLibrary.make_directory(directory_path) + + +def new_level(level_path): + unreal.EditorLevelLibrary.new_level(level_path) + + +def load_level(level_path): + unreal.EditorLevelLibrary.load_level(level_path) + + +def save_current_level(): + unreal.EditorLevelLibrary.save_current_level() + + +def save_all_dirty_levels(): + unreal.EditorLevelLibrary.save_all_dirty_levels() + + +def get_editor_world(): + world = None + if UNREAL_VERSION.major == 5: + world = unreal.UnrealEditorSubsystem().get_editor_world() + else: + world = unreal.EditorLevelLibrary.get_editor_world() + return world.get_path_name() + + +def get_selected_assets(): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + + return [obj.get_path_name() for obj in sel_objects] + + +def get_selected_actors(): + sel_actors = unreal.EditorUtilityLibrary.get_selected_level_actors() + + return [actor.get_path_name() for actor in sel_actors] diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py index 6d59ea93711..9780735688b 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py @@ -1,20 +1,31 @@ +from functions import ( + delete_asset, + does_asset_exist, + does_directory_exist, + make_directory, + new_level, + load_level, + save_current_level, + save_all_dirty_levels, + get_selected_assets, +) + from pipeline import ( + log, ls, containerise, instantiate, + project_content_dir, create_container, imprint, ) +from plugins.create import ( + new_publish_instance, +) + from plugins.load import ( create_unique_asset_name, - does_asset_exist, - does_directory_exist, - make_directory, - new_level, - load_level, - save_current_level, - save_all_dirty_levels, add_level_to_world, list_assets, get_assets_of_class, diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py index cccc79da204..931355d1255 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py @@ -1,8 +1,30 @@ +import os import ast +import semver from typing import List import unreal +UNREAL_VERSION = semver.VersionInfo( + *os.getenv("OPENPYPE_UNREAL_VERSION").split(".") +) + + +def log(message: str, level: str = "info"): + """Log message to Unreal Editor. + + Args: + message (str): Message to log. + level (str): Log level. Defaults to "info". + + """ + if level == "info": + unreal.log(message) + elif level == "warning": + unreal.log_warning(message) + elif level == "error": + unreal.log_error(message) + def cast_map_to_str_dict(umap) -> dict: """Cast Unreal Map to dict. @@ -123,6 +145,16 @@ def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: return name +def project_content_dir(): + """Get project content directory. + + Returns: + str: path to project content directory + + """ + return unreal.Paths.project_content_dir() + + def create_container(container: str, path: str) -> unreal.Object: """Helper function to create Asset Container class on given path. @@ -208,7 +240,14 @@ def ls(): """ ar = unreal.AssetRegistryHelpers.get_asset_registry() - openpype_containers = ar.get_assets_by_class("AssetContainer", True) + class_name = [ + "/Script/OpenPype", + "AssetContainer" + ] if ( + UNREAL_VERSION.major == 5 + and UNREAL_VERSION.minor > 0 + ) else "AssetContainer" # noqa + openpype_containers = ar.get_assets_by_class(class_name, True) containers = [] @@ -227,8 +266,37 @@ def ls(): return containers +def ls_inst(): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + # UE 5.1 changed how class name is specified + class_name = [ + "/Script/OpenPype", + "OpenPypePublishInstance" + ] if ( + UNREAL_VERSION.major == 5 + and UNREAL_VERSION.minor > 0 + ) else "OpenPypePublishInstance" # noqa + instances = ar.get_assets_by_class(class_name, True) + + containers = [] + + # 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 + data = cast_map_to_str_dict(data) + + containers.append(data) + + return containers + + def containerise( - name, namespc, str_nodes, str_context, loader="", suffix="_CON" + name, namespc, str_nodes, str_context, loader="", suffix="_CON" ): """Bundles *nodes* (assets) into a *container* and add metadata to it. diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py new file mode 100644 index 00000000000..6b50205c980 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py @@ -0,0 +1,28 @@ +import ast + +import unreal + +from pipeline import ( + create_publish_instance, + imprint, +) + + +def new_publish_instance( + instance_name, path, str_instance_data, str_members +): + instance_data = ast.literal_eval(str_instance_data) + members = ast.literal_eval(str_members) + + pub_instance = create_publish_instance(instance_name, path) + + 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 members: + obj = ar.get_asset_by_object_path(member).get_asset() + assets.add(obj) + + imprint(f"{path}/{instance_name}", instance_data) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py index 41fc95846b9..7ef3253ad1f 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py @@ -18,34 +18,6 @@ def create_unique_asset_name(root, asset, name, version="", suffix=""): f"{root}/{asset}/{subset}", suffix) -def does_asset_exist(asset_path): - return unreal.EditorAssetLibrary.does_asset_exist(asset_path) - - -def does_directory_exist(directory_path): - return unreal.EditorAssetLibrary.does_directory_exist(directory_path) - - -def make_directory(directory_path): - unreal.EditorAssetLibrary.make_directory(directory_path) - - -def new_level(level_path): - unreal.EditorLevelLibrary.new_level(level_path) - - -def load_level(level_path): - unreal.EditorLevelLibrary.load_level(level_path) - - -def save_current_level(): - unreal.EditorLevelLibrary.save_current_level() - - -def save_all_dirty_levels(): - unreal.EditorLevelLibrary.save_all_dirty_levels() - - def get_current_level(): curr_level = unreal.LevelEditorSubsystem().get_current_level() curr_level_path = curr_level.get_outer().get_path_name() @@ -730,7 +702,6 @@ def remove_layout( for i, section in enumerate(sections): section.set_row_index(i) - if visibility_track: sections = visibility_track.get_sections() for section in sections: @@ -758,7 +729,8 @@ def remove_layout( unreal.EditorLevelLibrary.save_all_dirty_levels() unreal.EditorAssetLibrary.make_directory(f"{root}/tmp") tmp_level = f"{root}/tmp/temp_map" - if not unreal.EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + if not unreal.EditorAssetLibrary.does_asset_exist( + f"{tmp_level}.temp_map"): unreal.EditorLevelLibrary.new_level(tmp_level) else: unreal.EditorLevelLibrary.load_level(tmp_level) From f15d6ea368fbd692ca8eb1d0edee224354dc9466 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Mar 2023 15:19:28 +0000 Subject: [PATCH 35/55] Changed menu actions to open new publisher --- openpype/hosts/unreal/api/communication_server.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/api/communication_server.py b/openpype/hosts/unreal/api/communication_server.py index b452b5c1f7b..3764b236b32 100644 --- a/openpype/hosts/unreal/api/communication_server.py +++ b/openpype/hosts/unreal/api/communication_server.py @@ -328,7 +328,8 @@ async def loader_tool(self): async def creator_tool(self): log.info("Triggering Creator tool") - item = MainThreadItem(self.tools_helper.show_creator) + item = MainThreadItem( + self.tools_helper.show_publisher_tool, tab="create") await self._async_execute_in_main_thread(item, wait=False) async def subset_manager_tool(self): @@ -340,7 +341,8 @@ async def subset_manager_tool(self): async def publish_tool(self): log.info("Triggering Publish tool") - item = MainThreadItem(self.tools_helper.show_publish) + item = MainThreadItem( + self.tools_helper.show_publisher_tool, tab="publish") self._execute_in_main_thread(item) return From de22683a4ad068602c30b6b035367e7e8676bd8b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 17 Mar 2023 14:35:39 +0000 Subject: [PATCH 36/55] Fix problem with context file creation --- openpype/hosts/unreal/api/pipeline.py | 18 +++++++++++++----- .../OpenPype/Content/Python/functions.py | 8 ++++++++ .../OpenPype/Content/Python/init_unreal.py | 4 ++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0448c8c0079..b35e17b9723 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -56,7 +56,14 @@ def get_containers(self): return ls() def update_context_data(self, data, changes): - content_path = send_request("project_content_dir") + content_path = send_request_literal("project_content_dir") + + # The context json will be stored in the OpenPype folder, so we need + # to create it if it doesn't exist. + if not send_request_literal( + "does_directory_exist", params=["/Game/OpenPype"]): + send_request("make_directory", params=["/Game/OpenPype"]) + op_ctx = content_path + CONTEXT_CONTAINER attempts = 3 for i in range(attempts): @@ -64,13 +71,14 @@ def update_context_data(self, data, changes): with open(op_ctx, "w+") as f: json.dump(data, f) break - except IOError: + except IOError as e: if i == attempts - 1: - raise Exception("Failed to write context data. Aborting.") + raise IOError( + "Failed to write context data. Aborting.") from e send_request( "log", params=[ - "Failed to write context data. Retrying..." + "Failed to write context data. Retrying...", "warning"]) i += 1 time.sleep(3) @@ -137,7 +145,7 @@ def format_string(input): string = input.replace('\\', '/') string = string.replace('"', '\\"') string = string.replace("'", "\\'") - return '"' + string + '"' + return f'"{string}"' def send_request(request, params=None): diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py index 03045aee82d..5b0c691d5b9 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py @@ -54,3 +54,11 @@ def get_selected_actors(): sel_actors = unreal.EditorUtilityLibrary.get_selected_level_actors() return [actor.get_path_name() for actor in sel_actors] + + +def get_system_path(asset_path): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset = ar.get_asset_by_object_path(asset_path).get_asset() + + return unreal.SystemLibrary.get_system_path(asset) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py index 9780735688b..3fa7cab9cac 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py @@ -7,12 +7,16 @@ load_level, save_current_level, save_all_dirty_levels, + get_editor_world, get_selected_assets, + get_selected_actors, + get_system_path, ) from pipeline import ( log, ls, + ls_inst, containerise, instantiate, project_content_dir, From e9342a6134bc3d92abb274fca8fcdf1edd186131 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 17 Mar 2023 14:37:17 +0000 Subject: [PATCH 37/55] Updated camera, look and uasset creators --- .../OpenPype/Content/Python/init_unreal.py | 1 + .../OpenPype/Content/Python/plugins/create.py | 32 ++++++++++++++ .../unreal/plugins/create/create_camera.py | 14 ++----- .../unreal/plugins/create/create_look.py | 42 ++++--------------- .../unreal/plugins/create/create_uasset.py | 11 ++--- 5 files changed, 46 insertions(+), 54 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py index 3fa7cab9cac..c5e6dcef584 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py @@ -26,6 +26,7 @@ from plugins.create import ( new_publish_instance, + create_look, ) from plugins.load import ( diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py index 6b50205c980..4a07a58aee9 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py @@ -26,3 +26,35 @@ def new_publish_instance( assets.add(obj) imprint(f"{path}/{instance_name}", instance_data) + + +def create_look(selected_asset, 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') + + 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')) + + members.append(object_path) + + unreal.EditorAssetLibrary.save_asset(object_path) + + return members diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 642924e2d69..927070d0180 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- -import unreal - from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION +from openpype.hosts.unreal.api import pipeline as up from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) @@ -18,19 +16,13 @@ class CreateCamera(UnrealAssetCreator): def create(self, subset_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] + selection = up.send_request("get_selected_assets") 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() + instance_data["level"] = up.send_request("get_editor_world") super(CreateCamera, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index f6c73e47e67..063f15f05b2 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- -import unreal - from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api.pipeline import ( - create_folder -) +from openpype.hosts.unreal.api import pipeline as up from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) @@ -22,8 +18,7 @@ class CreateLook(UnrealAssetCreator): def create(self, subset_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] + selection = up.send_request("get_selected_assets") if len(selection) != 1: raise CreatorError("Please select only one asset.") @@ -33,38 +28,15 @@ def create(self, subset_name, instance_data, pre_create_data): look_directory = "/Game/OpenPype/Looks" # Create the folder - folder_name = create_folder(look_directory, subset_name) + folder_name = up.send_request( + "create_folder", params=[look_directory, subset_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) + pre_create_data["members"] = up.send_request( + "create_look", params=[selected_asset, path] + ) super(CreateLook, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index 70f17d478b0..1bb27d63e48 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- from pathlib import Path -import unreal - from openpype.pipeline import CreatorError +from openpype.hosts.unreal.api import pipeline as up from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) @@ -19,18 +18,14 @@ class CreateUAsset(UnrealAssetCreator): def create(self, subset_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] + selection = up.send_request("get_selected_assets") 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) + sys_path = up.send_request("get_system_path", params=[obj]) if not sys_path: raise CreatorError( From 03c9685f34f139b0597cfa7bb408416397b0a858 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 30 Mar 2023 04:41:01 +0100 Subject: [PATCH 38/55] Complete overhaul of the communication between OpenPype and Unreal --- openpype/hosts/unreal/api/__init__.py | 4 + .../hosts/unreal/api/communication_server.py | 2 +- openpype/hosts/unreal/api/pipeline.py | 84 +- openpype/hosts/unreal/api/plugin.py | 76 +- .../OpenPype/Content/Python/functions.py | 98 +- .../UE_5.0/OpenPype/Content/Python/helpers.py | 68 + .../OpenPype/Content/Python/init_unreal.py | 70 +- .../OpenPype/Content/Python/pipeline.py | 307 ++--- .../UE_5.0/OpenPype/Content/Python/plugin.py | 1096 +++++++++++++++++ .../Content/Python/plugins/__init__.py | 0 .../OpenPype/Content/Python/plugins/create.py | 60 - .../OpenPype/Content/Python/plugins/load.py | 746 ----------- .../OpenPype/Content/Python}/rendering.py | 60 +- .../Private/OpenPypeCommunication.cpp | 69 +- .../unreal/plugins/create/create_camera.py | 8 +- .../unreal/plugins/create/create_look.py | 17 +- .../unreal/plugins/create/create_uasset.py | 9 +- .../plugins/load/load_alembic_animation.py | 166 +-- .../unreal/plugins/load/load_animation.py | 314 ++--- .../hosts/unreal/plugins/load/load_camera.py | 364 +++--- .../plugins/load/load_geometrycache_abc.py | 164 +-- .../hosts/unreal/plugins/load/load_layout.py | 881 +++++++------ .../plugins/load/load_skeletalmesh_abc.py | 176 ++- .../plugins/load/load_skeletalmesh_fbx.py | 158 +-- .../plugins/load/load_staticmesh_abc.py | 187 ++- .../plugins/load/load_staticmesh_fbx.py | 129 +- .../hosts/unreal/plugins/load/load_uasset.py | 119 +- 27 files changed, 2817 insertions(+), 2615 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/helpers.py create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/__init__.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py rename openpype/hosts/unreal/{api => integration/UE_5.0/OpenPype/Content/Python}/rendering.py (72%) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index 2d1256a76a7..df01de1cfb7 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -10,7 +10,9 @@ from .pipeline import ( install, uninstall, + imprint, ls, + ls_inst, publish, containerise, show_creator, @@ -27,7 +29,9 @@ "install", "uninstall", "Loader", + "imprint", "ls", + "ls_inst", "publish", "containerise", "show_creator", diff --git a/openpype/hosts/unreal/api/communication_server.py b/openpype/hosts/unreal/api/communication_server.py index 3764b236b32..8b3db21b24f 100644 --- a/openpype/hosts/unreal/api/communication_server.py +++ b/openpype/hosts/unreal/api/communication_server.py @@ -240,7 +240,7 @@ def send_notification(self, client, method, params=None): def send_request(self, client, method, params=None, timeout=0): if params is None: - params = [] + params = {} client_host = client.host diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index b35e17b9723..9f29b3518af 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -3,7 +3,6 @@ import os import json import logging -from typing import List from contextlib import contextmanager import semver import time @@ -15,7 +14,6 @@ register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, - AVALON_CONTAINER_ID, ) from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -56,14 +54,22 @@ def get_containers(self): return ls() def update_context_data(self, data, changes): - content_path = send_request_literal("project_content_dir") + content_path = send_request("project_content_dir") + + unreal_log("Updating context data...", "info") # The context json will be stored in the OpenPype folder, so we need # to create it if it doesn't exist. - if not send_request_literal( - "does_directory_exist", params=["/Game/OpenPype"]): - send_request("make_directory", params=["/Game/OpenPype"]) + dir_exists = send_request( + "does_directory_exist", + params={"directory_path": "/Game/OpenPype"}) + + if not dir_exists: + send_request( + "make_directory", + params={"directory_path": "/Game/OpenPype"}) + # op_ctx = content_path + CONTEXT_CONTAINER attempts = 3 for i in range(attempts): @@ -75,11 +81,8 @@ def update_context_data(self, data, changes): if i == attempts - 1: raise IOError( "Failed to write context data. Aborting.") from e - send_request( - "log", - params=[ - "Failed to write context data. Retrying...", - "warning"]) + unreal_log( + "Failed to write context data. Retrying...", "warning") i += 1 time.sleep(3) continue @@ -141,26 +144,41 @@ def _register_events(): pass -def format_string(input): - string = input.replace('\\', '/') +def format_string(input_str): + string = input_str.replace('\\', '/') string = string.replace('"', '\\"') string = string.replace("'", "\\'") return f'"{string}"' -def send_request(request, params=None): +def send_request(request: str, params: dict = None): communicator = CommunicationWrapper.communicator - formatted_params = [] - if params: - for p in params: - if isinstance(p, str): - p = format_string(p) - formatted_params.append(p) - return communicator.send_request(request, formatted_params) + if ret_value := ast.literal_eval( + communicator.send_request(request, params) + ): + return ret_value.get("return") + return None + + +def unreal_log(message, level): + """Log message to Unreal. + + Args: + message (str): message to log + level (str): level of message + + """ + send_request("log", params={"message": message, "level": level}) -def send_request_literal(request, params=None): - return ast.literal_eval(send_request(request, params)) +def imprint(node, data): + """Imprint data to container. + + Args: + node (str): path to container + data (dict): data to imprint + """ + send_request("imprint", params={"node": node, "data": data}) def ls(): @@ -170,7 +188,7 @@ def ls(): metadata from them. Adding `objectName` to set. """ - return send_request_literal("ls") + return send_request("ls") def ls_inst(): @@ -180,7 +198,7 @@ def ls_inst(): metadata from them. Adding `objectName` to set. """ - return send_request_literal("ls_inst") + return send_request("ls_inst") def publish(): @@ -190,7 +208,7 @@ def publish(): return pyblish.util.publish() -def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): +def containerise(root, name, data, 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. @@ -209,7 +227,12 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): """ return send_request( - "containerise", [name, namespace, nodes, context, loader, suffix]) + "containerise", + params={ + "root": root, + "name": name, + "data": data, + "suffix": suffix}) def instantiate(root, name, data, assets=None, suffix="_INS"): @@ -231,7 +254,12 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): """ return send_request( - "instantiate", params=[root, name, data, assets, suffix]) + "instantiate", params={ + "root": root, + "name": name, + "data": data, + "assets": assets, + "suffix": suffix}) def show_creator(): diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 5c7c57a2fe3..0c69b170804 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -3,15 +3,8 @@ import collections import sys import six -from abc import ( - ABC, - ABCMeta, -) +from abc import ABCMeta -from .pipeline import ( - ls_inst, - UNREAL_VERSION -) from openpype.lib import ( BoolDef, UILabelDef @@ -22,7 +15,14 @@ CreatorError, CreatedInstance ) -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.pipeline import ( + send_request, + unreal_log, + ls_inst, + imprint, + containerise, + instantiate +) @six.add_metaclass(ABCMeta) @@ -54,8 +54,7 @@ def cache_subsets(shared_data): unreal_cached_subsets = collections.defaultdict(list) unreal_cached_legacy_subsets = collections.defaultdict(list) for instance in ls_inst(): - creator_id = instance.get("creator_identifier") - if creator_id: + if creator_id := instance.get("creator_identifier"): unreal_cached_subsets[creator_id].append(instance) else: family = instance.get("family") @@ -81,11 +80,11 @@ def create(self, subset_name, instance_data, pre_create_data): self) self._add_instance_to_context(instance) - up.send_request( - "new_publish_instance", - params=[ - instance_name, self.root, instance.data_to_store(), - pre_create_data.get("members", [])]) + instantiate( + self.root, + instance_name, + instance.data_to_store(), + pre_create_data.get("members", [])) return instance @@ -113,24 +112,21 @@ def update_instances(self, update_list): instance_node = created_inst.get("instance_path", "") if not instance_node: - up.send_request( - "log", - params=[ - f"Instance node not found for {created_inst}", - "warning"]) + message = f"Instance node not found for {created_inst}" + unreal_log(message, "warning") continue new_values = { key: changes[key].new_value for key in changes.changed_keys } - up.send_request("imprint", params=[instance_node, new_values]) + imprint(instance_node, new_values) def remove_instances(self, instances): for instance in instances: - instance_node = instance.data.get("instance_path", "") - if instance_node: - up.send_request("delete_asset", params=[instance_node]) + if instance_node := instance.data.get("instance_path", ""): + send_request( + "delete_asset", params={"asset_path": instance_node}) self._remove_instance_from_context(instance) @@ -157,7 +153,7 @@ def create(self, subset_name, instance_data, pre_create_data): pre_create_data["members"] = [] if pre_create_data.get("use_selection"): - pre_create_data["members"] = up.send_request( + pre_create_data["members"] = send_request( "get_selected_assets") super(UnrealAssetCreator, self).create( @@ -193,7 +189,7 @@ def create(self, subset_name, instance_data, pre_create_data): CreatedInstance: Created instance. """ try: - world = up.send_request("get_editor_world") + world = send_request("get_editor_world") # Check if the level is saved if world.startswith("/Temp/"): @@ -203,7 +199,7 @@ def create(self, subset_name, instance_data, pre_create_data): # Check if instance data has members, filled by the plugin. # If not, use selection. if not instance_data.get("members"): - instance_data["members"] = up.send_request( + instance_data["members"] = send_request( "get_selected_actors") instance_data["level"] = world @@ -225,6 +221,24 @@ def get_pre_create_attr_defs(self): ] -class Loader(LoaderPlugin, ABC): - """This serves as skeleton for future OpenPype specific functionality""" - pass +@six.add_metaclass(ABCMeta) +class UnrealBaseLoader(LoaderPlugin): + """Base class for Unreal loader plugins.""" + root = "/Game/OpenPype" + suffix = "_CON" + + def update(self, container, representation): + asset_dir = container["namespace"] + container_name = container['objectName'] + + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + + containerise(asset_dir, container_name, data) + + def remove(self, container): + path = container["namespace"] + + send_request("remove_asset", params={"path": path}) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py index 5b0c691d5b9..6554e210629 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/functions.py @@ -1,29 +1,93 @@ +import unreal + +from helpers import get_params from pipeline import UNREAL_VERSION -import unreal + +def log(params): + """Log message to Unreal Editor. + + Args: + params (str): string containing a dictionary with parameters: + message (str): message to log + level (str): log level, can be "info", "warning" or "error" + """ + message, level = get_params(params, "message", "level") + + if level == "info": + unreal.log(message) + elif level == "warning": + unreal.log_warning(message) + elif level == "error": + unreal.log_error(message) + else: + raise ValueError(f"Unknown log level: {level}") -def delete_asset(asset_path): +def delete_asset(params): + """ + Args: + params (str): string containing a dictionary with parameters: + asset_path (str): path to asset to delete + """ + asset_path = get_params(params, 'asset_path') + unreal.EditorAssetLibrary.delete_asset(asset_path) -def does_asset_exist(asset_path): - return unreal.EditorAssetLibrary.does_asset_exist(asset_path) +def does_asset_exist(params): + """ + Args: + params (str): string containing a dictionary with parameters: + asset_path (str): path to asset to check + """ + asset_path = get_params(params, 'asset_path') + + return {"return": unreal.EditorAssetLibrary.does_asset_exist(asset_path)} + + +def does_directory_exist(params): + """ + Args: + params (str): string containing a dictionary with parameters: + directory_path (str): path to directory to check + """ + directory_path = get_params(params, 'directory_path') + return {"return": unreal.EditorAssetLibrary.does_directory_exist( + directory_path)} -def does_directory_exist(directory_path): - return unreal.EditorAssetLibrary.does_directory_exist(directory_path) +def make_directory(params): + """ + Args: + params (str): string containing a dictionary with parameters: + directory_path (str): path to directory to create + """ + directory_path = get_params(params, 'directory_path') -def make_directory(directory_path): unreal.EditorAssetLibrary.make_directory(directory_path) -def new_level(level_path): +def new_level(params): + """ + Args: + params (str): string containing a dictionary with parameters: + level_path (str): path to level to create + """ + level_path = get_params(params, 'level_path') + unreal.EditorLevelLibrary.new_level(level_path) -def load_level(level_path): +def load_level(params): + """ + Args: + params (str): string containing a dictionary with parameters: + level_path (str): path to level to load + """ + level_path = get_params(params, 'level_path') + unreal.EditorLevelLibrary.load_level(level_path) @@ -36,7 +100,6 @@ def save_all_dirty_levels(): def get_editor_world(): - world = None if UNREAL_VERSION.major == 5: world = unreal.UnrealEditorSubsystem().get_editor_world() else: @@ -47,18 +110,25 @@ def get_editor_world(): def get_selected_assets(): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - return [obj.get_path_name() for obj in sel_objects] + return {"return": [obj.get_path_name() for obj in sel_objects]} def get_selected_actors(): sel_actors = unreal.EditorUtilityLibrary.get_selected_level_actors() - return [actor.get_path_name() for actor in sel_actors] + return {"return": [actor.get_path_name() for actor in sel_actors]} + +def get_system_path(params): + """ + Args: + params (str): string containing a dictionary with parameters: + asset_path (str): path to asset to get system path for + """ + asset_path = get_params(params, 'asset_path') -def get_system_path(asset_path): ar = unreal.AssetRegistryHelpers.get_asset_registry() asset = ar.get_asset_by_object_path(asset_path).get_asset() - return unreal.SystemLibrary.get_system_path(asset) + return {"return": unreal.SystemLibrary.get_system_path(asset)} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/helpers.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/helpers.py new file mode 100644 index 00000000000..a8226ed74c4 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/helpers.py @@ -0,0 +1,68 @@ +import ast + +import unreal + + +def get_params(params, *args): + params = ast.literal_eval(params) + if len(args) == 1: + return params.get(args[0]) + else: + return tuple(params.get(arg) for arg in args) + + +def format_string(input_str): + string = input_str.replace('\\', '/') + string = string.replace('"', '\\"') + string = string.replace("'", "\\'") + return f'"{string}"' + + +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_asset(path): + """ + Args: + path (str): path to the asset + """ + ar = unreal.AssetRegistryHelpers.get_asset_registry() + return ar.get_asset_by_object_path(path).get_asset() + + +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 [] diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py index c5e6dcef584..ec643f4cff8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py @@ -1,4 +1,5 @@ from functions import ( + log, delete_asset, does_asset_exist, does_directory_exist, @@ -14,22 +15,20 @@ ) from pipeline import ( - log, + parse_container, + imprint, + create_folder, + project_content_dir, + create_container, + create_publish_instance, ls, ls_inst, containerise, instantiate, - project_content_dir, - create_container, - imprint, ) -from plugins.create import ( - new_publish_instance, +from plugin import ( create_look, -) - -from plugins.load import ( create_unique_asset_name, add_level_to_world, list_assets, @@ -50,8 +49,61 @@ add_animation_to_sequencer, import_camera, get_actor_and_skeleton, + get_skeleton_from_skeletal_mesh, remove_asset, delete_all_bound_assets, remove_camera, remove_layout, ) + +__all__ = [ + "log", + "delete_asset", + "does_asset_exist", + "does_directory_exist", + "make_directory", + "new_level", + "load_level", + "save_current_level", + "save_all_dirty_levels", + "get_editor_world", + "get_selected_assets", + "get_selected_actors", + "get_system_path", + "parse_container", + "imprint", + "create_folder", + "project_content_dir", + "create_container", + "create_publish_instance", + "ls", + "ls_inst", + "containerise", + "instantiate", + "create_look", + "create_unique_asset_name", + "add_level_to_world", + "list_assets", + "get_assets_of_class", + "get_all_assets_of_class", + "get_first_asset_of_class", + "save_listed_assets", + "import_abc_task", + "import_fbx_task", + "get_sequence_frame_range", + "generate_sequence", + "generate_master_sequence", + "set_sequence_hierarchy", + "set_sequence_visibility", + "process_family", + "apply_animation_to_actor", + "apply_animation", + "add_animation_to_sequencer", + "import_camera", + "get_actor_and_skeleton", + "get_skeleton_from_skeletal_mesh", + "remove_asset", + "delete_all_bound_assets", + "remove_camera", + "remove_layout", +] diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py index 931355d1255..a126a63ef8f 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/pipeline.py @@ -1,68 +1,46 @@ import os -import ast import semver -from typing import List import unreal +from helpers import ( + get_params, + cast_map_to_str_dict +) + UNREAL_VERSION = semver.VersionInfo( *os.getenv("OPENPYPE_UNREAL_VERSION").split(".") ) -def log(message: str, level: str = "info"): - """Log message to Unreal Editor. - - Args: - message (str): Message to log. - level (str): Log level. Defaults to "info". - - """ - if level == "info": - unreal.log(message) - elif level == "warning": - unreal.log_warning(message) - elif level == "error": - unreal.log_error(message) - - -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 parse_container(container): +def parse_container(params): """To get data from container, AssetContainer must be loaded. Args: - container(str): path to container - - Returns: - dict: metadata stored on container + params (str): string containing a dictionary with parameters: + container (str): path to container """ + container = get_params(params, "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 + return {"return": data} + + +def imprint(params): + """Imprint data to container. + Args: + params (str): string containing a dictionary with parameters: + node (str): path to container + data (dict): data to imprint + """ + node, data = get_params(params, "node", "data") -def imprint(node, data): loaded_asset = unreal.EditorAssetLibrary.load_asset(node) - data = ast.literal_eval(data) for key, value in data.items(): # Support values evaluated at imprint if callable(value): @@ -78,15 +56,16 @@ def imprint(node, data): unreal.EditorAssetLibrary.save_asset(node) -def create_folder(root: str, name: str) -> str: +def create_folder(params): """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 + params (str): string containing a dictionary with parameters: + root (str): path root + name (str): folder name Returns: str: folder name @@ -98,51 +77,19 @@ def create_folder(root: str, name: str) -> str: /Game/Foo1 """ + root, name = get_params(params, "root", "name") + eal = unreal.EditorAssetLibrary index = 1 while True: - if eal.does_directory_exist("{}/{}".format(root, name)): - name = "{}{}".format(name, index) + if eal.does_directory_exist(f"{root}/{name}"): + name = f"{name}{index}" index += 1 else: - eal.make_directory("{}/{}".format(root, name)) + 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, "{}/{}/{}".format(root, name, loaded.get_name()) - ) - - return name + return {"return": name} def project_content_dir(): @@ -152,19 +99,20 @@ def project_content_dir(): str: path to project content directory """ - return unreal.Paths.project_content_dir() + return {"return": unreal.Paths.project_content_dir()} -def create_container(container: str, path: str) -> unreal.Object: +def create_container(params): """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 + params (str): string containing a dictionary with parameters: + 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 @@ -177,22 +125,24 @@ def create_container(container: str, path: str) -> unreal.Object: ) """ + container, path = get_params(params, "container", "path") + factory = unreal.AssetContainerFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(container, path, None, factory) - return asset + return {"return": tools.create_asset(container, path, None, factory)} -def create_publish_instance(instance: str, path: str) -> unreal.Object: +def create_publish_instance(params): """Helper function to create OpenPype Publish Instance on given path. This behaves similarly as :func:`create_openpype_container`. Args: - path (str): Path where to create Publish Instance. - This path should point into container folder - instance (str): Publish Instance name + params (str): string containing a dictionary with parameters: + 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 @@ -205,31 +155,11 @@ def create_publish_instance(instance: str, path: str) -> unreal.Object: ) """ + instance, path = get_params(params, "instance", "path") + factory = unreal.OpenPypePublishInstanceFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(instance, path, None, factory) - return asset - - -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 = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - break - if subscene_track is not None and subscene_track.get_sections(): - return subscene_track.get_sections() - return [] + return {"return": tools.create_asset(instance, path, None, factory)} def ls(): @@ -263,7 +193,7 @@ def ls(): containers.append(data) - return containers + return {"return": containers} def ls_inst(): @@ -276,104 +206,87 @@ def ls_inst(): UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 ) else "OpenPypePublishInstance" # noqa - instances = ar.get_assets_by_class(class_name, True) + openpype_instances = ar.get_assets_by_class(class_name, True) - containers = [] + instances = [] # 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: + for asset_data in openpype_instances: asset = asset_data.get_asset() data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) data["objectName"] = asset_data.asset_name data = cast_map_to_str_dict(data) - containers.append(data) + instances.append(data) - return containers + return {"return": instances} -def containerise( - name, namespc, str_nodes, str_context, loader="", suffix="_CON" -): - """Bundles *nodes* (assets) into a *container* and add metadata to it. +def containerise(params): + """ + Args: + params (str): string containing a dictionary with parameters: + root (str): root path of the container + name (str): name of the container + data (dict): data of the container + suffix (str): suffix of the container + """ + root, name, data, suffix = get_params( + params, 'root', 'name', 'data', 'suffix') - 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. OpenPype 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. + suffix = suffix or "_CON" - This is list of strings starting with asset type and ending with its path: - `Material /Game/OpenPype/Test/TestMaterial.TestMaterial` + container_name = f"{name}{suffix}" - """ - namespace = namespc - nodes = ast.literal_eval(str_nodes) - context = ast.literal_eval(str_context) - if loader == "": - loader = None - # 1 - create directory for container - root = "/Game" - container_name = "{}{}".format(name, suffix) - new_name = move_assets_to_path(root, container_name, nodes) - - # 2 - create Asset Container there - path = "{}/{}".format(root, new_name) - create_container(container=container_name, path=path) - - namespace = path - - data = { - "schema": "openpype:container-2.0", - "id": "pyblish.avalon.container", - "name": new_name, - "namespace": namespace, - "loader": str(loader), - "representation": context["representation"]["_id"], - } - # 3 - imprint data - imprint("{}/{}".format(path, container_name), data) - return path - - -def instantiate(root, name, str_data, str_assets="", suffix="_INS"): - """Bundles *nodes* into *container*. - - Marking it with metadata as publishable instance. If assets are provided, - they are moved to new path where `OpenPypePublishInstance` class asset is - created and imprinted with metadata. - - This can then be collected for publishing by Pyblish for example. + # Check if container already exists + if not unreal.EditorAssetLibrary.does_asset_exist( + f"{root}/{container_name}"): + create_container(str({ + "container": container_name, + "path": root})) + + imprint( + str({"node": f"{root}/{container_name}", "data": data})) + + assets = unreal.EditorAssetLibrary.list_assets(root, True, True) + + for asset in assets: + unreal.EditorAssetLibrary.save_asset(asset) - 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 +def instantiate(params): """ - data = ast.literal_eval(str_data) - assets = ast.literal_eval(str_assets) - container_name = "{}{}".format(name, suffix) + Args: + params (str): string containing a dictionary with parameters: + root (str): root path of the instance + name (str): name of the instance + data (dict): data of the instance + assets (list): list of assets to add to the instance + suffix (str): suffix of the instance + """ + root, name, data, assets, suffix = get_params( + params, 'root', 'name', 'data', 'assets', 'suffix') + + suffix = suffix or "_INS" + + instance_name = f"{name}{suffix}" + + pub_instance = create_publish_instance( + str({"instance": instance_name, "path": root})).get("return") - # 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) + unreal.EditorAssetLibrary.save_asset(pub_instance.get_path_name()) - path = "{}/{}".format(root, new_name) - create_publish_instance(instance=container_name, path=path) + pub_instance.set_editor_property('add_external_assets', True) + asset_data = pub_instance.get_editor_property('asset_data_external') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for asset in assets: + obj = ar.get_asset_by_object_path(asset).get_asset() + asset_data.add(obj) - imprint("{}/{}".format(path, container_name), data) + imprint( + str({"node": f"{root}/{instance_name}", "data": data})) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py new file mode 100644 index 00000000000..6d3ef68b661 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py @@ -0,0 +1,1096 @@ +from pathlib import Path + +import unreal + +from helpers import ( + get_params, + format_string, + get_asset, +) +from pipeline import ( + UNREAL_VERSION, + ls, +) + + +def create_look(params): + """ + Args: + params (str): string containing a dictionary with parameters: + path (str): path to the instance + selected_asset (str): path to the selected asset + """ + path, selected_asset = get_params(params, 'path', 'selected_asset') + + # 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') + + 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')) + + members.append(object_path) + + unreal.EditorAssetLibrary.save_asset(object_path) + + return {"return": members} + + +def create_unique_asset_name(params): + """ + Args: + params (str): string containing a dictionary with parameters: + root (str): root path of the asset + asset (str): name of the asset + name (str): name of the subset + version (int): version of the subset + suffix (str): suffix of the asset + """ + root, asset, name, version, suffix = get_params( + params, 'root', 'asset', 'name', 'version', 'suffix') + + if not suffix: + suffix = "" + + tools = unreal.AssetToolsHelpers().get_asset_tools() + subset = f"{name}_v{version:03d}" if version else name + return {"return": tools.create_unique_asset_name( + f"{root}/{asset}/{subset}", suffix)} + + +def get_current_level(): + curr_level = (unreal.LevelEditorSubsystem().get_current_level() + if UNREAL_VERSION >= 5 + else unreal.EditorLevelLibrary.get_editor_world()) + + 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. + return { + "return": curr_level_path + if curr_level_path.startswith("/Game/") else None} + + +def add_level_to_world(params): + """ + Args: + params (str): string containing a dictionary with parameters: + level_path (str): path to the level + """ + level_path = get_params(params, 'level_path') + + unreal.EditorLevelUtils.add_level_to_world( + unreal.EditorLevelLibrary.get_editor_world(), + level_path, + unreal.LevelStreamingDynamic + ) + + +def list_assets(params): + """ + Args: + params (str): string containing a dictionary with parameters: + directory_path (str): path to the directory + recursive (bool): whether to list assets recursively + include_folder (bool): whether to include folders + """ + directory_path, recursive, include_folder = get_params( + params, 'directory_path', 'recursive', 'include_folder') + + return {"return": unreal.EditorAssetLibrary.list_assets( + directory_path, recursive, include_folder)} + + +def get_assets_of_class(params): + """ + Args: + params (str): string containing a dictionary with parameters: + asset_list (list): list of assets + class_name (str): name of the class + """ + asset_list, class_name = get_params(params, 'asset_list', 'class_name') + + assets = [] + for asset in asset_list: + if unreal.EditorAssetLibrary.does_asset_exist(asset): + asset_object = unreal.EditorAssetLibrary.load_asset(asset) + if asset_object.get_class().get_name() == class_name: + assets.append(asset) + return {"return": assets} + + +def get_all_assets_of_class(params): + """ + Args: + params (str): string containing a dictionary with parameters: + class_name (str): name of the class + path (str): path to the directory + recursive (bool): whether to list assets recursively + """ + class_name, path, recursive = get_params( + params, 'class_name', 'path', 'recursive') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + ar_filter = unreal.ARFilter( + class_names=[class_name], + package_paths=[path], + recursive_paths=recursive) + + assets = ar.get_assets(ar_filter) + + return { + "return": [str(asset.get_editor_property('object_path')) + for asset in assets]} + + +def get_first_asset_of_class(params): + """ + Args: + params (str): string containing a dictionary with parameters: + class_name (str): name of the class + path (str): path to the directory + recursive (bool): whether to list assets recursively + """ + return get_all_assets_of_class(params)[0] + + +def _get_first_asset_of_class(class_name, path, recursive): + """ + Args: + class_name (str): name of the class + path (str): path to the directory + recursive (bool): whether to list assets recursively + """ + return get_first_asset_of_class(format_string(str({ + "class_name": class_name, + "path": path, + "recursive": recursive}))).get('return') + + +def save_listed_assets(params): + """ + Args: + params (str): string containing a dictionary with parameters: + asset_list (list): list of assets + """ + asset_list = get_params(params, 'asset_list') + + for asset in asset_list: + unreal.EditorAssetLibrary.save_asset(asset) + + +def _import( + filename, destination_path, destination_name, replace_existing, + automated, save, options, options_properties, options_extra_properties +): + """ + Args: + filename (str): path to the file + destination_path (str): path to the destination + destination_name (str): name of the destination + replace_existing (bool): whether to replace existing assets + automated (bool): whether to import the asset automatically + save (bool): whether to save the asset + options: options for the import + options_properties (list): list of properties for the options + options_extra_properties (list): list of extra properties for the + options + """ + task = unreal.AssetImportTask() + + task.set_editor_property('filename', filename) + task.set_editor_property('destination_path', destination_path) + task.set_editor_property('destination_name', destination_name) + task.set_editor_property('replace_existing', replace_existing) + task.set_editor_property('automated', automated) + task.set_editor_property('save', save) + + for prop in options_properties: + options.set_editor_property(prop[0], eval(prop[1])) + + for prop in options_extra_properties: + options.get_editor_property(prop[0]).set_editor_property( + prop[1], eval(prop[2])) + + task.options = options + + return task + + +def import_abc_task(params): + """ + Args: + params (str): string containing a dictionary with parameters: + filename (str): path to the file + destination_path (str): path to the destination + destination_name (str): name of the file + replace_existing (bool): whether to replace existing assets + automated (bool): whether to run the task automatically + save (bool): whether to save the asset + options_properties (list): list of properties for the options + sub_options_properties (list): list of properties that require + extra processing + conversion_settings (dict): dictionary of conversion settings + """ + (filename, destination_path, destination_name, replace_existing, + automated, save, options_properties, sub_options_properties, + conversion_settings) = get_params( + params, 'filename', 'destination_path', 'destination_name', + 'replace_existing', 'automated', 'save', 'options_properties', + 'sub_options_properties', 'conversion_settings') + + task = _import( + filename, destination_path, destination_name, replace_existing, + automated, save, unreal.AbcImportSettings(), + options_properties, sub_options_properties) + + if conversion_settings: + conversion = unreal.AbcConversionSettings( + preset=unreal.AbcConversionPreset.CUSTOM, + flip_u=conversion_settings.get("flip_u"), + flip_v=conversion_settings.get("flip_v"), + rotation=conversion_settings.get("rotation"), + scale=conversion_settings.get("scale")) + + task.options.conversion_settings = conversion + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + +def import_fbx_task(params): + """ + Args: + params (str): string containing a dictionary with parameters: + task_properties (list): list of properties for the task + options_properties (list): list of properties for the options + options_extra_properties (list): list of extra properties for the + options + """ + (filename, destination_path, destination_name, replace_existing, + automated, save, options_properties, sub_options_properties) = get_params( + params, 'filename', 'destination_path', 'destination_name', + 'replace_existing', 'automated', 'save', 'options_properties', + 'sub_options_properties') + + task = _import( + filename, destination_path, destination_name, replace_existing, + automated, save, unreal.FbxImportUI(), + options_properties, sub_options_properties) + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + +def get_sequence_frame_range(params): + """ + Args: + params (str): string containing a dictionary with parameters: + sequence_path (str): path to the sequence + """ + sequence_path = get_params(params, 'sequence_path') + + sequence = get_asset(sequence_path) + return {"return": ( + sequence.get_playback_start(), sequence.get_playback_end())} + + +def generate_sequence(params): + """ + Args: + params (str): string containing a dictionary with parameters: + asset_name (str): name of the asset + asset_path (str): path to the asset + start_frame (int): start frame of the sequence + end_frame (int): end frame of the sequence + fps (int): frames per second + """ + asset_name, asset_path, start_frame, end_frame, fps = get_params( + params, 'asset_name', 'asset_path', 'start_frame', 'end_frame', 'fps') + + tools = unreal.AssetToolsHelpers().get_asset_tools() + + sequence = tools.create_asset( + asset_name=asset_name, + package_path=asset_path, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + sequence.set_display_rate(unreal.FrameRate(fps, 1.0)) + sequence.set_playback_start(start_frame) + sequence.set_playback_end(end_frame) + + return {"return": sequence.get_path_name()} + + +def generate_master_sequence(params): + """ + Args: + params (str): string containing a dictionary with parameters: + asset_name (str): name of the asset + asset_path (str): path to the asset + start_frame (int): start frame of the sequence + end_frame (int): end frame of the sequence + fps (int): frames per second + """ + sequence_path = generate_sequence(params).get("return") + sequence = get_asset(sequence_path) + + tracks = sequence.get_master_tracks() + track = next( + ( + t + for t in tracks + if t.get_class().get_name() == "MovieSceneCameraCutTrack" + ), + None + ) + if not track: + sequence.add_master_track(unreal.MovieSceneCameraCutTrack) + + return {"return": sequence.get_path_name()} + + +def set_sequence_hierarchy(params): + """ + Args: + params (str): string containing a dictionary with parameters: + parent_path (str): path to the parent sequence + child_path (str): path to the child sequence + child_start_frame (int): start frame of the child sequence + child_end_frame (int): end frame of the child sequence + """ + parent_path, child_path, child_start_frame, child_end_frame = get_params( + params, 'parent_path', 'child_path', 'child_start_frame', + 'child_end_frame') + + parent = get_asset(parent_path) + child = get_asset(child_path) + + # Get existing sequencer tracks or create them if they don't exist + tracks = parent.get_master_tracks() + subscene_track = next( + ( + t + for t in tracks + if t.get_class().get_name() == "MovieSceneSubTrack" + ), + None, + ) + if not subscene_track: + subscene_track = parent.add_master_track( + unreal.MovieSceneSubTrack) + + # Create the sub-scene section + subscenes = subscene_track.get_sections() + subscene = next( + ( + s + for s in subscenes + if s.get_editor_property('sub_sequence') == child + ), + None, + ) + if not subscene: + subscene = subscene_track.add_section() + subscene.set_row_index(len(subscene_track.get_sections())) + subscene.set_editor_property('sub_sequence', child) + subscene.set_range(child_start_frame, child_end_frame + 1) + + +def set_sequence_visibility(params): + """ + Args: + params (str): string containing a dictionary with parameters: + parent_path (str): path to the parent sequence + parent_end_frame (int): end frame of the parent sequence + child_start_frame (int): start frame of the child sequence + child_end_frame (int): end frame of the child sequence + map_paths (list): list of paths to the maps + """ + (parent_path, parent_end_frame, child_start_frame, child_end_frame, + map_paths) = get_params(params, 'parent_path', 'parent_end_frame', + 'child_start_frame', 'child_end_frame', + 'map_paths') + + parent = get_asset(parent_path) + + # Get existing sequencer tracks or create them if they don't exist + tracks = parent.get_master_tracks() + visibility_track = next( + ( + t + for t in tracks + if t.get_class().get_name() == "MovieSceneLevelVisibilityTrack" + ), + None, + ) + if not visibility_track: + visibility_track = parent.add_master_track( + unreal.MovieSceneLevelVisibilityTrack) + + # 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(child_start_frame, child_end_frame + 1) + vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) + vis_section.set_row_index(index) + vis_section.set_level_names(maps) + + if child_start_frame > 1: + hid_section = visibility_track.add_section() + hid_section.set_range(1, child_start_frame) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + if child_end_frame < parent_end_frame: + hid_section = visibility_track.add_section() + hid_section.set_range(child_end_frame + 1, parent_end_frame + 1) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + + +def _get_transform(import_data, basis_data, transform_data): + """ + Args: + import_data (unreal.FbxImportUI): import data + basis_data (list): basis data + transform_data (list): transform data + """ + filename = import_data.get_first_filename() + path = Path(filename) + + conversion = unreal.Matrix.IDENTITY.transform() + tuning = unreal.Matrix.IDENTITY.transform() + + basis = unreal.Matrix( + basis_data[0], + basis_data[1], + basis_data[2], + basis_data[3] + ).transform() + transform = unreal.Matrix( + transform_data[0], + transform_data[1], + transform_data[2], + transform_data[3] + ).transform() + + # Check for the conversion settings. We cannot access + # the alembic conversion settings, so we assume that + # the maya ones have been applied. + if path.suffix == '.fbx': + loc = import_data.import_translation + rot = import_data.import_rotation.to_vector() + scale = import_data.import_uniform_scale + conversion = unreal.Transform( + location=[loc.x, loc.y, loc.z], + rotation=[rot.x, -rot.y, -rot.z], + scale=[scale, scale, scale] + ) + tuning = unreal.Transform( + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0] + ) + elif path.suffix == '.abc': + # This is the standard conversion settings for + # alembic files from Maya. + conversion = unreal.Transform( + location=[0.0, 0.0, 0.0], + rotation=[90.0, 0.0, 0.0], + scale=[1.0, -1.0, 1.0] + ) + tuning = unreal.Transform( + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0] + ) + + new_transform = basis.inverse() * transform * basis + return tuning * conversion.inverse() * new_transform + + +def process_family(params): + """ + Args: + params (str): string containing a dictionary with parameters: + assets (list): list of paths to the assets + class_name (str): name of the class to spawn + instance_name (str): name of the instance + transform (list): list of 4 vectors representing the transform + basis (list): list of 4 vectors representing the basis + sequence_path (str): path to the sequence + """ + (assets, class_name, instance_name, transform, basis, + sequence_path) = get_params(params, 'assets', 'class_name', + 'instance_name', 'transform', 'basis', + 'sequence_path') + + actors = [] + bindings = [] + + component_property = '' + mesh_property = '' + + if class_name == 'SkeletalMesh': + component_property = 'skeletal_mesh_component' + mesh_property = 'skeletal_mesh' + elif class_name == 'StaticMesh': + component_property = 'static_mesh_component' + mesh_property = 'static_mesh' + + sequence = get_asset(sequence_path) if sequence_path else None + + for asset in assets: + obj = get_asset(asset) + if obj and obj.get_class().get_name() == class_name: + actor = unreal.EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0)) + actor.set_actor_label(instance_name) + + component = actor.get_editor_property(component_property) + mesh = component.get_editor_property(mesh_property) + import_data = mesh.get_editor_property('asset_import_data') + + transform = _get_transform(import_data, basis, transform) + + actor.set_actor_transform(transform, False, True) + + if class_name == 'SkeletalMesh': + skm_comp = actor.get_editor_property('skeletal_mesh_component') + skm_comp.set_bounds_scale(10.0) + + actors.append(actor.get_path_name()) + + if sequence: + binding = next( + ( + p + for p in sequence.get_possessables() + if p.get_name() == actor.get_name() + ), + None, + ) + if not binding: + binding = sequence.add_possessable(actor) + + bindings.append(binding.get_id().to_string()) + + return {"return": (actors, bindings)} + + +def apply_animation_to_actor(params): + """ + Args: + params (str): string containing a dictionary with parameters: + actor_path (str): path to the actor + animation_path (str): path to the animation + """ + actor_path, animation_path = get_params( + params, 'actor_path', 'animation_path') + + actor = get_asset(actor_path) + animation = get_asset(animation_path) + + 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) + + +def apply_animation(params): + """ + Args: + params (str): string containing a dictionary with parameters: + animation_path (str): path to the animation + instance_name (str): name of the instance + sequences (str): list of paths to the sequences + """ + animation_path, instance_name, sequences = get_params( + params, 'animation_path', 'instance_name', 'sequences') + + animation = get_asset(animation_path) + + anim_track_class = "MovieSceneSkeletalAnimationTrack" + anim_section_class = "MovieSceneSkeletalAnimationSection" + + for sequence_path in sequences: + sequence = get_asset(sequence_path) + possessables = [ + possessable for possessable in sequence.get_possessables() + if possessable.get_display_name() == instance_name] + + for possessable in possessables: + tracks = [ + track for track in possessable.get_tracks() + if (track.get_class().get_name() == anim_track_class)] + + if not tracks: + track = possessable.add_track( + unreal.MovieSceneSkeletalAnimationTrack) + tracks.append(track) + + for track in tracks: + sections = [ + section for section in track.get_sections() + if (section.get_class().get_name == anim_section_class)] + + if not sections: + sections.append(track.add_section()) + + for section in sections: + section.params.set_editor_property('animation', animation) + section.set_range( + sequence.get_playback_start(), + sequence.get_playback_end() - 1) + section.set_completion_mode( + unreal.MovieSceneCompletionMode.KEEP_STATE) + + +def add_animation_to_sequencer(params): + """ + Args: + params (str): string containing a dictionary with parameters: + sequence_path (str): path to the sequence + binding_guid (str): guid of the binding + animation_path (str): path to the animation + """ + sequence_path, binding_guid, animation_path = get_params( + params, 'sequence_path', 'binding_guid', 'animation_path') + + sequence = get_asset(sequence_path) + animation = get_asset(animation_path) + + binding = next( + ( + b + for b in sequence.get_possessables() + if b.get_id().to_string() == binding_guid + ), + None, + ) + tracks = binding.get_tracks() + track = tracks[0] if tracks else binding.add_track( + unreal.MovieSceneSkeletalAnimationTrack) + + sections = track.get_sections() + if not sections: + section = track.add_section() + else: + section = sections[0] + + sec_params = section.get_editor_property('params') + if curr_anim := sec_params.get_editor_property('animation'): + # 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('\\', '/') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + _filter = unreal.ARFilter( + class_names=["AssetContainer"], + 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 import_camera(params): + """ + Args: + params (str): string containing a dictionary with parameters: + sequence_path (str): path to the sequence + import_filename (str): path to the fbx file + """ + sequence_path, import_filename = get_params( + params, 'sequence_path', 'import_filename') + + sequence = get_asset(sequence_path) + + world = unreal.EditorLevelLibrary.get_editor_world() + + settings = unreal.MovieSceneUserImportFBXSettings() + settings.set_editor_property('reduce_keys', False) + + if UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor <= 26: + unreal.SequencerTools.import_fbx( + world, + sequence, + sequence.get_bindings(), + settings, + import_filename + ) + elif ((UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor >= 27) or + UNREAL_VERSION.major == 5): + unreal.SequencerTools.import_level_sequence_fbx( + world, + sequence, + sequence.get_bindings(), + settings, + import_filename + ) + else: + raise NotImplementedError( + f"Unreal version {UNREAL_VERSION.major} not supported") + + +def get_actor_and_skeleton(params): + """ + Args: + params (str): string containing a dictionary with parameters: + instance_name (str): name of the instance + """ + instance_name = get_params(params, 'instance_name') + + actor_subsystem = unreal.EditorActorSubsystem() + actors = actor_subsystem.get_all_level_actors() + actor = None + 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 RuntimeError(f"Could not find actor {instance_name}") + + skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton + + return {"return": (actor.get_path_name(), skeleton.get_path_name())} + + +def get_skeleton_from_skeletal_mesh(params): + """ + Args: + params (str): string containing a dictionary with parameters: + skeletal_mesh_path (str): path to the skeletal mesh + """ + skeletal_mesh_path = get_params(params, 'skeletal_mesh_path') + + skeletal_mesh = unreal.EditorAssetLibrary.load_asset(skeletal_mesh_path) + skeleton = skeletal_mesh.get_editor_property('skeleton') + + return {"return": skeleton.get_path_name()} + + +def remove_asset(params): + """ + Args: + params (str): string containing a dictionary with parameters: + path (str): path to the asset + """ + path = get_params(params, 'path') + + parent_path = Path(path).parent.as_posix() + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False, include_folder=True + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) + + +def delete_all_bound_assets(params): + """ + Delete from the current level all the assets that are bound to the + level sequence. + + Args: + params (str): string containing a dictionary with parameters: + level_sequence_path (str): path to the level sequence + """ + level_sequence_path = get_params(params, 'level_sequence_path') + + level_sequence = get_asset(level_sequence_path) + + # Get the actors in the level sequence. + bound_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 obj in bound_objs: + actor_path = obj.bound_objects[0].get_path_name().split(":")[-1] + actor = unreal.EditorLevelLibrary.get_actor_reference(actor_path) + unreal.EditorLevelLibrary.destroy_actor(actor) + + +def remove_camera(params): + """ + Args: + params (str): string containing a dictionary with parameters: + root (str): path to the root folder + asset_dir (str): path to the asset folder + """ + root, asset_dir = get_params(params, 'root', 'asset_dir') + + parent_path = Path(asset_dir).parent.as_posix() + + old_sequence = _get_first_asset_of_class("LevelSequence", asset_dir, False) + level = _get_first_asset_of_class("World", parent_path, True) + + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(level) + + # There should be only one sequence in the path. + level_sequence = get_asset(old_sequence) + sequence_name = level_sequence.get_fname() + + delete_all_bound_assets(level_sequence.get_path_name()) + + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to find + # the level sequence. + namespace = asset_dir.replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + master_sequence = get_asset(_get_first_asset_of_class( + "LevelSequence", f"{root}/{ms_asset}", False)) + + sequences = [master_sequence] + + parent_sequence = None + for sequence in sequences: + tracks = sequence.get_master_tracks() + # Get the subscene track. + if subscene_track := next( + ( + track + for track in tracks + if track.get_class().get_name() == "MovieSceneSubTrack" + ), + None, + ): + sections = subscene_track.get_sections() + for section in sections: + if section.get_sequence().get_name() == sequence_name: + parent_sequence = sequence + subscene_track.remove_section(section) + break + sequences.append(section.get_sequence()) + # Update subscenes indexes. + for i, section in enumerate(sections): + section.set_row_index(i) + + if parent_sequence: + break + + assert parent_sequence, "Could not find the parent sequence" + + unreal.EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) + + # Check if there isn't any more assets in the parent folder, and + # delete it if not. + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False, include_folder=True + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) + + return parent_sequence.get_path_name() + + +def _remove_subsequences(master_sequence, asset): + """ + Traverse hierarchy to remove subsequences. + + Args: + master_sequence (LevelSequence): master sequence + asset (str): asset name + """ + sequences = [master_sequence] + + parent = None + for sequence in sequences: + tracks = sequence.get_master_tracks() + subscene_track = None + visibility_track = None + for track in tracks: + if track.get_class().get_name() == "MovieSceneSubTrack": + subscene_track = track + if (track.get_class().get_name() == + "MovieSceneLevelVisibilityTrack"): + visibility_track = track + + if subscene_track: + sections = subscene_track.get_sections() + for section in sections: + if section.get_sequence().get_name() == asset: + parent = sequence + subscene_track.remove_section(section) + break + sequences.append(section.get_sequence()) + # Update subscenes indexes. + for i, section in enumerate(sections): + section.set_row_index(i) + + if visibility_track: + sections = visibility_track.get_sections() + for section in sections: + if (unreal.Name(f"{asset}_map") + in section.get_level_names()): + visibility_track.remove_section(section) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for section in sections: + if prev_name != section.get_level_names(): + i += 1 + section.set_row_index(i) + prev_name = section.get_level_names() + + if parent: + break + + assert parent, "Could not find the parent sequence" + + +def _remove_sequences_in_hierarchy(asset_dir, level_sequence, asset, root): + delete_all_bound_assets(level_sequence.get_path_name()) + + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to + # find the level sequence. + namespace = asset_dir.replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + master_sequence = get_asset(_get_first_asset_of_class( + "LevelSequence", f"{root}/{ms_asset}", False)) + master_level = _get_first_asset_of_class( + "World", f"{root}/{ms_asset}", False) + + _remove_subsequences(master_sequence, asset) + + return master_level + + +def remove_layout(params): + """ + Args: + params (str): string containing a dictionary with parameters: + root (str): path to the root folder + asset (str): path to the asset + asset_dir (str): path to the asset folder + asset_name (str): name of the asset + loaded_assets (str): list of loaded assets + create_sequences (str): boolean to create sequences + """ + (root, asset, asset_dir, asset_name, loaded_assets, + create_sequences) = get_params(params, 'root', 'asset', 'asset_dir', + 'asset_name', 'loaded_assets', + 'create_sequences') + + path = Path(asset_dir) + parent_path = path.parent.as_posix() + + level_sequence = get_asset(_get_first_asset_of_class( + "LevelSequence", path.as_posix(), False)) + level = _get_first_asset_of_class("World", parent_path, True) + + unreal.EditorLevelLibrary.load_level(level) + + containers = ls() + layout_containers = [ + c for c in containers + if c.get('asset_name') != 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 loaded_asset in eval(loaded_assets): + layouts = [ + lc for lc in layout_containers + if loaded_asset in lc.get('loaded_assets')] + + if not layouts: + unreal.EditorAssetLibrary.delete_directory( + Path(loaded_asset).parent.as_posix()) + + # Delete the parent folder if there aren't any more + # layouts in it. + asset_content = unreal.EditorAssetLibrary.list_assets( + Path(loaded_asset).parent.parent.as_posix(), recursive=False, + include_folder=True + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory( + str(Path(loaded_asset).parent.parent)) + + master_level = None + + if create_sequences: + master_level = _remove_sequences_in_hierarchy( + asset_dir, level_sequence, asset, root) + + actors = unreal.EditorLevelLibrary.get_all_level_actors() + + if not actors: + # Delete the level if it's empty. + # Create a temporary level to delete the layout level. + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorAssetLibrary.make_directory(f"{root}/tmp") + tmp_level = f"{root}/tmp/temp_map" + if not unreal.EditorAssetLibrary.does_asset_exist( + f"{tmp_level}.temp_map"): + unreal.EditorLevelLibrary.new_level(tmp_level) + else: + unreal.EditorLevelLibrary.load_level(tmp_level) + + # Delete the layout directory. + unreal.EditorAssetLibrary.delete_directory(path.as_posix()) + + if not actors: + unreal.EditorAssetLibrary.delete_directory(path.parent.as_posix()) + + if create_sequences: + unreal.EditorLevelLibrary.load_level(master_level) + unreal.EditorAssetLibrary.delete_directory(f"{root}/tmp") diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/__init__.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py deleted file mode 100644 index 4a07a58aee9..00000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/create.py +++ /dev/null @@ -1,60 +0,0 @@ -import ast - -import unreal - -from pipeline import ( - create_publish_instance, - imprint, -) - - -def new_publish_instance( - instance_name, path, str_instance_data, str_members -): - instance_data = ast.literal_eval(str_instance_data) - members = ast.literal_eval(str_members) - - pub_instance = create_publish_instance(instance_name, path) - - 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 members: - obj = ar.get_asset_by_object_path(member).get_asset() - assets.add(obj) - - imprint(f"{path}/{instance_name}", instance_data) - - -def create_look(selected_asset, 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') - - 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')) - - members.append(object_path) - - unreal.EditorAssetLibrary.save_asset(object_path) - - return members diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py deleted file mode 100644 index 7ef3253ad1f..00000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugins/load.py +++ /dev/null @@ -1,746 +0,0 @@ -from pipeline import ls - -import ast -from pathlib import Path - -import unreal - - -def get_asset(path): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - return ar.get_asset_by_object_path(path).get_asset() - - -def create_unique_asset_name(root, asset, name, version="", suffix=""): - tools = unreal.AssetToolsHelpers().get_asset_tools() - subset = f"{name}_v{version:03d}" if version else name - return tools.create_unique_asset_name( - f"{root}/{asset}/{subset}", suffix) - - -def get_current_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/"): - return curr_level_path - - return "" - - -def add_level_to_world(level_path): - unreal.EditorLevelUtils.add_level_to_world( - unreal.EditorLevelLibrary.get_editor_world(), - level_path, - unreal.LevelStreamingDynamic - ) - - -def list_assets(directory_path, recursive, include_folder): - recursive = ast.literal_eval(recursive) - include_folder = ast.literal_eval(include_folder) - return str(unreal.EditorAssetLibrary.list_assets( - directory_path, recursive, include_folder)) - - -def get_assets_of_class(asset_list, class_name): - asset_list = ast.literal_eval(asset_list) - assets = [] - for asset in asset_list: - if unreal.EditorAssetLibrary.does_asset_exist(asset): - asset_object = unreal.EditorAssetLibrary.load_asset(asset) - if asset_object.get_class().get_name() == class_name: - assets.append(asset) - return assets - - -def get_all_assets_of_class(class_name, path, recursive): - recursive = ast.literal_eval(recursive) - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - filter = unreal.ARFilter( - class_names=[class_name], - package_paths=[path], - recursive_paths=recursive) - - assets = ar.get_assets(filter) - - return [str(asset.get_editor_property('object_path')) for asset in assets] - - -def get_first_asset_of_class(class_name, path, recursive): - return get_all_assets_of_class(class_name, path, recursive)[0] - - -def save_listed_assets(asset_list): - asset_list = ast.literal_eval(asset_list) - for asset in asset_list: - unreal.EditorAssetLibrary.save_asset(asset) - - -def _import( - task_arg, options_arg, - task_properties, options_properties, options_extra_properties -): - task = task_arg - options = options_arg - - task_properties = ast.literal_eval(task_properties) - for prop in task_properties: - task.set_editor_property(prop[0], eval(prop[1])) - - options_properties = ast.literal_eval(options_properties) - for prop in options_properties: - options.set_editor_property(prop[0], eval(prop[1])) - - options_extra_properties = ast.literal_eval(options_extra_properties) - for prop in options_extra_properties: - options.get_editor_property(prop[0]).set_editor_property( - prop[1], eval(prop[2])) - - return task, options - - -def import_abc_task( - task_properties, options_properties, options_extra_properties -): - task, options = _import( - unreal.AssetImportTask(), unreal.AbcImportSettings(), - task_properties, options_properties, options_extra_properties) - - task.options = options - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - -def import_fbx_task( - task_properties, options_properties, options_extra_properties -): - task, options = _import( - unreal.AssetImportTask(), unreal.FbxImportUI(), - task_properties, options_properties, options_extra_properties) - - task.options = options - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - -def get_sequence_frame_range(sequence_path): - sequence = get_asset(sequence_path) - return (sequence.get_playback_start(), sequence.get_playback_end()) - - -def generate_sequence(asset_name, asset_path, start_frame, end_frame, fps): - tools = unreal.AssetToolsHelpers().get_asset_tools() - - sequence = tools.create_asset( - asset_name=asset_name, - package_path=asset_path, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - sequence.set_display_rate(unreal.FrameRate(fps, 1.0)) - sequence.set_playback_start(start_frame) - sequence.set_playback_end(end_frame) - - return sequence.get_path_name() - - -def generate_master_sequence( - asset_name, asset_path, start_frame, end_frame, fps -): - sequence_path = generate_sequence( - asset_name, asset_path, start_frame, end_frame, fps) - sequence = get_asset(sequence_path) - - tracks = sequence.get_master_tracks() - track = None - for t in tracks: - if t.get_class().get_name() == "MovieSceneCameraCutTrack": - track = t - break - if not track: - track = sequence.add_master_track(unreal.MovieSceneCameraCutTrack) - - return sequence.get_path_name() - - -def set_sequence_hierarchy( - parent_path, child_path, child_start_frame, child_end_frame -): - parent = get_asset(parent_path) - child = get_asset(child_path) - - # Get existing sequencer tracks or create them if they don't exist - tracks = parent.get_master_tracks() - subscene_track = None - for t in tracks: - if t.get_class().get_name() == "MovieSceneSubTrack": - subscene_track = t - break - if not subscene_track: - subscene_track = parent.add_master_track( - unreal.MovieSceneSubTrack) - - # Create the sub-scene section - subscenes = subscene_track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == child: - 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', child) - subscene.set_range(child_start_frame, child_end_frame + 1) - - -def set_sequence_visibility( - parent_path, parent_end_frame, child_start_frame, child_end_frame, - map_paths_str -): - map_paths = ast.literal_eval(map_paths_str) - - parent = get_asset(parent_path) - - # Get existing sequencer tracks or create them if they don't exist - tracks = parent.get_master_tracks() - visibility_track = None - for t in tracks: - if t.get_class().get_name() == "MovieSceneLevelVisibilityTrack": - visibility_track = t - break - if not visibility_track: - visibility_track = parent.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) - - # 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(child_start_frame, child_end_frame + 1) - vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) - vis_section.set_row_index(index) - vis_section.set_level_names(maps) - - if child_start_frame > 1: - hid_section = visibility_track.add_section() - hid_section.set_range(1, child_start_frame) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - if child_end_frame < parent_end_frame: - hid_section = visibility_track.add_section() - hid_section.set_range(child_end_frame + 1, parent_end_frame + 1) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - - -def get_transform(actor, import_data, basis_data, transform_data): - filename = import_data.get_first_filename() - path = Path(filename) - - conversion = unreal.Matrix.IDENTITY.transform() - tuning = unreal.Matrix.IDENTITY.transform() - - basis = unreal.Matrix( - basis_data[0], - basis_data[1], - basis_data[2], - basis_data[3] - ).transform() - transform = unreal.Matrix( - transform_data[0], - transform_data[1], - transform_data[2], - transform_data[3] - ).transform() - - # Check for the conversion settings. We cannot access - # the alembic conversion settings, so we assume that - # the maya ones have been applied. - if path.suffix == '.fbx': - loc = import_data.import_translation - rot = import_data.import_rotation.to_vector() - scale = import_data.import_uniform_scale - conversion = unreal.Transform( - location=[loc.x, loc.y, loc.z], - rotation=[rot.x, -rot.y, -rot.z], - scale=[scale, scale, scale] - ) - tuning = unreal.Transform( - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0] - ) - elif path.suffix == '.abc': - # This is the standard conversion settings for - # alembic files from Maya. - conversion = unreal.Transform( - location=[0.0, 0.0, 0.0], - rotation=[90.0, 0.0, 0.0], - scale=[1.0, -1.0, 1.0] - ) - tuning = unreal.Transform( - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0] - ) - - new_transform = basis.inverse() * transform * basis - return tuning * conversion.inverse() * new_transform - - -def process_family( - assets_str, class_name, instance_name, - transform_str, basis_str, sequence_path -): - assets = ast.literal_eval(assets_str) - basis_data = ast.literal_eval(basis_str) - transform_data = ast.literal_eval(transform_str) - - actors = [] - bindings = [] - - component_property = '' - mesh_property = '' - - if class_name == 'StaticMesh': - component_property = 'static_mesh_component' - mesh_property = 'static_mesh' - elif class_name == 'SkeletalMesh': - component_property = 'skeletal_mesh_component' - mesh_property = 'skeletal_mesh' - - sequence = get_asset(sequence_path) if sequence_path else None - - for asset in assets: - obj = get_asset(asset) - if obj and obj.get_class().get_name() == class_name: - actor = unreal.EditorLevelLibrary.spawn_actor_from_object( - obj, unreal.Vector(0.0, 0.0, 0.0)) - actor.set_actor_label(instance_name) - - component = actor.get_editor_property(component_property) - mesh = component.get_editor_property(mesh_property) - import_data = mesh.get_editor_property('asset_import_data') - - transform = get_transform( - actor, import_data, basis_data, transform_data) - - actor.set_actor_transform(transform, False, True) - - if class_name == 'SkeletalMesh': - skm_comp = actor.get_editor_property('skeletal_mesh_component') - skm_comp.set_bounds_scale(10.0) - - actors.append(actor.get_path_name()) - - 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.get_id().to_string()) - - return (actors, bindings) - - -def apply_animation_to_actor(actor_path, animation_path): - actor = get_asset(actor_path) - animation = get_asset(animation_path) - - 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) - - -def apply_animation(animation_path, instance_name, sequences): - animation = get_asset(animation_path) - sequences = ast.literal_eval(sequences) - - anim_track_class = "MovieSceneSkeletalAnimationTrack" - anim_section_class = "MovieSceneSkeletalAnimationSection" - - for sequence_path in sequences: - sequence = get_asset(sequence_path) - possessables = [ - possessable for possessable in sequence.get_possessables() - if possessable.get_display_name() == instance_name] - - for possessable in possessables: - tracks = [ - track for track in possessable.get_tracks() - if (track.get_class().get_name() == anim_track_class)] - - if not tracks: - track = possessable.add_track( - unreal.MovieSceneSkeletalAnimationTrack) - tracks.append(track) - - for track in tracks: - sections = [ - section for section in track.get_sections() - if (section.get_class().get_name == anim_section_class)] - - if not sections: - sections.append(track.add_section()) - - for section in sections: - section.params.set_editor_property('animation', animation) - section.set_range( - sequence.get_playback_start(), - sequence.get_playback_end() - 1) - section.set_completion_mode( - unreal.MovieSceneCompletionMode.KEEP_STATE) - - -def add_animation_to_sequencer(sequence_path, binding_guid, animation_path): - sequence = get_asset(sequence_path) - animation = get_asset(animation_path) - - binding = None - for b in sequence.get_possessables(): - if b.get_id().to_string() == binding_guid: - binding = b - break - - 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('\\', '/') - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - _filter = unreal.ARFilter( - class_names=["AssetContainer"], - 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 import_camera(sequence_path, import_filename): - sequence = get_asset(sequence_path) - - world = unreal.EditorLevelLibrary.get_editor_world() - - settings = unreal.MovieSceneUserImportFBXSettings() - settings.set_editor_property('reduce_keys', False) - - 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, - sequence.get_bindings(), - settings, - import_filename - ) - elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5: - unreal.SequencerTools.import_level_sequence_fbx( - world, - sequence, - sequence.get_bindings(), - settings, - import_filename - ) - else: - raise NotImplementedError( - f"Unreal version {ue_major} not supported") - - -def get_actor_and_skeleton(instance_name): - actor_subsystem = unreal.EditorActorSubsystem() - actors = actor_subsystem.get_all_level_actors() - actor = None - 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 - - return (actor.get_path_name(), skeleton.get_path_name()) - - -def remove_asset(path): - parent_path = Path(path).parent.as_posix() - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) - - -def delete_all_bound_assets(level_sequence_str): - level_sequence = get_asset(level_sequence_str) - - # Delete from the current level all the assets that are bound to the - # level sequence. - - # Get the actors in the level sequence. - bound_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 obj in bound_objs: - actor_path = obj.bound_objects[0].get_path_name().split(":")[-1] - actor = unreal.EditorLevelLibrary.get_actor_reference(actor_path) - unreal.EditorLevelLibrary.destroy_actor(actor) - - -def remove_camera(root, asset_dir): - path = Path(asset_dir) - parent_path = path.parent.as_posix() - - old_sequence = get_first_asset_of_class( - "LevelSequence", path.as_posix(), "False") - level = get_first_asset_of_class("World", parent_path, "True") - - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorLevelLibrary.load_level(level) - - # There should be only one sequence in the path. - level_sequence = get_asset(old_sequence) - sequence_name = level_sequence.get_fname() - - delete_all_bound_assets(level_sequence.get_path_name()) - - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to find - # the level sequence. - namespace = asset_dir.replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - master_sequence = get_asset(get_first_asset_of_class( - "LevelSequence", f"{root}/{ms_asset}", "False")) - - sequences = [master_sequence] - - parent_sequence = None - for sequence in sequences: - tracks = sequence.get_master_tracks() - subscene_track = None - for track in tracks: - if track.get_class().get_name() == "MovieSceneSubTrack": - subscene_track = track - break - if subscene_track: - sections = subscene_track.get_sections() - for section in sections: - if section.get_sequence().get_name() == sequence_name: - parent_sequence = sequence - subscene_track.remove_section(section) - break - sequences.append(section.get_sequence()) - # Update subscenes indexes. - for i, section in enumerate(sections): - section.set_row_index(i) - - if parent_sequence: - break - - assert parent_sequence, "Could not find the parent sequence" - - unreal.EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) - - # Check if there isn't any more assets in the parent folder, and - # delete it if not. - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) - - return parent_sequence.get_path_name() - - -def remove_layout( - root, asset, asset_dir, asset_name, loaded_assets, create_sequences -): - path = Path(asset_dir) - parent_path = path.parent.as_posix() - - level_sequence = get_asset(get_first_asset_of_class( - "LevelSequence", path.as_posix(), "False")) - level = get_first_asset_of_class("World", parent_path, "True") - - unreal.EditorLevelLibrary.load_level(level) - - containers = ls() - layout_containers = [ - c for c in containers - if c.get('asset_name') != 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 loaded_asset in eval(loaded_assets): - layouts = [ - lc for lc in layout_containers - if loaded_asset in lc.get('loaded_assets')] - - if not layouts: - unreal.EditorAssetLibrary.delete_directory( - Path(loaded_asset).parent.as_posix()) - - # Delete the parent folder if there aren't any more - # layouts in it. - asset_content = unreal.EditorAssetLibrary.list_assets( - Path(loaded_asset).parent.parent.as_posix(), recursive=False, - include_folder=True - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory( - str(Path(loaded_asset).parent.parent)) - - master_sequence = None - master_level = None - sequences = [] - - if create_sequences: - delete_all_bound_assets(level_sequence.get_path_name()) - - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to - # find the level sequence. - namespace = asset_dir.replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - master_sequence = get_asset(get_first_asset_of_class( - "LevelSequence", f"{root}/{ms_asset}", "False")) - master_level = get_first_asset_of_class( - "World", f"{root}/{ms_asset}", "False") - - sequences = [master_sequence] - - parent = None - for sequence in sequences: - tracks = sequence.get_master_tracks() - subscene_track = None - visibility_track = None - for track in tracks: - if track.get_class().get_name() == "MovieSceneSubTrack": - subscene_track = track - if (track.get_class().get_name() == - "MovieSceneLevelVisibilityTrack"): - visibility_track = track - if subscene_track: - sections = subscene_track.get_sections() - for section in sections: - if section.get_sequence().get_name() == asset: - parent = sequence - subscene_track.remove_section(section) - break - sequences.append(section.get_sequence()) - # Update subscenes indexes. - for i, section in enumerate(sections): - section.set_row_index(i) - - if visibility_track: - sections = visibility_track.get_sections() - for section in sections: - if (unreal.Name(f"{asset}_map") - in section.get_level_names()): - visibility_track.remove_section(section) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for section in sections: - if prev_name != section.get_level_names(): - i += 1 - section.set_row_index(i) - prev_name = section.get_level_names() - if parent: - break - - assert parent, "Could not find the parent sequence" - - actors = unreal.EditorLevelLibrary.get_all_level_actors() - - if not actors: - # Delete the level if it's empty. - # Create a temporary level to delete the layout level. - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorAssetLibrary.make_directory(f"{root}/tmp") - tmp_level = f"{root}/tmp/temp_map" - if not unreal.EditorAssetLibrary.does_asset_exist( - f"{tmp_level}.temp_map"): - unreal.EditorLevelLibrary.new_level(tmp_level) - else: - unreal.EditorLevelLibrary.load_level(tmp_level) - - # Delete the layout directory. - unreal.EditorAssetLibrary.delete_directory(path.as_posix()) - - if not actors: - unreal.EditorAssetLibrary.delete_directory(path.parent.as_posix()) - - if create_sequences: - unreal.EditorLevelLibrary.load_level(master_level) - unreal.EditorAssetLibrary.delete_directory(f"{root}/tmp") diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/rendering.py similarity index 72% rename from openpype/hosts/unreal/api/rendering.py rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/rendering.py index 29e4747f6ec..3d94737e560 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/rendering.py @@ -3,15 +3,19 @@ import unreal from openpype.pipeline import Anatomy -from openpype.hosts.unreal.api import pipeline - +from helpers import ( + get_subsequences, +) +from pipeline import ( + parse_container, +) queue = None executor = None def _queue_finish_callback(exec, success): - unreal.log("Render completed. Success: " + str(success)) + unreal.log(f"Render completed. Success: {str(success)}") # Delete our reference so we don't keep it alive. global executor @@ -45,7 +49,7 @@ def start_rendering(): inst_data = [] for i in instances: - data = pipeline.parse_container(i.get_path_name()) + data = parse_container(i.get_path_name()) if data["family"] == "render": inst_data.append(data) @@ -53,8 +57,9 @@ def start_rendering(): project = os.environ.get("AVALON_PROJECT") anatomy = Anatomy(project) root = anatomy.roots['renders'] - except Exception: - raise Exception("Could not find render root in anatomy settings.") + except Exception as e: + raise RuntimeError( + "Could not find render root in anatomy settings.") from e render_dir = f"{root}/{project}" @@ -82,24 +87,26 @@ def start_rendering(): # add them and their frame ranges to the render list. We also # use the names for the output paths. for s in sequences: - subscenes = pipeline.get_subsequences(s.get('sequence')) - - if subscenes: - for ss in subscenes: - sequences.append({ + if subscenes := get_subsequences(s.get('sequence')): + sequences.extend( + { "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), + "output": ( + f"{s.get('output')}/" + f"{ss.get_sequence().get_name()}" + ), "frame_range": ( - ss.get_start_frame(), ss.get_end_frame()) - }) - else: - # Avoid rendering camera sequences - if "_camera" not in s.get('sequence').get_name(): - render_list.append(s) + ss.get_start_frame(), + ss.get_end_frame(), + ), + } + for ss in subscenes + ) + elif "_camera" not in s.get('sequence').get_name(): + render_list.append(s) # Create the rendering jobs and add them to the queue. - for r in render_list: + for render in render_list: job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) job.sequence = unreal.SoftObjectPath(i["master_sequence"]) job.map = unreal.SoftObjectPath(i["master_level"]) @@ -110,18 +117,21 @@ def start_rendering(): # for instance, pass the AvalonPublishInstance's path to the job. # job.user_data = "" + frame_range = render.get("frame_range") + output = render.get("output") + settings = job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineOutputSetting) settings.output_resolution = unreal.IntPoint(1920, 1080) - settings.custom_start_frame = r.get("frame_range")[0] - settings.custom_end_frame = r.get("frame_range")[1] + settings.custom_start_frame = frame_range[0] + settings.custom_end_frame = frame_range[1] settings.use_custom_playback_range = True settings.file_name_format = "{sequence_name}.{frame_number}" - settings.output_directory.path = f"{render_dir}/{r.get('output')}" + settings.output_directory.path = f"{render_dir}/{output}" - renderPass = job.get_configuration().find_or_add_setting_by_class( + render_pass = job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineDeferredPassBase) - renderPass.disable_multisample_effects = True + render_pass.disable_multisample_effects = True job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineImageSequenceOutput_PNG) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp index 36e732bf50b..4cb02cd74ae 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -7,6 +7,8 @@ #include "IPythonScriptPlugin.h" #include "PythonScriptTypes.h" +#include + // Initialize static attributes TSharedPtr FOpenPypeCommunication::Socket = nullptr; @@ -181,19 +183,68 @@ void FOpenPypeCommunication::RunMethod(TSharedPtr Root) } FString Method = Root->GetStringField(TEXT("method")); - UE_LOG(LogTemp, Verbose, TEXT("Calling a function: %s"), *Method); + UE_LOG(LogTemp, Warning, TEXT("Calling a function: %s"), *Method); - FPythonCommandEx Command; - Command.ExecutionMode = EPythonCommandExecutionMode::EvaluateStatement; - Command.Command = Method + "("; - auto params = Root->GetArrayField(TEXT("params")); - for (auto param : params) + TSharedPtr ParamsObject = Root->GetObjectField("params"); + + TArray< TSharedPtr > FieldNames; + ParamsObject->Values.GenerateValueArray(FieldNames); + + FString ParamStrings; + + // Checks if there are parameters in the params field. + if (FieldNames.Num() > 0) { - Command.Command += " " + param->AsString() + ","; + // If there are parameters, we serialize the params field to a string. + TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&ParamStrings); + FJsonSerializer::Serialize(ParamsObject.ToSharedRef(), Writer); + + UE_LOG(LogTemp, Warning, TEXT("Parameters: %s"), *ParamStrings); + + // We need to escape the parameters string, because it will be used in a python command. + // We use the std::regex library to do this. + std::string ParamsStr(TCHAR_TO_UTF8(*ParamStrings)); + + std::regex re; + std::string str; + + re = std::regex("\\\\"); + str = std::regex_replace(ParamsStr, re, "/"); + + re = std::regex("'"); + str = std::regex_replace(str, re, "\\'"); + + re = std::regex("\""); + str = std::regex_replace(str, re, "\\\""); + + // Fix true and false for python + std::regex true_regex("\\btrue\\b"); + std::regex false_regex("\\bfalse\\b"); + str = std::regex_replace(str, true_regex, "True"); + str = std::regex_replace(str, false_regex, "False"); + + // We also need to remove the new line characters from the string, because + // they will cause an error in the python command. + std::string FormattedParamsStr; + std::remove_copy(str.begin(), str.end(), std::back_inserter(FormattedParamsStr), '\n'); + + ParamStrings = FString(UTF8_TO_TCHAR(FormattedParamsStr.c_str())); + + UE_LOG(LogTemp, Warning, TEXT("Formatted Parameters: %s"), *ParamStrings); + + ParamStrings = "\"\"\"" + ParamStrings + "\"\"\""; } - Command.Command += ")"; - UE_LOG(LogTemp, Verbose, TEXT("Full command: %s"), *Command.Command); + // To execute the method from python, we need to construct the command + // as a string. We use the FPythonCommandEx struct to do this. We set + // the ExecutionMode to EvaluateStatement, so that the command is + // executed as a statement. + FPythonCommandEx Command; + Command.ExecutionMode = EPythonCommandExecutionMode::EvaluateStatement; + // Get the command from the params field, that is a python dict. + Command.Command = Method + "(" + ParamStrings + ")"; + + UE_LOG(LogTemp, Warning, TEXT("Full command: %s"), *Command.Command); FString StringResponse; diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 927070d0180..3bba84d1780 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.pipeline import ( + send_request, +) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) @@ -16,13 +18,13 @@ class CreateCamera(UnrealAssetCreator): def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): - selection = up.send_request("get_selected_assets") + selection = send_request("get_selected_assets") if len(selection) != 1: raise CreatorError("Please select only one object.") # Add the current level path to the metadata - instance_data["level"] = up.send_request("get_editor_world") + instance_data["level"] = send_request("get_editor_world") super(CreateCamera, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 063f15f05b2..273399979ce 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.pipeline import ( + send_request, +) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) @@ -18,7 +20,7 @@ class CreateLook(UnrealAssetCreator): def create(self, subset_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 - selection = up.send_request("get_selected_assets") + selection = send_request("get_selected_assets") if len(selection) != 1: raise CreatorError("Please select only one asset.") @@ -28,15 +30,16 @@ def create(self, subset_name, instance_data, pre_create_data): look_directory = "/Game/OpenPype/Looks" # Create the folder - folder_name = up.send_request( - "create_folder", params=[look_directory, subset_name]) + folder_name = send_request( + "create_folder", + params={"root": look_directory, "name": subset_name}) path = f"{look_directory}/{folder_name}" instance_data["look"] = path - pre_create_data["members"] = up.send_request( - "create_look", params=[selected_asset, path] - ) + pre_create_data["members"] = send_request( + "create_look", + params={"path": path, "selected_asset": selected_asset}) super(CreateLook, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index 1bb27d63e48..f44e981746f 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -2,7 +2,9 @@ from pathlib import Path from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.pipeline import ( + send_request, +) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) @@ -18,14 +20,15 @@ class CreateUAsset(UnrealAssetCreator): def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): - selection = up.send_request("get_selected_assets") + selection = send_request("get_selected_assets") if len(selection) != 1: raise CreatorError("Please select only one object.") obj = selection[0] - sys_path = up.send_request("get_system_path", params=[obj]) + sys_path = send_request( + "get_system_path", params={"asset_path": obj}) if not sys_path: raise CreatorError( diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index 496b6056ea6..b544f8805c4 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- """Load Alembic Animation.""" -import os from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class AnimationAlembicLoader(plugin.Loader): +class AnimationAlembicLoader(UnrealBaseLoader): """Load Unreal SkeletalMesh from Alembic""" families = ["animation"] @@ -20,33 +21,33 @@ class AnimationAlembicLoader(plugin.Loader): 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): + @staticmethod + def _import_abc_task( + filename, destination_path, destination_name, replace + ): + conversion = { + "flip_u": False, + "flip_v": False, + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, -1.0], + } + + params = { + "filename": filename, + "destination_path": destination_path, + "destination_name": destination_name, + "replace_existing": replace, + "automated": True, + "save": True, + "options_properties": [ + ['import_type', 'unreal.AlembicImportType.SKELETAL'] + ], + "conversion_settings": conversion + } + + send_request("import_abc_task", params=params) + + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -61,40 +62,30 @@ def load(self, context, name, namespace, data): 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 + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. """ # Create directory for asset and openpype container - root = "/Game/OpenPype/Assets" + root = f"{self.root}/Assets" asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" version = context.get('version').get('name') - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task(self.fname, asset_dir, asset_name, False) + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name, + "version": version}) - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - asset_tools.import_asset_tasks([task]) + if not send_request( + "does_directory_exist", params={"directory_path": asset_dir}): + send_request( + "make_directory", params={"directory_path": asset_dir}) - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + self._import_abc_task( + self.fname, asset_dir, asset_name, False) data = { "schema": "openpype:container-2.0", @@ -103,60 +94,19 @@ def load(self, context, name, namespace, data): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "loader": self.__class__.__name__, + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(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 + containerise(asset_dir, container_name, data) def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] - - task = self.get_task(source_path, destination_path, 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": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - 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) + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + self._import_abc_task(filename, asset_dir, asset_name, True) - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + super(UnrealBaseLoader, self).update(container, representation) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index e09b0d82e5c..a77330365dc 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Load FBX with animations.""" -import os import json from openpype.pipeline.context_tools import get_current_project_asset @@ -8,11 +7,14 @@ get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class AnimationFBXLoader(plugin.Loader): +class AnimationFBXLoader(UnrealBaseLoader): """Load Unreal SkeletalMesh from FBX.""" families = ["animation"] @@ -21,6 +23,52 @@ class AnimationFBXLoader(plugin.Loader): icon = "cube" color = "orange" + @staticmethod + def _import_fbx_task( + filename, destination_path, destination_name, replace, automated, + skeleton + ): + asset_doc = get_current_project_asset(fields=["data.fps"]) + fps = asset_doc.get("data", {}).get("fps") + + options_properties = [ + ["automated_import_should_detect_type", "False"], + ["original_import_type", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH"], + ["mesh_type_to_import", + "unreal.FBXImportType.FBXIT_ANIMATION"], + ["import_mesh", "False"], + ["import_animations", "True"], + ["override_full_name", "True"], + ["skeleton", f"get_asset({skeleton})"] + ] + + sub_options_properties = [ + ["anim_sequence_import_data", "animation_length", + "unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME"], + ["anim_sequence_import_data", + "import_meshes_in_bone_hierarchy", "False"], + ["anim_sequence_import_data", "use_default_sample_rate", "False"], + ["anim_sequence_import_data", "custom_sample_rate", str(fps)], + ["anim_sequence_import_data", "import_custom_attribute", "True"], + ["anim_sequence_import_data", "import_bone_tracks", "True"], + ["anim_sequence_import_data", "remove_redundant_keys", "False"], + ["anim_sequence_import_data", "convert_scene", "True"] + ] + + params = { + "filename": filename, + "destination_path": destination_path, + "destination_name": destination_name, + "replace_existing": replace, + "automated": automated, + "save": True, + "options_properties": options_properties, + "sub_options_properties": sub_options_properties + } + + send_request("import_fbx_task", params=params) + def _process(self, asset_dir, asset_name, instance_name): automated = False actor = None @@ -28,75 +76,41 @@ def _process(self, asset_dir, asset_name, instance_name): if instance_name: automated = True - actor, skeleton = up.send_request_literal( - "get_actor_and_skeleton", params=[instance_name]) + actor, skeleton = send_request( + "get_actor_and_skeleton", + params={"instance_name": instance_name}) if not actor: return None - asset_doc = get_current_project_asset(fields=["data.fps"]) - fps = asset_doc.get("data", {}).get("fps") + self._import_fbx_task( + self.fname, asset_dir, asset_name, False, automated, + skeleton) - task_properties = [ - ("filename", up.format_string(self.fname)), - ("destination_path", up.format_string(asset_dir)), - ("destination_name", up.format_string(asset_name)), - ("replace_existing", "False"), - ("automated", str(automated)), - ("save", "False") - ] - - options_properties = [ - ("automated_import_should_detect_type", "False"), - ("original_import_type", - "unreal.FBXImportType.FBXIT_SKELETAL_MESH"), - ("mesh_type_to_import", - "unreal.FBXImportType.FBXIT_ANIMATION"), - ("import_mesh", "False"), - ("import_animations", "True"), - ("override_full_name", "True"), - ("skeleton", f"get_asset({up.format_string(skeleton)})") - ] - - options_extra_properties = [ - ("anim_sequence_import_data", "animation_length", - "unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME"), - ("anim_sequence_import_data", - "import_meshes_in_bone_hierarchy", "False"), - ("anim_sequence_import_data", "use_default_sample_rate", "False"), - ("anim_sequence_import_data", "custom_sample_rate", str(fps)), - ("anim_sequence_import_data", "import_custom_attribute", "True"), - ("anim_sequence_import_data", "import_bone_tracks", "True"), - ("anim_sequence_import_data", "remove_redundant_keys", "False"), - ("anim_sequence_import_data", "convert_scene", "True") - ] - - up.send_request( - "import_fbx_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) - - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) + asset_content = send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) animation = None - animations = up.send_request_literal( + if animations := send_request( "get_assets_of_class", - params=[asset_content, "AnimSequence"]) - if animations: + params={"asset_list": asset_content, "class_name": "AnimSequence"}, + ): animation = animations[0] if animation: - up.send_request( - "apply_animation_to_actor", params=[actor, animation]) + send_request( + "apply_animation_to_actor", + params={ + "actor_path": actor, + "animation_path": animation}) return animation - def load(self, context, name, namespace, options=None): + def load(self, context, name=None, namespace=None, options=None): """ Load and containerise representation into Content Browser. @@ -112,42 +126,44 @@ def load(self, context, name, namespace, options=None): 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 + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. """ - # Create directory for asset and avalon container + # Create directory for asset and OpenPype container hierarchy = context.get('asset').get('data').get('parents') - root = "/Game/OpenPype" + root = self.root asset = context.get('asset').get('name') - suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" - asset_dir, container_name = up.send_request_literal( - "create_unique_asset_name", - params=[f"{root}/Animations", asset, name]) + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name}) - master_level = up.send_request( + master_level = send_request( "get_first_asset_of_class", - params=["World", f"{root}/{hierarchy[0]}", "False"]) + params={ + "class_name": "World", + "path": f"{root}/{hierarchy[0]}", + "recursive": False}) hierarchy_dir = root for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_dir = f"{hierarchy_dir}/{asset}" - level = up.send_request( + level = send_request( "get_first_asset_of_class", - params=["World", f"{hierarchy_dir}/", "False"]) - - up.send_request("save_all_dirty_levels") - up.send_request("load_level", params=[level]) + params={ + "class_name": "World", + "path": f"{hierarchy_dir}/", + "recursive": False}) - container_name += suffix + send_request("save_all_dirty_levels") + send_request("load_level", params={"level_path": level}) - up.send_request("make_directory", params=[asset_dir]) + send_request("make_directory", params={"directory_path": asset_dir}) libpath = self.fname.replace("fbx", "json") @@ -158,24 +174,28 @@ def load(self, context, name, namespace, options=None): animation = self._process(asset_dir, asset_name, instance_name) - asset_content = up.send_request_literal( - "list_assets", params=[hierarchy_dir, "True", "False"]) + asset_content = send_request( + "list_assets", params={ + "directory_path": hierarchy_dir, + "recursive": True, + "include_folder": False}) # Get the sequence for the layout, excluding the camera one. - all_sequences = up.send_request_literal( + all_sequences = send_request( "get_assets_of_class", - params=[asset_content, "LevelSequence"]) + params={ + "asset_list": asset_content, + "class_name": "LevelSequence"}) sequences = [ a for a in all_sequences if "_camera" not in a.split("/")[-1]] - up.send_request( + send_request( "apply_animation", - params=[animation, instance_name, str(sequences)]) - - # Create Asset Container - up.send_request( - "create_container", params=[container_name, asset_dir]) + params={ + "animation_path": animation, + "instance_name": instance_name, + "sequences": sequences}) data = { "schema": "openpype:container-2.0", @@ -184,104 +204,28 @@ def load(self, context, name, namespace, options=None): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), + "loader": self.__class__.__name__, "representation": str(context["representation"]["_id"]), "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "False"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - up.send_request("save_current_level") - up.send_request("load_level", params=[master_level]) - - return asset_content - - # def update(self, container, representation): - # name = container["asset_name"] - # source_path = get_representation_path(representation) - # asset_doc = get_current_project_asset(fields=["data.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', 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', asset_doc.get("data", {}).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 - # up.imprint( - # container_path, - # { - # "representation": str(representation["_id"]), - # "parent": str(representation["parent"]) - # }) - - # 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) + + containerise(asset_dir, container_name, data) + + send_request("save_current_level") + send_request("load_level", params={"level_path": master_level}) + + def update(self, container, representation): + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] + + skeleton = send_request( + "get_skeleton_from_skeletal_mesh", + params={ + "skeletal_mesh_path": f"{asset_dir}/{asset_name}"}) + + self._import_fbx_task( + filename, asset_dir, asset_name, True, True, skeleton) + + super(UnrealBaseLoader, self).update(container, representation) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 801eb4ac2ec..438bb5f5f03 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -7,11 +7,14 @@ AVALON_CONTAINER_ID, legacy_io, ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class CameraLoader(plugin.Loader): +class CameraLoader(UnrealBaseLoader): """Load Unreal StaticMesh from FBX""" families = ["camera"] @@ -20,7 +23,38 @@ class CameraLoader(plugin.Loader): icon = "cube" color = "orange" - def _get_frame_info(self, h_dir): + @staticmethod + def _create_levels( + hierarchy_dir_list, hierarchy, asset_path_parent, asset + ): + # 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[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + if not send_request( + "does_asset_exist", params={"asset_path": master_level}): + send_request( + "new_level", + params={"level_path": f"{h_dir}/{h_asset}_map"}) + + level = f"{asset_path_parent}/{asset}_map.{asset}_map" + if not send_request( + "does_asset_exist", params={"asset_path": level}): + send_request( + "new_level", + params={"level_path": f"{asset_path_parent}/{asset}_map"}) + + send_request("load_level", params={"level_path": master_level}) + send_request("add_level_to_world", params={"level_path": level}) + + send_request("save_all_dirty_levels") + send_request("load_level", params={"level_path": level}) + + return master_level + + @staticmethod + def _get_frame_info(h_dir): project_name = legacy_io.active_project() asset_data = get_asset_by_name( project_name, @@ -51,97 +85,7 @@ def _get_frame_info(self, h_dir): return min_frame, max_frame, asset_data.get('data').get("fps") - 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): subset name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and avalon container - hierarchy = context.get('asset').get('data').get('parents') - root = "/Game/OpenPype" - hierarchy_dir = root - hierarchy_dir_list = [] - for h in hierarchy: - hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir_list.append(hierarchy_dir) - asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) - - # Create a unique name for the camera directory - unique_number = 1 - if up.send_request_literal("does_directory_exist", - params=[f"{hierarchy_dir}/{asset}"]): - asset_content = up.send_request_literal( - "list_assets", params=[f"{root}/{asset}", "False", "True"]) - - # Get highest number to make a unique name - folders = [a for a in asset_content - if a[-1] == "/" and f"{name}_" in a] - f_numbers = [] - for f in folders: - # Get number from folder name. Splits the string by "_" and - # removes the last element (which is a "/"). - f_numbers.append(int(f.split("_")[-1][:-1])) - f_numbers.sort() - if not f_numbers: - unique_number = 1 - else: - unique_number = f_numbers[-1] + 1 - - asset_dir, container_name = up.send_request_literal( - "create_unique_asset_name", params=[ - hierarchy_dir, asset, name, unique_number]) - - asset_path = Path(asset_dir) - asset_path_parent = str(asset_path.parent.as_posix()) - - container_name += suffix - - up.send_request("make_directory", params=[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[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - if not up.send_request_literal( - "does_asset_exist", params=[master_level]): - up.send_request( - "new_level", params=[f"{h_dir}/{h_asset}_map"]) - - level = f"{asset_path_parent}/{asset}_map.{asset}_map" - if not up.send_request_literal( - "does_asset_exist", params=[level]): - up.send_request( - "new_level", params=[f"{asset_path_parent}/{asset}_map"]) - - up.send_request("load_level", params=[master_level]) - up.send_request("add_level_to_world", params=[level]) - up.send_request("save_all_dirty_levels") - up.send_request("load_level", params=[level]) - + def _get_sequences(self, hierarchy_dir_list, hierarchy): # TODO refactor # - Creationg of hierarchy should be a function in unreal integration # - it's used in multiple loaders but must not be loader's logic @@ -159,66 +103,147 @@ def load(self, context, name, namespace, options): sequences = [] frame_ranges = [] for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): - root_content = up.send_request_literal( - "list_assets", params=[h_dir, "False", "False"]) + root_content = send_request( + "list_assets", params={ + "directory_path": h_dir, + "recursive": False, + "include_folder": False}) - print(root_content) - - existing_sequences = up.send_request_literal( + if existing_sequences := send_request( "get_assets_of_class", - params=[root_content, "LevelSequence"]) - - print(existing_sequences) - - if not existing_sequences: + params={ + "asset_list": root_content, "class_name": "LevelSequence"}, + ): + for sequence in existing_sequences: + sequences.append(sequence) + frame_ranges.append( + send_request( + "get_sequence_frame_range", + params={"sequence_path": sequence})) + else: start_frame, end_frame, fps = self._get_frame_info(h_dir) - sequence = up.send_request( + sequence = send_request( "generate_master_sequence", - params=[h, h_dir, start_frame, end_frame, fps]) + params={ + "asset_name": h, + "asset_path": h_dir, + "start_frame": start_frame, + "end_frame": end_frame, + "fps": fps}) sequences.append(sequence) frame_ranges.append((start_frame, end_frame)) - else: - for sequence in existing_sequences: - sequences.append(sequence) - frame_ranges.append( - up.send_request_literal( - "get_sequence_frame_range", - params=[sequence])) + return sequences, frame_ranges + + @staticmethod + def _process(sequences, frame_ranges, asset, asset_dir, filename): project_name = legacy_io.active_project() data = get_asset_by_name(project_name, asset)["data"] start_frame = 0 end_frame = data.get('clipOut') - data.get('clipIn') + 1 fps = data.get("fps") - cam_sequence = up.send_request( + cam_sequence = send_request( "generate_sequence", - params=[ - f"{asset}_camera", asset_dir, start_frame, end_frame, fps]) + params={ + "asset_name": f"{asset}_camera", + "asset_path": asset_dir, + "start_frame": start_frame, + "end_frame": end_frame, + "fps": fps}) # Add sequences data to hierarchy - for i in range(0, len(sequences) - 1): - up.send_request( + for i in range(len(sequences) - 1): + send_request( "set_sequence_hierarchy", - params=[ - sequences[i], sequences[i + 1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1]]) + params={ + "parent_path": sequences[i], + "child_path": sequences[i + 1], + "child_start_frame": frame_ranges[i + 1][0], + "child_end_frame": frame_ranges[i + 1][1]}) - up.send_request( + send_request( "set_sequence_hierarchy", - params=[ - sequences[-1], cam_sequence, - data.get('clipIn'), data.get('clipOut')]) + params={ + "parent_path": sequences[-1], + "child_path": cam_sequence, + "child_start_frame": data.get('clipIn'), + "child_end_frame": data.get('clipOut')}) - up.send_request( + send_request( "import_camera", - params=[ - cam_sequence, self.fname]) + params={ + "sequence_path": cam_sequence, + "import_filename": filename}) - # Create Asset Container - up.send_request( - "create_container", params=[container_name, asset_dir]) + def load(self, context, name=None, namespace=None, 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): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. + """ + # Create directory for asset and OpenPype container + hierarchy = context.get('asset').get('data').get('parents') + root = self.root + asset = context.get('asset').get('name') + asset_name = f"{asset}_{name}" if asset else f"{name}" + + hierarchy_dir = root + hierarchy_dir_list = [] + for h in hierarchy: + hierarchy_dir = f"{hierarchy_dir}/{h}" + hierarchy_dir_list.append(hierarchy_dir) + + # Create a unique name for the camera directory + unique_number = 1 + if send_request( + "does_directory_exist", + params={"directory_path": f"{hierarchy_dir}/{asset}"}): + asset_content = send_request( + "list_assets", params={ + "directory_path": f"{root}/{asset}", + "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] + 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 = send_request( + "create_unique_asset_name", params={ + "root": hierarchy_dir, + "asset": asset, + "name": name, + "version": unique_number}) + + asset_path_parent = Path(asset_dir).parent.as_posix() + + send_request("make_directory", params={"directory_path": asset_dir}) + + master_level = self._create_levels( + hierarchy_dir_list, hierarchy, asset_path_parent, asset) + + sequences, frame_ranges = self._get_sequences( + hierarchy_dir_list, hierarchy) + + self._process(sequences, frame_ranges, asset, asset_dir, self.fname) data = { "schema": "openpype:container-2.0", @@ -232,77 +257,44 @@ def load(self, context, name, namespace, options): "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - - up.send_request("save_all_dirty_levels") - up.send_request("load_level", params=[master_level]) - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) + containerise(asset_dir, container_name, data) - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - return asset_content + send_request("save_all_dirty_levels") + send_request("load_level", params={"level_path": master_level}) def update(self, container, representation): - asset_dir = container.get("namespace") context = representation.get("context") asset = container.get('asset') + asset_dir = container.get("namespace") + filename = representation["data"]["path"] - root = "/Game/OpenPype" + root = self.root hierarchy = context.get('hierarchy').split("/") h_dir = f"{root}/{hierarchy[0]}" h_asset = hierarchy[0] master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - parent_sequence = up.send_request( - "remove_camera", params=[root, asset_dir]) - - project_name = legacy_io.active_project() - data = get_asset_by_name(project_name, asset)["data"] - start_frame = 0 - end_frame = data.get('clipOut') - data.get('clipIn') + 1 - fps = data.get("fps") - - cam_sequence = up.send_request( - "generate_sequence", - params=[ - f"{asset}_camera", asset_dir, start_frame, end_frame, fps]) - - up.send_request( - "set_sequence_hierarchy", - params=[ - parent_sequence, cam_sequence, - data.get('clipIn'), data.get('clipOut')]) - - up.send_request( - "import_camera", - params=[ - cam_sequence, str(representation["data"]["path"])]) + parent_sequence = send_request( + "remove_camera", params={ + "root": root, "asset_dir": asset_dir}) - data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - } - up.send_request( - "imprint", params=[f"{asset_dir}/{container.get('container_name')}", str(data)]) + sequences = [parent_sequence] + frame_ranges = [] - up.send_request("save_all_dirty_levels") - up.send_request("load_level", params=[master_level]) + self._process(sequences, frame_ranges, asset, asset_dir, filename) - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) + super(UnrealBaseLoader, self).update(container, representation) - up.send_request( - "save_listed_assets", params=[str(asset_content)]) + send_request("save_all_dirty_levels") + send_request("load_level", params={"level_path": master_level}) def remove(self, container): - root = "/Game/OpenPype" + root = self.root + path = container["namespace"] - up.send_request( - "remove_camera", params=[root, container.get("namespace")]) + send_request( + "remove_camera", params={ + "root": root, "asset_dir": path}) - up.send_request( - "remove_asset", params=[container.get("namespace")]) + send_request("remove_asset", params={"path": path}) diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index a135051e6ca..2993745f5b9 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- """Loader for published alembics.""" -import os from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class PointCacheAlembicLoader(plugin.Loader): +class PointCacheAlembicLoader(UnrealBaseLoader): """Load Point Cache from Alembic""" families = ["model", "pointcache"] @@ -19,48 +21,43 @@ class PointCacheAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - def _import_fbx_task( - self, filename, destination_path, destination_name, replace, - frame_start, frame_end, default_conversion + @staticmethod + def _import_abc_task( + filename, destination_path, destination_name, replace, + frame_start, frame_end, default_conversion ): - task_properties = [ - ("filename", up.format_string(filename)), - ("destination_path", up.format_string(destination_path)), - ("destination_name", up.format_string(destination_name)), - ("replace_existing", str(replace)), - ("automated", "True"), - ("save", "True") - ] - - options_properties = [ - ("import_type", "unreal.AlembicImportType.GEOMETRY_CACHE") - ] - - options_extra_properties = [ - ("geometry_cache_settings", "flatten_tracks", "False"), - ("sampling_settings", "frame_start", str(frame_start)), - ("sampling_settings", "frame_end", str(frame_end)) - ] - - if not default_conversion: - options_extra_properties.extend([ - ("conversion_settings", "preset", - "unreal.AbcConversionPreset.CUSTOM"), - ("conversion_settings", "flip_u", "False"), - ("conversion_settings", "flip_v", "True"), - ("conversion_settings", "rotation", "[0.0, 0.0, 0.0]"), - ("conversion_settings", "scale", "[1.0, 1.0, 1.0]") - ]) - - up.send_request( - "import_abc_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) - - def load(self, context, name, namespace, options): + conversion = ( + None + if default_conversion + else { + "flip_u": False, + "flip_v": True, + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0], + } + ) + + params = { + "filename": filename, + "destination_path": destination_path, + "destination_name": destination_name, + "replace_existing": replace, + "automated": True, + "save": True, + "options_properties": [ + ["import_type", "unreal.AlembicImportType.GEOMETRY_CACHE"] + ], + "sub_options_properties": [ + ["geometry_cache_settings", "flatten_tracks", "False"], + ["sampling_settings", "frame_start", str(frame_start)], + ["sampling_settings", "frame_end", str(frame_end)] + ], + "conversion_settings": conversion + } + + send_request("import_abc_task", params=params) + + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -75,35 +72,28 @@ def load(self, context, name, namespace, options): 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 - + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. """ # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + root = f"{self.root}/Assets" asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" version = context.get('version').get('name') - default_conversion = False - if options.get("default_conversion"): - default_conversion = options.get("default_conversion") + default_conversion = options.get("default_conversion") or False - asset_dir, container_name = up.send_request_literal( - "create_unique_asset_name", params=[root, asset, name, version]) + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name, + "version": version}) - container_name += suffix - - if not up.send_request_literal( - "does_directory_exist", params=[asset_dir]): - up.send_request("make_directory", params=[asset_dir]) + if not send_request( + "does_directory_exist", params={"directory_path": asset_dir}): + send_request( + "make_directory", params={"directory_path": asset_dir}) frame_start = context.get('asset').get('data').get('frameStart') frame_end = context.get('asset').get('data').get('frameEnd') @@ -113,14 +103,10 @@ def load(self, context, name, namespace, options): if frame_start == frame_end: frame_end += 1 - self._import_fbx_task( + self._import_abc_task( self.fname, asset_dir, asset_name, False, frame_start, frame_end, default_conversion) - # Create Asset Container - up.send_request( - "create_container", params=[container_name, asset_dir]) - data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, @@ -128,7 +114,7 @@ def load(self, context, name, namespace, options): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), + "loader": self.__class__.__name__, "representation": str(context["representation"]["_id"]), "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], @@ -136,46 +122,20 @@ def load(self, context, name, namespace, options): "frame_end": context["asset"]["data"]["frameEnd"], "default_conversion": default_conversion } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - return asset_content + containerise(asset_dir, container_name, data) def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] asset_name = container["asset_name"] - container_name = container['objectName'] frame_start = container["frameStart"] frame_end = container["frameStart"] default_conversion = container["default_conversion"] - self._import_fbx_task( + self._import_abc_task( filename, asset_dir, asset_name, True, frame_start, frame_end, default_conversion) - data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - def remove(self, container): - path = container["namespace"] - - up.send_request( - "remove_asset", params=[path]) + super(UnrealBaseLoader, self).update(container, representation) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index c706456832d..8aafa54599b 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -4,7 +4,11 @@ import collections from pathlib import Path -from openpype.client import get_asset_by_name, get_assets, get_representations +from openpype.client import ( + get_asset_by_name, + get_assets, + get_representations +) from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -15,11 +19,14 @@ ) from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class LayoutLoader(plugin.Loader): +class LayoutLoader(UnrealBaseLoader): """Load Layout from a JSON file""" families = ["layout"] @@ -28,197 +35,314 @@ class LayoutLoader(plugin.Loader): label = "Load Layout" icon = "code-fork" color = "orange" - ASSET_ROOT = "/Game/OpenPype" - # def _get_asset_containers(self, path): - # ar = unreal.AssetRegistryHelpers.get_asset_registry() + @staticmethod + def _create_levels( + hierarchy_dir_list, hierarchy, asset_path_parent, asset, + create_sequences_option + ): + level = f"{asset_path_parent}/{asset}_map.{asset}_map" + master_level = None + + if not send_request( + "does_asset_exist", params={"asset_path": level}): + send_request( + "new_level", + params={"level_path": f"{asset_path_parent}/{asset}_map"}) + + if create_sequences_option: + # 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 send_request( + "does_asset_exist", + params={"asset_path": master_level}): + send_request( + "new_level", + params={"level_path": f"{h_dir}/{h_asset}_map"}) + + if master_level: + send_request("load_level", + params={"level_path": master_level}) + send_request("add_level_to_world", + params={"level_path": level}) + send_request("save_all_dirty_levels") + send_request("load_level", params={"level_path": level}) + + return level, master_level + + @staticmethod + def _get_frame_info(h_dir): + project_name = legacy_io.active_project() + asset_data = get_asset_by_name( + project_name, + h_dir.split('/')[-1], + fields=["_id", "data.fps"] + ) + + start_frames = [] + end_frames = [] + + elements = list(get_assets( + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(get_assets( + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + + min_frame = min(start_frames) + max_frame = max(end_frames) + + return min_frame, max_frame, asset_data.get('data').get("fps") - # asset_content = EditorAssetLibrary.list_assets( - # path, recursive=True) + def _get_sequences(self, hierarchy_dir_list, hierarchy): + # Get all the sequences in the hierarchy. It will create them, if + # they don't exist. + sequences = [] + frame_ranges = [] + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): + root_content = send_request( + "list_assets", params={ + "directory_path": h_dir, + "recursive": False, + "include_folder": False}) + + if existing_sequences := send_request( + "get_assets_of_class", + params={ + "asset_list": root_content, + "class_name": "LevelSequence"}, + ): + for sequence in existing_sequences: + sequences.append(sequence) + frame_ranges.append( + send_request( + "get_sequence_frame_range", + params={"sequence_path": sequence})) + else: + start_frame, end_frame, fps = self._get_frame_info(h_dir) + sequence = send_request( + "generate_master_sequence", + params={ + "asset_name": h, + "asset_path": h_dir, + "start_frame": start_frame, + "end_frame": end_frame, + "fps": fps}) - # asset_containers = [] + sequences.append(sequence) + frame_ranges.append((start_frame, end_frame)) - # # 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() == 'AssetContainer': - # asset_containers.append(obj) + send_request( + "save_listed_assets", + params={"asset_list": sequences}) - # return asset_containers + return sequences, frame_ranges + + @staticmethod + def _process_sequences(sequences, frame_ranges, asset, asset_dir, level): + project_name = legacy_io.active_project() + data = get_asset_by_name(project_name, asset)["data"] + start_frame = 0 + end_frame = data.get('clipOut') - data.get('clipIn') + 1 + fps = data.get("fps") + + shot = send_request( + "generate_sequence", + params={ + "asset_name": asset, + "asset_path": asset_dir, + "start_frame": start_frame, + "end_frame": end_frame, + "fps": fps}) + + # sequences and frame_ranges have the same length + for i in range(len(sequences) - 1): + send_request( + "set_sequence_hierarchy", + params={ + "parent_path": sequences[i], + "child_path": sequences[i + 1], + "child_start_frame": frame_ranges[i + 1][0], + "child_end_frame": frame_ranges[i + 1][1]}) + send_request( + "set_sequence_visibility", + params={ + "parent_path": sequences[i], + "parent_end_frame": frame_ranges[i][1], + "child_start_frame": frame_ranges[i + 1][0], + "child_end_frame": frame_ranges[i + 1][1], + "map_paths": [level]}) + + if sequences: + send_request( + "set_sequence_hierarchy", + params={ + "parent_path": sequences[-1], + "child_path": shot, + "child_start_frame": data.get('clipIn'), + "child_end_frame": data.get('clipOut')}) + + send_request( + "set_sequence_visibility", + params={ + "parent_path": sequences[-1], + "parent_end_frame": frame_ranges[-1][1], + "child_start_frame": data.get('clipIn'), + "child_end_frame": data.get('clipOut'), + "map_paths": [level]}) + + return shot @staticmethod def _get_fbx_loader(loaders, family): name = "" - if family == 'rig': - name = "SkeletalMeshFBXLoader" + if family == 'camera': + name = "CameraLoader" 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 + elif family == 'rig': + name = "SkeletalMeshFBXLoader" + return ( + next( + ( + loader for loader in loaders if loader.__name__ == name + ), + None + ) + if name + else None + ) @staticmethod def _get_abc_loader(loaders, family): name = "" - if family == 'rig': - name = "SkeletalMeshAlembicLoader" - elif family == 'model': + if family == 'model': name = "StaticMeshAlembicLoader" + elif family == 'rig': + name = "SkeletalMeshAlembicLoader" + return ( + next( + ( + loader for loader in loaders if loader.__name__ == name + ), + None + ) + if name + else None + ) - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - def _import_animation( - self, asset_dir, path, instance_name, skeleton, actors_dict, - animation_file, bindings_dict, sequence + @staticmethod + def _import_fbx_animation( + asset_dir, path, instance_name, skeleton, animation_file ): - anim_file = Path(animation_file) - anim_file_name = anim_file.with_suffix('') + anim_file_name = Path(animation_file).with_suffix('') anim_path = f"{asset_dir}/animations/{anim_file_name}" - asset_doc = get_current_project_asset() + asset_doc = get_current_project_asset(fields=["data.fps"]) fps = asset_doc.get("data", {}).get("fps") - task_properties = [ - ("filename", up.format_string(str( - path.with_suffix(f".{animation_file}")))), - ("destination_path", up.format_string(anim_path)), - ("destination_name", up.format_string( - f"{instance_name}_animation")), - ("replace_existing", "False"), - ("automated", "True"), - ("save", "False") - ] - options_properties = [ - ("automated_import_should_detect_type", "False"), - ("original_import_type", - "unreal.FBXImportType.FBXIT_SKELETAL_MESH"), - ("mesh_type_to_import", - "unreal.FBXImportType.FBXIT_SKELETAL_MESH"), - ("original_import_type", "unreal.FBXImportType.FBXIT_ANIMATION"), - ("import_mesh", "False"), - ("import_animations", "True"), - ("override_full_name", "True"), - ("skeleton", f"get_asset({up.format_string(skeleton)})") + ["automated_import_should_detect_type", "False"], + ["original_import_type", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH"], + ["mesh_type_to_import", + "unreal.FBXImportType.FBXIT_ANIMATION"], + ["import_mesh", "False"], + ["import_animations", "True"], + ["override_full_name", "True"], + ["skeleton", f"get_asset({skeleton})"] ] - options_extra_properties = [ - ("anim_sequence_import_data", "animation_length", - "unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME"), - ("anim_sequence_import_data", - "import_meshes_in_bone_hierarchy", "False"), - ("anim_sequence_import_data", "use_default_sample_rate", "False"), - ("anim_sequence_import_data", "custom_sample_rate", str(fps)), - ("anim_sequence_import_data", "import_custom_attribute", "True"), - ("anim_sequence_import_data", "import_bone_tracks", "True"), - ("anim_sequence_import_data", "remove_redundant_keys", "False"), - ("anim_sequence_import_data", "convert_scene", "False") + sub_options_properties = [ + ["anim_sequence_import_data", "animation_length", + "unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME"], + ["anim_sequence_import_data", + "import_meshes_in_bone_hierarchy", "False"], + ["anim_sequence_import_data", "use_default_sample_rate", "False"], + ["anim_sequence_import_data", "custom_sample_rate", str(fps)], + ["anim_sequence_import_data", "import_custom_attribute", "True"], + ["anim_sequence_import_data", "import_bone_tracks", "True"], + ["anim_sequence_import_data", "remove_redundant_keys", "False"], + ["anim_sequence_import_data", "convert_scene", "True"] ] - up.send_request( - "import_fbx_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) + params = { + "filename": path.with_suffix(f".{animation_file}"), + "destination_path": anim_path, + "destination_name": f"{instance_name}_animation", + "replace_existing": False, + "automated": True, + "save": True, + "options_properties": options_properties, + "sub_options_properties": sub_options_properties + } - asset_content = up.send_request_literal( - "list_assets", params=[anim_path, "False", "False"]) + send_request("import_fbx_task", params=params) - up.send_request( - "save_listed_assets", params=[str(asset_content)]) + @staticmethod + def _process_animation( + asset_dir, instance_name, actors_dict, bindings_dict, sequence + ): + asset_content = send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) animation = None - animations = up.send_request_literal( - "get_assets_of_class", - params=[asset_content, "AnimSequence"]) - if animations: + if animations := send_request( + "get_assets_of_class", + params={"asset_list": asset_content, + "class_name": "AnimSequence"}, + ): animation = animations[0] if animation: actor = None if actors_dict.get(instance_name): - actors = up.send_request_literal( + actors = send_request( "get_assets_of_class", - params=[ - actors_dict.get(instance_name), "SkeletalMeshActor"]) + params={ + "asset_list": actors_dict.get(instance_name), + "class_name": "SkeletalMeshActor"}) assert len(actors) == 1, ( "There should be only one skeleton in the loaded assets.") actor = actors[0] - up.send_request( - "apply_animation_to_actor", params=[actor, animation]) + send_request( + "apply_animation_to_actor", + params={ + "actor_path": actor, + "animation_path": animation}) if sequence: # Add animation to the sequencer bindings = bindings_dict.get(instance_name) for binding in bindings: - up.send_request( + send_request( "add_animation_to_sequencer", - params=[sequence, binding, animation]) - - def _get_frame_info(self, h_dir): - project_name = legacy_io.active_project() - asset_data = get_asset_by_name( - project_name, - h_dir.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] + params={ + "sequence_path": sequence, + "binding_guid": binding, + "animation_path": animation}) - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - 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_repre_docs_by_version_id(self, data): + @staticmethod + def _get_repre_docs_by_version_id(data): version_ids = { element.get("version") for element in data @@ -242,7 +366,121 @@ def _get_repre_docs_by_version_id(self, data): output[version_id].append(repre_doc) return output - def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): + def _get_representation(self, element, repre_docs_by_version_id): + representation = None + repr_format = None + if element.get('representation'): + repre_docs = repre_docs_by_version_id[element.get("version")] + if not repre_docs: + self.log.error( + f"No valid representation found for version " + f"{element.get('version')}") + return None, None + repre_doc = repre_docs[0] + representation = str(repre_doc["_id"]) + repr_format = repre_doc["name"] + + # This is to keep compatibility with old versions of the + # json format. + elif element.get('reference_fbx'): + representation = element.get('reference_fbx') + repr_format = 'fbx' + elif element.get('reference_abc'): + representation = element.get('reference_abc') + repr_format = 'abc' + + return representation, repr_format + + def _load_representation( + self, family, representation, repr_format, instance_name, all_loaders + ): + 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 None, None, None, None + + assets = load_container( + loader, + representation, + namespace=instance_name) + + asset_containers = send_request( + "get_assets_of_class", + params={ + "asset_list": assets, + "class_name": "AssetContainer"}) + assert len(asset_containers) == 1, ( + "There should be only one AssetContainer in " + "the loaded assets.") + container = asset_containers[0] + + skeletons = send_request( + "get_assets_of_class", + params={ + "asset_list": assets, + "class_name": "Skeleton"}) + assert len(skeletons) <= 1, ( + "There should be one skeleton at most in " + "the loaded assets.") + skeleton = skeletons[0] if skeletons else None + return assets, container, skeleton, loader + + @staticmethod + def _process_instances( + data, element, representation, family, sequence, assets + ): + actors_dict = {} + bindings_dict = {} + + instances = [ + item for item in data + if ((item.get('version') and + item.get('version') == element.get('version')) or + item.get('reference_fbx') == representation or + item.get('reference_abc') == representation)] + + for instance in instances: + transform = str(instance.get('transform_matrix')) + basis = str(instance.get('basis')) + instance_name = instance.get('instance_name') + + if family == 'model': + send_request( + "process_family", + params={ + "assets": assets, + "class_name": 'StaticMesh', + "instance_name": instance_name, + "transform": transform, + "basis": basis, + "sequence_path": sequence}) + elif family == 'rig': + (actors, bindings) = send_request( + "process_family", + params={ + "assets": assets, + "class_name": 'SkeletalMesh', + "instance_name": instance_name, + "transform": transform, + "basis": basis, + "sequence_path": sequence}) + + actors_dict[instance_name] = actors + bindings_dict[instance_name] = bindings + + return actors_dict, bindings_dict + + def _process_assets(self, lib_path, asset_dir, sequence, repr_loaded=None): with open(lib_path, "r") as fp: data = json.load(fp) @@ -261,28 +499,8 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): repre_docs_by_version_id = self._get_repre_docs_by_version_id(data) for element in data: - representation = None - repr_format = None - if element.get('representation'): - repre_docs = repre_docs_by_version_id[element.get("version")] - if not repre_docs: - self.log.error( - f"No valid representation found for version " - f"{element.get('version')}") - continue - repre_doc = repre_docs[0] - representation = str(repre_doc["_id"]) - repr_format = repre_doc["name"] - - representation = str(repr_data.get('_id')) - # This is to keep compatibility with old versions of the - # json format. - elif element.get('reference_fbx'): - representation = element.get('reference_fbx') - repr_format = 'fbx' - elif element.get('reference_abc'): - representation = element.get('reference_abc') - repr_format = 'abc' + representation, repr_format = self._get_representation( + element, repre_docs_by_version_id) # If reference is None, this element is skipped, as it cannot be # imported in Unreal @@ -291,87 +509,24 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): instance_name = element.get('instance_name') - skeleton = None - + # Check if representation has already been loaded if representation not in repr_loaded: repr_loaded.append(representation) family = element.get('family') - 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}") - continue - - options = { - # "asset_dir": asset_dir - } - - assets = load_container( - loader, - representation, - namespace=instance_name, - options=options - ) - - container = None - - asset_containers = up.send_request_literal( - "get_assets_of_class", - params=[assets, "AssetContainer"]) - assert len(asset_containers) == 1, ( - "There should be only one AssetContainer in " - "the loaded assets.") - container = asset_containers[0] - - skeletons = up.send_request_literal( - "get_assets_of_class", - params=[assets, "Skeleton"]) - assert len(skeletons) <= 1, ( - "There should be one skeleton at most in " - "the loaded assets.") - if skeletons: - skeleton = skeletons[0] + (assets, container, + skeleton, loader) = self._load_representation( + family, representation, repr_format, instance_name, + all_loaders) loaded_assets.append(container) - instances = [ - item for item in data - if ((item.get('version') and - item.get('version') == element.get('version')) or - item.get('reference_fbx') == representation or - item.get('reference_abc') == representation)] - - for instance in instances: - # transform = instance.get('transform') - transform = str(instance.get('transform_matrix')) - basis = str(instance.get('basis')) - instance_name = instance.get('instance_name') - - actors = [] - - if family == 'model': - (actors, _) = up.send_request_literal( - "process_family", params=[ - assets, 'StaticMesh', instance_name, - transform, basis, sequence]) - elif family == 'rig': - (actors, bindings) = up.send_request_literal( - "process_family", params=[ - assets, 'SkeletalMesh', instance_name, - transform, basis, sequence]) - - actors_dict[instance_name] = actors - bindings_dict[instance_name] = bindings + new_actors, new_bindings = self._process_instances( + data, element, representation, family, sequence, assets) + + actors_dict |= new_actors + bindings_dict |= new_bindings if skeleton: skeleton_dict[representation] = skeleton @@ -381,13 +536,16 @@ def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): 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) + self._import_fbx_animation( + asset_dir, path, instance_name, skeleton, animation_file) + + self._process_animation( + asset_dir, instance_name, actors_dict, + bindings_dict, sequence) return loaded_assets - def load(self, context, name, namespace, options): + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -403,140 +561,51 @@ def load(self, context, name, namespace, options): 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 + used now, data are imprinted by `containerise()`. """ data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] + create_sequences_option = data["unreal"]["level_sequences_for_layouts"] # Create directory for asset and avalon container hierarchy = context.get('asset').get('data').get('parents') - root = self.ASSET_ROOT + root = self.root + asset = context.get('asset').get('name') + asset_name = f"{asset}_{name}" if asset else f"{name}" + hierarchy_dir = root hierarchy_dir_list = [] for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_dir_list.append(hierarchy_dir) - asset = context.get('asset').get('name') - suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else name - asset_dir, container_name = up.send_request_literal( - "create_unique_asset_name", params=[hierarchy_dir, asset, name]) + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name}) - asset_path = Path(asset_dir) - asset_path_parent = str(asset_path.parent.as_posix()) + asset_path_parent = Path(asset_dir).parent.as_posix() - container_name += suffix + send_request("make_directory", params={"directory_path": asset_dir}) - up.send_request("make_directory", params=[asset_dir]) + shot = None - master_level = None - shot = "" - sequences = [] + level, master_level = self._create_levels( + hierarchy_dir_list, hierarchy, asset_path_parent, asset, + create_sequences_option) - level = f"{asset_path_parent}/{asset}_map.{asset}_map" - if not up.send_request_literal( - "does_asset_exist", params=[level]): - up.send_request( - "new_level", params=[f"{asset_path_parent}/{asset}_map"]) + if create_sequences_option: + sequences, frame_ranges = self._get_sequences( + hierarchy_dir_list, hierarchy) - 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 up.send_request_literal( - "does_asset_exist", params=[master_level]): - up.send_request( - "new_level", params=[f"{h_dir}/{h_asset}_map"]) + shot = self._process_sequences( + sequences, frame_ranges, asset, asset_dir, level) - if master_level: - up.send_request("load_level", params=[master_level]) - up.send_request("add_level_to_world", params=[level]) - up.send_request("save_all_dirty_levels") - up.send_request("load_level", params=[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 = up.send_request_literal( - "list_assets", params=[h_dir, "False", "False"]) - - existing_sequences = up.send_request_literal( - "get_assets_of_class", - params=[root_content, "LevelSequence"]) + send_request("load_level", params={"level_path": level}) - if not existing_sequences: - start_frame, end_frame, fps = self._get_frame_info(h_dir) - sequence = up.send_request( - "generate_master_sequence", - params=[h, h_dir, start_frame, end_frame, fps]) + loaded_assets = self._process_assets(self.fname, asset_dir, shot) - sequences.append(sequence) - frame_ranges.append((start_frame, end_frame)) - else: - for sequence in existing_sequences: - sequences.append(sequence) - frame_range = up.send_request_literal( - "get_sequence_frame_range", - params=[sequence]) - frame_ranges.append(frame_range) - - project_name = legacy_io.active_project() - data = get_asset_by_name(project_name, asset)["data"] - shot_start_frame = 0 - shot_end_frame = data.get('clipOut') - data.get('clipIn') + 1 - fps = data.get("fps") - - shot = up.send_request( - "generate_sequence", - params=[ - asset, asset_dir, shot_start_frame, shot_end_frame, fps]) - - # sequences and frame_ranges have the same length - for i in range(0, len(sequences) - 1): - up.send_request( - "set_sequence_hierarchy", - params=[ - sequences[i], sequences[i + 1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1]]) - up.send_request( - "set_sequence_visibility", - params=[ - sequences[i], frame_ranges[i][1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1], - str([level])]) - - if sequences: - up.send_request( - "set_sequence_hierarchy", - params=[ - sequences[-1], shot, - data.get('clipIn'), data.get('clipOut')]) - up.send_request( - "set_sequence_visibility", - params=[ - sequences[-1], frame_ranges[-1][1], - data.get('clipIn'), data.get('clipOut'), - str([level])]) - - up.send_request("load_level", params=[level]) - - loaded_assets = self._process(self.fname, asset_dir, shot) - - up.send_request("save_listed_assets", params=[str(sequences)]) - - up.send_request("save_current_level") - - # Create Asset Container - up.send_request( - "create_container", params=[container_name, asset_dir]) + send_request("save_current_level") data = { "schema": "openpype:container-2.0", @@ -551,56 +620,58 @@ def load(self, context, name, namespace, options): "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "False"]) - up.send_request( - "save_listed_assets", params=[str(asset_content)]) + containerise(asset_dir, container_name, data) if master_level: - up.send_request("load_level", params=[master_level]) + send_request("load_level", params={"level_path": master_level}) + + @staticmethod + def _remove_bound_assets(asset_dir): + parent_path = Path(asset_dir).parent.as_posix() - return asset_content + layout_level = send_request( + "get_first_asset_of_class", + params={ + "class_name": "World", + "path": parent_path, + "recursive": False}) + + send_request("load_level", params={"level_path": layout_level}) + + layout_sequence = send_request( + "get_first_asset_of_class", + params={ + "class_name": "LevelSequence", + "path": asset_dir, + "recursive": False}) + + send_request( + "delete_all_bound_assets", + params={"level_sequence_path": layout_sequence}) def update(self, container, representation): - root = "/Game/OpenPype" + root = self.root asset_dir = container.get('namespace') + container_name = container['objectName'] context = representation.get("context") data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] + create_sequences_option = data["unreal"]["level_sequences_for_layouts"] master_level = None - prev_level = None - layout_sequence = "" + layout_sequence = None - if create_sequences: + if create_sequences_option: hierarchy = context.get('hierarchy').split("/") h_dir = f"{root}/{hierarchy[0]}" h_asset = hierarchy[0] master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - parent_path = Path(asset_dir).parent.as_posix() - - layout_level = up.send_request( - "get_first_asset_of_class", - params=["World", parent_path, "False"]) - - up.send_request("load_level", params=[layout_level]) - - layout_sequence = up.send_request( - "get_first_asset_of_class", - params=["LevelSequence", asset_dir, "False"]) - - up.send_request( - "delete_all_bound_assets", params=[layout_sequence]) - - if not master_level: - prev_level = up.send_request("get_current_level") + self._remove_bound_assets(asset_dir) + prev_level = None if master_level else send_request( + "get_current_level") source_path = get_representation_path(representation) loaded_assets = self._process(source_path, asset_dir, layout_sequence) @@ -610,40 +681,36 @@ def update(self, container, representation): "parent": str(representation["parent"]), "loaded_assets": loaded_assets } - up.send_request( - "imprint", params=[ - f"{asset_dir}/{container.get('container_name')}", str(data)]) - up.send_request("save_current_level") + containerise(asset_dir, container_name, data) - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "False"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) + send_request("save_current_level") if master_level: - up.send_request("load_level", params=[master_level]) + send_request("load_level", params={"level_path": master_level}) elif prev_level: - up.send_request("load_level", params=[prev_level]) - + send_request("load_level", params={"level_path": prev_level}) 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. """ - root = "/Game/OpenPype" + root = self.root asset = container.get('asset') asset_dir = container.get('namespace') asset_name = container.get('asset_name') loaded_assets = container.get('loaded_assets') data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] + create_sequences_option = data["unreal"]["level_sequences_for_layouts"] - up.send_request( + send_request( "remove_layout", - params=[ - root, asset, asset_dir, asset_name, loaded_assets, - "True" if create_sequences else "False"]) + params={ + "root": root, + "asset": asset, + "asset_dir": asset_dir, + "asset_name": asset_name, + "loaded_asset": loaded_assets, + "create_sequences": create_sequences_option}) diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index e316d255e9d..2bfe7f2ac0a 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- """Load Skeletal Mesh alembics.""" -import os from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class SkeletalMeshAlembicLoader(plugin.Loader): +class SkeletalMeshAlembicLoader(UnrealBaseLoader): """Load Unreal SkeletalMesh from Alembic""" families = ["pointcache", "skeletalMesh"] @@ -20,35 +21,38 @@ class SkeletalMeshAlembicLoader(plugin.Loader): 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) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - 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): + @staticmethod + def _import_abc_task( + filename, destination_path, destination_name, replace, + default_conversion + ): + conversion = ( + None + if default_conversion + else { + "flip_u": False, + "flip_v": False, + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0], + } + ) + + params = { + "filename": filename, + "destination_path": destination_path, + "destination_name": destination_name, + "replace_existing": replace, + "automated": True, + "save": True, + "options_properties": [ + ['import_type', 'unreal.AlembicImportType.SKELETAL'] + ], + "conversion_settings": conversion + } + + send_request("import_abc_task", params=params) + + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -63,39 +67,34 @@ def load(self, context, name, namespace, data): 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 + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. """ # Create directory for asset and openpype container - root = "/Game/OpenPype/Assets" + root = f"{self.root}/Assets" + if options and options.get("asset_dir"): + root = options["asset_dir"] asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" version = context.get('version').get('name') - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") - - container_name += suffix + default_conversion = options.get("default_conversion") or False - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name, + "version": version}) - task = self.get_task(self.fname, asset_dir, asset_name, False) + if not send_request( + "does_directory_exist", params={"directory_path": asset_dir}): + send_request( + "make_directory", params={"directory_path": asset_dir}) - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + self._import_abc_task( + self.fname, asset_dir, asset_name, False, default_conversion) data = { "schema": "openpype:container-2.0", @@ -104,58 +103,23 @@ def load(self, context, name, namespace, data): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "loader": self.__class__.__name__, + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "default_conversion": default_conversion } - unreal_pipeline.imprint( - "{}/{}".format(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 + containerise(asset_dir, container_name, data) def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - 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 = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) + default_conversion = container["default_conversion"] - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + self._import_abc_task( + filename, asset_dir, asset_name, True, default_conversion) - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + super(UnrealBaseLoader, self).update(container, representation) diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 27e9419e89c..aae62ca1778 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- """Load Skeletal Meshes form FBX.""" -import os from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class SkeletalMeshFBXLoader(plugin.Loader): +class SkeletalMeshFBXLoader(UnrealBaseLoader): """Load Unreal SkeletalMesh from FBX.""" families = ["rig", "skeletalMesh"] @@ -19,51 +21,44 @@ class SkeletalMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" + @staticmethod def _import_fbx_task( - self, filename, destination_path, destination_name, replace): - task_properties = [ - ("filename", up.format_string(filename)), - ("destination_path", up.format_string(destination_path)), - ("destination_name", up.format_string(destination_name)), - ("replace_existing", str(replace)), - ("automated", "True"), - ("save", "True") - ] - - options_properties = [ - ("import_as_skeletal", "True"), - ("import_animations", "False"), - ("import_mesh", "True"), - ("import_materials", "False"), - ("import_textures", "False"), - ("skeleton", "None"), - ("create_physics_asset", "False"), - ("mesh_type_to_import", - "unreal.FBXImportType.FBXIT_SKELETAL_MESH") - ] - - options_extra_properties = [ - ( - "skeletal_mesh_import_data", - "import_content_type", - "unreal.FBXImportContentType.FBXICT_ALL" - ), - ( - "skeletal_mesh_import_data", - "normal_import_method", - "unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS" - ) - ] - - up.send_request( - "import_fbx_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) - - def load(self, context, name, namespace, options): + filename, destination_path, destination_name, replace + ): + params = { + "filename": filename, + "destination_path": destination_path, + "destination_name": destination_name, + "replace_existing": replace, + "automated": True, + "save": True, + "options_properties": [ + ["import_animations", "False"], + ["import_mesh", "True"], + ["import_materials", "False"], + ["import_textures", "False"], + ["skeleton", "None"], + ["create_physics_asset", "False"], + ["mesh_type_to_import", + "unreal.FBXImportType.FBXIT_SKELETAL_MESH"] + ], + "sub_options_properties": [ + [ + "skeletal_mesh_import_data", + "import_content_type", + "unreal.FBXImportContentType.FBXICT_ALL" + ], + [ + "skeletal_mesh_import_data", + "normal_import_method", + "unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS" + ] + ] + } + + send_request("import_fbx_task", params=params) + + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -79,39 +74,30 @@ def load(self, context, name, namespace, options): 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 - + used now, data are imprinted by `containerise()`. """ # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + root = f"{self.root}/Assets" if options and options.get("asset_dir"): root = options["asset_dir"] asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" version = context.get('version').get('name') - asset_dir, container_name = up.send_request_literal( - "create_unique_asset_name", params=[root, asset, name, version]) + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name, + "version": version}) - container_name += suffix - - if not up.send_request_literal( - "does_directory_exist", params=[asset_dir]): - up.send_request("make_directory", params=[asset_dir]) + if not send_request( + "does_directory_exist", params={"directory_path": asset_dir}): + send_request( + "make_directory", params={"directory_path": asset_dir}) self._import_fbx_task(self.fname, asset_dir, asset_name, False) - # Create Asset Container - up.send_request( - "create_container", params=[container_name, asset_dir]) - data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, @@ -119,45 +105,19 @@ def load(self, context, name, namespace, options): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), + "loader": self.__class__.__name__, "representation": str(context["representation"]["_id"]), "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - return asset_content + containerise(asset_dir, container_name, data) def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] asset_name = container["asset_name"] - container_name = container['objectName'] self._import_fbx_task(filename, asset_dir, asset_name, True) - data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - def remove(self, container): - path = container["namespace"] - - up.send_request( - "remove_asset", params=[path]) + super(UnrealBaseLoader, self).update(container, representation) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index c7841cef53a..d2fd24b01a7 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- """Loader for Static Mesh alembics.""" -import os from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class StaticMeshAlembicLoader(plugin.Loader): +class StaticMeshAlembicLoader(UnrealBaseLoader): """Load Unreal StaticMesh from Alembic""" families = ["model", "staticMesh"] @@ -21,39 +22,40 @@ class StaticMeshAlembicLoader(plugin.Loader): color = "orange" @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 load(self, context, name, namespace, options): + def _import_abc_task( + filename, destination_path, destination_name, replace, + default_conversion + ): + conversion = ( + None + if default_conversion + else { + "flip_u": False, + "flip_v": False, + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0], + } + ) + + params = { + "filename": filename, + "destination_path": destination_path, + "destination_name": destination_name, + "replace_existing": replace, + "automated": True, + "save": True, + "options_properties": [ + ['import_type', 'unreal.AlembicImportType.STATIC_MESH'] + ], + "sub_options_properties": [ + ["static_mesh_settings", "merge_meshes", "True"] + ], + "conversion_settings": conversion + } + + send_request("import_abc_task", params=params) + + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -68,45 +70,34 @@ def load(self, context, name, namespace, options): 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 - + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. """ # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + root = f"{self.root}/Assets" + if options and options.get("asset_dir"): + root = options["asset_dir"] asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" version = context.get('version').get('name') - 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"{root}/{asset}/{name}_v{version:03d}", suffix="") + default_conversion = options.get("default_conversion") or False - container_name += suffix + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name, + "version": version}) - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) + if not send_request( + "does_directory_exist", params={"directory_path": asset_dir}): + send_request( + "make_directory", params={"directory_path": asset_dir}) - task = self.get_task( + self._import_abc_task( self.fname, asset_dir, asset_name, False, default_conversion) - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, @@ -114,59 +105,23 @@ def load(self, context, name, namespace, options): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "loader": self.__class__.__name__, + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "default_conversion": default_conversion } - unreal_pipeline.imprint( - "{}/{}".format(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 + containerise(asset_dir, container_name, data) def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - 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 = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + filename = get_representation_path(representation) + asset_dir = container["namespace"] + asset_name = container["asset_name"] - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) + default_conversion = container["default_conversion"] - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + self._import_abc_task( + filename, asset_dir, asset_name, True, default_conversion) - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + super(UnrealBaseLoader, self).update(container, representation) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index ed6fabef5e7..5746b000eb0 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- """Load Static meshes form FBX.""" -import os from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as up +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class StaticMeshFBXLoader(plugin.Loader): +class StaticMeshFBXLoader(UnrealBaseLoader): """Load Unreal StaticMesh from FBX.""" families = ["model", "staticMesh"] @@ -19,36 +21,30 @@ class StaticMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" + @staticmethod def _import_fbx_task( - self, filename, destination_path, destination_name, replace): - task_properties = [ - ("filename", up.format_string(filename)), - ("destination_path", up.format_string(destination_path)), - ("destination_name", up.format_string(destination_name)), - ("replace_existing", str(replace)), - ("automated", "True"), - ("save", "True") - ] - - options_properties = [ - ("automated_import_should_detect_type", "False"), - ("import_animations", "False") - ] - - options_extra_properties = [ - ("static_mesh_import_data", "combine_meshes", "True"), - ("static_mesh_import_data", "remove_degenerates", "False") - ] - - up.send_request( - "import_fbx_task", - params=[ - str(task_properties), - str(options_properties), - str(options_extra_properties) - ]) - - def load(self, context, name, namespace, options): + filename, destination_path, destination_name, replace + ): + params = { + "filename": filename, + "destination_path": destination_path, + "destination_name": destination_name, + "replace_existing": replace, + "automated": True, + "save": True, + "options_properties": [ + ["automated_import_should_detect_type", "False"], + ["import_animations", "False"] + ], + "sub_options_properties": [ + ["static_mesh_import_data", "combine_meshes", "True"], + ["static_mesh_import_data", "remove_degenerates", "False"] + ] + } + + send_request("import_fbx_task", params=params) + + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -64,39 +60,30 @@ def load(self, context, name, namespace, options): 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 + used now, data are imprinted by `containerise()`. """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + root = f"{self.root}/Assets" if options and options.get("asset_dir"): root = options["asset_dir"] asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" version = context.get('version').get('name') - asset_dir, container_name = up.send_request_literal( - "create_unique_asset_name", params=[root, asset, name, version]) + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name, + "version": version}) - container_name += suffix - - if not up.send_request_literal( - "does_directory_exist", params=[asset_dir]): - up.send_request("make_directory", params=[asset_dir]) + if not send_request( + "does_directory_exist", params={"directory_path": asset_dir}): + send_request( + "make_directory", params={"directory_path": asset_dir}) self._import_fbx_task(self.fname, asset_dir, asset_name, False) - # Create Asset Container - up.send_request( - "create_container", params=[container_name, asset_dir]) - data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, @@ -104,45 +91,19 @@ def load(self, context, name, namespace, options): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), + "loader": self.__class__.__name__, "representation": str(context["representation"]["_id"]), "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - return asset_content + containerise(asset_dir, container_name, data) def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] asset_name = container["asset_name"] - container_name = container['objectName'] self._import_fbx_task(filename, asset_dir, asset_name, True) - data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - } - up.send_request( - "imprint", params=[f"{asset_dir}/{container_name}", str(data)]) - - asset_content = up.send_request_literal( - "list_assets", params=[asset_dir, "True", "True"]) - - up.send_request( - "save_listed_assets", params=[str(asset_content)]) - - def remove(self, container): - path = container["namespace"] - - up.send_request( - "remove_asset", params=[path]) + super(UnrealBaseLoader, self).update(container, representation) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index eccfc7b445d..71dbc5231e5 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -7,12 +7,14 @@ get_representation_path, AVALON_CONTAINER_ID ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class UAssetLoader(plugin.Loader): +class UAssetLoader(UnrealBaseLoader): """Load UAsset.""" families = ["uasset"] @@ -21,7 +23,7 @@ class UAssetLoader(plugin.Loader): icon = "cube" color = "orange" - def load(self, context, name, namespace, options): + def load(self, context, name=None, namespace=None, options=None): """Load and containerise representation into Content Browser. Args: @@ -32,39 +34,28 @@ def load(self, context, name, namespace, options): 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 + used now, data are imprinted by `containerise()`. """ # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + root = f"{self.root}/Assets" asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + asset_dir, container_name = send_request( + "create_unique_asset_name", params={ + "root": root, + "asset": asset, + "name": name}) - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) + send_request( + "make_directory", params={"directory_path": asset_dir}) + project_content_dir = send_request("project_content_dir") destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + "/Game", Path(project_content_dir).as_posix(), 1) - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + shutil.copy(self.fname, f"{destination_path}/{asset_name}.uasset") data = { "schema": "openpype:container-2.0", @@ -73,73 +64,23 @@ def load(self, context, name, namespace, options): "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "loader": self.__class__.__name__, + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(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 + containerise(asset_dir, container_name, data) def update(self, container, representation): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - + filename = get_representation_path(representation) asset_dir = container["namespace"] - name = representation["context"]["subset"] + asset_name = container["asset_name"] + project_content_dir = send_request("project_content_dir") 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 not obj.get_class().get_name() == 'AssetContainer': - unreal.EditorAssetLibrary.delete_asset(asset) - - update_filepath = get_representation_path(representation) - - shutil.copy(update_filepath, f"{destination_path}/{name}.uasset") - - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - 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) + "/Game", Path(project_content_dir).as_posix(), 1) - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) + shutil.copy(filename, f"{destination_path}/{asset_name}.uasset") - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + super(UnrealBaseLoader, self).update(container, representation) From 5d8bfeb72cfbe62129db1ff4f90ececbf9e808e6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 30 Mar 2023 16:24:01 +0100 Subject: [PATCH 39/55] Updated layout existing loader --- .../OpenPype/Content/Python/init_unreal.py | 8 + .../UE_5.0/OpenPype/Content/Python/plugin.py | 148 ++++++ .../plugins/load/load_layout_existing.py | 432 +++++++----------- 3 files changed, 319 insertions(+), 269 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py index ec643f4cff8..52dcb3a7556 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py @@ -54,6 +54,10 @@ delete_all_bound_assets, remove_camera, remove_layout, + match_actor, + spawn_existing_actors, + spawn_actors, + remove_unmatched_actors, ) __all__ = [ @@ -106,4 +110,8 @@ "delete_all_bound_assets", "remove_camera", "remove_layout", + "match_actor", + "spawn_existing_actors", + "spawn_actors", + "remove_unmatched_actors", ] diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py index 6d3ef68b661..1e5491e3327 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py @@ -1094,3 +1094,151 @@ def remove_layout(params): if create_sequences: unreal.EditorLevelLibrary.load_level(master_level) unreal.EditorAssetLibrary.delete_directory(f"{root}/tmp") + + +def match_actor(params): + """ + Match existing actors in the scene to the layout that is being loaded. + It will create a container for each of them, and apply the transformations + from the layout. + + Args: + params (str): string containing a dictionary with parameters: + actors_matched (list): list of actors already matched + lasset (dict): dictionary containing the layout asset + repr_data (dict): dictionary containing the representation + """ + actors_matched, lasset, repr_data = get_params( + params, 'actors_matched', 'lasset', 'repr_data') + + actors = unreal.EditorLevelLibrary.get_all_level_actors() + + for actor in actors: + if 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 repr_data.get('data').get('path')): + continue + + actor.set_actor_label(lasset.get('instance_name')) + + mesh_path = Path(mesh.get_path_name()).parent.as_posix() + + # Set the transform for the actor. + basis_data = lasset.get('basis') + transform_data = lasset.get('transform_matrix') + transform = _get_transform(import_data, basis_data, transform_data) + + actor.set_actor_transform(transform, False, True) + + return True, mesh_path + + return False, None + + +def _spawn_actor(obj, lasset): + actor = unreal.EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0) + ) + + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property('static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property('asset_import_data') + + basis_data = lasset.get('basis') + transform_data = lasset.get('transform_matrix') + transform = _get_transform(import_data, basis_data, transform_data) + + actor.set_actor_transform(transform, False, True) + + +def spawn_existing_actors(params): + """ + Spawn actors that have already been loaded from the layout asset. + + Args: + params (str): string containing a dictionary with parameters: + repr_data (dict): dictionary containing the representation + lasset (dict): dictionary containing the layout asset + """ + repr_data, lasset = get_params(params, 'repr_data', 'lasset') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + all_containers = ls() + + for container in all_containers: + representation = container.get('representation') + + if representation != str(repr_data.get('_id')): + continue + + asset_dir = container.get('namespace') + + _filter = unreal.ARFilter( + class_names=["StaticMesh"], + package_paths=[asset_dir], + recursive_paths=False) + assets = ar.get_assets(_filter) + + for asset in assets: + obj = asset.get_asset() + _spawn_actor(obj, lasset) + + return True + + return False + + +def spawn_actors(params): + """ + Spawn actors from a list of assets. + + Args: + params (str): string containing a dictionary with parameters: + lasset (dict): dictionary containing the layout asset + repr_data (dict): dictionary containing the representation + """ + assets, lasset = get_params(params, 'assets', 'lasset') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for asset in assets: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() != 'StaticMesh': + continue + _spawn_actor(obj, lasset) + + return True + + +def remove_unmatched_actors(params): + """ + Remove actors that have not been matched to the layout. + + Args: + params (str): string containing a dictionary with parameters: + actors_matched (list): list of actors already matched + """ + actors_matched = get_params(params, 'actors_matched') + + actors = unreal.EditorLevelLibrary.get_all_level_actors() + + for actor in actors: + if actor.get_class().get_name() != 'StaticMeshActor': + continue + if actor not in actors_matched: + unreal.log_warning(f"Actor {actor.get_name()} not matched.") + unreal.EditorLevelLibrary.destroy_actor(actor) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 092b273dedc..e9a6a96837a 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -1,9 +1,8 @@ +# -*- coding: utf-8 -*- +"""Loader for apply layout to already existing assets.""" import json from pathlib import Path -import unreal -from unreal import EditorLevelLibrary - from openpype.client import get_representations from openpype.pipeline import ( discover_loader_plugins, @@ -13,11 +12,14 @@ AVALON_CONTAINER_ID, legacy_io, ) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as upipeline +from openpype.hosts.unreal.api.plugin import UnrealBaseLoader +from openpype.hosts.unreal.api.pipeline import ( + send_request, + containerise, +) -class ExistingLayoutLoader(plugin.Loader): +class ExistingLayoutLoader(UnrealBaseLoader): """ Load Layout for an existing scene, and match the existing assets. """ @@ -28,7 +30,6 @@ class ExistingLayoutLoader(plugin.Loader): label = "Load Layout on Existing Scene" icon = "code-fork" color = "orange" - ASSET_ROOT = "/Game/OpenPype" delete_unmatched_assets = True @@ -41,23 +42,11 @@ def apply_settings(cls, project_settings, *args, **kwargs): project_settings["unreal"]["delete_unmatched_assets"] ) - @staticmethod def _create_container( - asset_name, asset_dir, asset, representation, parent, family + self, asset_name, asset_dir, asset, representation, parent, family ): container_name = f"{asset_name}_CON" - container = None - 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": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, @@ -65,132 +54,82 @@ def _create_container( "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, - # "loader": str(self.__class__.__name__), + "loader": self.__class__.__name__, "representation": representation, "parent": parent, "family": family } - upipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + container = containerise(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 _get_transform(self, ext, import_data, lasset): - conversion = unreal.Matrix.IDENTITY.transform() - fbx_tuning = unreal.Matrix.IDENTITY.transform() - - basis = unreal.Matrix( - lasset.get('basis')[0], - lasset.get('basis')[1], - lasset.get('basis')[2], - lasset.get('basis')[3] - ).transform() - transform = unreal.Matrix( - lasset.get('transform_matrix')[0], - lasset.get('transform_matrix')[1], - lasset.get('transform_matrix')[2], - lasset.get('transform_matrix')[3] - ).transform() - - # Check for the conversion settings. We cannot access - # the alembic conversion settings, so we assume that - # the maya ones have been applied. - if ext == '.fbx': - loc = import_data.import_translation - rot = import_data.import_rotation.to_vector() - scale = import_data.import_uniform_scale - conversion = unreal.Transform( - location=[loc.x, loc.y, loc.z], - rotation=[rot.x, rot.y, rot.z], - scale=[-scale, scale, scale] - ) - fbx_tuning = unreal.Transform( - rotation=[180.0, 0.0, 90.0], - scale=[1.0, 1.0, 1.0] - ) - elif ext == '.abc': - # This is the standard conversion settings for - # alembic files from Maya. - conversion = unreal.Transform( - location=[0.0, 0.0, 0.0], - rotation=[0.0, 0.0, 0.0], - scale=[1.0, -1.0, 1.0] - ) - - new_transform = (basis.inverse() * transform * basis) - return fbx_tuning * conversion.inverse() * new_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')) - 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) - - transform = self._get_transform( - path.suffix, import_data, lasset) - - actor.set_actor_transform(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': + if family == 'camera': name = "CameraLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None + elif family == 'model': + name = "StaticMeshFBXLoader" + elif family == 'rig': + name = "SkeletalMeshFBXLoader" + return ( + next( + ( + loader for loader in loaders if loader.__name__ == name + ), + None + ) + if name + else None + ) @staticmethod def _get_abc_loader(loaders, family): name = "" - if family == 'rig': - name = "SkeletalMeshAlembicLoader" - elif family == 'model': + if family == 'model': name = "StaticMeshAlembicLoader" + elif family == 'rig': + name = "SkeletalMeshAlembicLoader" + return ( + next( + ( + loader for loader in loaders if loader.__name__ == name + ), + None + ) + if name + else None + ) - 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() + def _get_representation(self, element, repre_docs_by_version_id): + representation = None + repr_format = None + if element.get('representation'): + repre_docs = repre_docs_by_version_id[element.get("version")] + if not repre_docs: + self.log.error( + f"No valid representation found for version " + f"{element.get('version')}") + return None, None + repre_doc = repre_docs[0] + representation = str(repre_doc["_id"]) + repr_format = repre_doc["name"] + + # This is to keep compatibility with old versions of the + # json format. + elif element.get('reference_fbx'): + representation = element.get('reference_fbx') + repr_format = 'fbx' + elif element.get('reference_abc'): + representation = element.get('reference_abc') + repr_format = 'abc' + + return representation, repr_format + + def _load_representation( + self, family, representation, repr_format, instance_name, all_loaders + ): loaders = loaders_from_representation( all_loaders, representation) @@ -202,27 +141,14 @@ def _load_asset(self, repr_data, representation, instance_name, family): loader = self._get_abc_loader(loaders, family) if not loader: - self.log.error(f"No valid loader found for {representation}") + 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 load_container(loader, representation, namespace=instance_name) - return assets - - def _get_valid_repre_docs(self, project_name, version_ids): + @staticmethod + def _get_valid_repre_docs(project_name, version_ids): valid_formats = ['fbx', 'abc'] repre_docs = list(get_representations( @@ -230,28 +156,20 @@ def _get_valid_repre_docs(self, project_name, version_ids): representation_names=valid_formats, version_ids=version_ids )) - repre_doc_by_version_id = {} - for repre_doc in repre_docs: - version_id = str(repre_doc["parent"]) - repre_doc_by_version_id[version_id] = repre_doc - return repre_doc_by_version_id - def _process(self, lib_path, project_name): - ar = unreal.AssetRegistryHelpers.get_asset_registry() + return { + str(repre_doc["parent"]): repre_doc for repre_doc in repre_docs} - actors = EditorLevelLibrary.get_all_level_actors() - - with open(lib_path, "r") as fp: - data = json.load(fp) - - elements = [] + @staticmethod + def _get_layout_data(data, project_name): + assets = [] 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: + for asset in data: + if repre_id := asset.get('representation'): repre_ids.add(repre_id) - elements.append(element) + assets.append(asset) repre_docs = get_representations( project_name, representation_ids=repre_ids @@ -260,56 +178,56 @@ def _process(self, lib_path, project_name): str(repre_doc["_id"]): repre_doc for repre_doc in repre_docs } + layout_data = [] version_ids = set() - for element in elements: - repre_id = element.get("representation") + for asset in assets: + repre_id = asset.get("representation") repre_doc = repre_docs_by_id.get(repre_id) if not repre_doc: raise AssertionError("Representation not found") - if not (repre_doc.get('data') or repre_doc['data'].get('path')): + if not repre_doc.get('data') and not repre_doc['data'].get('path'): raise AssertionError("Representation does not have path") if not repre_doc.get('context'): raise AssertionError("Representation does not have context") - layout_data.append((repre_doc, element)) + layout_data.append((repre_doc, asset)) version_ids.add(repre_doc["parent"]) + return layout_data, version_ids + + def _process(self, lib_path, project_name): + with open(lib_path, "r") as fp: + data = json.load(fp) + + all_loaders = discover_loader_plugins() + + layout_data, version_ids = self._get_layout_data(data, project_name) + # Prequery valid repre documents for all elements at once valid_repre_doc_by_version_id = self._get_valid_repre_docs( project_name, version_ids) + containers = [] actors_matched = [] for (repr_data, 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. + # 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 - - 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 repr_data.get('data').get('path')): - continue - actor.set_actor_label(lasset.get('instance_name')) + matched, mesh_path = send_request( + "match_actor", + params={ + "actors_matched": actors_matched, + "lasset": lasset, + "repr_data": repr_data}) - mesh_path = Path(mesh.get_path_name()).parent.as_posix() - - # Create the container for the asset. + # 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 matched: asset = repr_data.get('context').get('asset') subset = repr_data.get('context').get('subset') container = self._create_container( @@ -319,86 +237,67 @@ def _process(self, lib_path, project_name): ) containers.append(container) - # Set the transform for the actor. - transform = self._get_transform( - path.suffix, import_data, lasset) - actor.set_actor_transform(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: - repr = container.get('representation') - - if not repr == str(repr_data.get('_id')): - continue - - asset_dir = container.get('namespace') - - filter = unreal.ARFilter( - class_names=["StaticMesh"], - package_paths=[asset_dir], - recursive_paths=False) - assets = ar.get_assets(filter) + loaded = send_request( + "spawn_actors", + params={ + "repr_data": repr_data, + "lasset": lasset}) - 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: + # The asset was already loaded, and we spawned it in the scene, + # so we can continue. continue - assets = self._load_asset( - valid_repre_doc_by_version_id.get(lasset.get('version')), - lasset.get('representation'), - lasset.get('instance_name'), - lasset.get('family') - ) + # If we get here, it means that the asset was not loaded yet, + # so we load it and spawn it in the scene. + representation, repr_format = self._get_representation( + lasset, valid_repre_doc_by_version_id) - 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) + family = lasset.get('family') + instance_name = lasset.get('instance_name') - break + assets = self._load_representation( + family, representation, repr_format, instance_name, + all_loaders) - # 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) + send_request( + "spawn_actors", + params={ + "assets": assets, "lasset": lasset}) - return containers + # Remove not matched actors, if the option is set. + if self.delete_unmatched_assets: + send_request( + "remove_unmatched_actors", + params={"actors_matched": actors_matched}) - def load(self, context, name, namespace, options): - print("Loading Layout and Match Assets") + return containers + def load(self, context, name=None, namespace=None, options=None): + """Load and containerise representation into Content Browser. + + Load and apply layout to already existing assets in Unreal. + It will create a container for each asset in the scene, and a + container for the layout. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. + """ asset = context.get('asset').get('name') asset_name = f"{asset}_{name}" if asset else name + container_name = f"{asset}_{name}_CON" - curr_level = self._get_current_level() + curr_level = send_request("get_current_level") if not curr_level: raise AssertionError("Current level not saved") @@ -406,14 +305,7 @@ def load(self, context, name, namespace, options): project_name = context["project"]["name"] containers = self._process(self.fname, 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) + curr_level_path = Path(curr_level).parent.as_posix() data = { "schema": "openpype:container-2.0", @@ -423,15 +315,17 @@ def load(self, context, name, namespace, options): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": str(context["representation"]["_id"]), + "parent": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], "loaded_assets": containers } - upipeline.imprint(f"{curr_level_path}/{container_name}", data) + + containerise(curr_level_path, container_name, data) def update(self, container, representation): asset_dir = container.get('namespace') + container_name = container['objectName'] source_path = get_representation_path(representation) project_name = legacy_io.active_project() @@ -442,5 +336,5 @@ def update(self, container, representation): "parent": str(representation["parent"]), "loaded_assets": containers } - upipeline.imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) + + containerise(asset_dir, container_name, data) From 0665145d0ed43d7005b84a53d82a5c96c6162a0a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Apr 2023 10:13:09 +0100 Subject: [PATCH 40/55] Fixed Loader import --- openpype/hosts/unreal/api/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index df01de1cfb7..82b2b2412e8 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -4,7 +4,7 @@ from .plugin import ( UnrealActorCreator, UnrealAssetCreator, - Loader + UnrealBaseLoader, ) from .pipeline import ( @@ -14,12 +14,12 @@ ls, ls_inst, publish, - containerise, show_creator, show_loader, show_publisher, show_manager, show_experimental_tools, + containerise, instantiate, UnrealHost, maintained_selection @@ -28,17 +28,19 @@ __all__ = [ "install", "uninstall", - "Loader", + "UnrealActorCreator", + "UnrealAssetCreator", + "UnrealBaseLoader", "imprint", "ls", "ls_inst", "publish", - "containerise", "show_creator", "show_loader", "show_publisher", "show_manager", "show_experimental_tools", + "containerise", "instantiate", "UnrealHost", "maintained_selection" From 049228498efb51039d2b0eb68cf88dd208fb8a17 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 May 2023 11:33:09 +0100 Subject: [PATCH 41/55] Updated creation for renders --- .../OpenPype/Content/Python/init_unreal.py | 2 + .../UE_5.0/OpenPype/Content/Python/plugin.py | 97 ++++++++++++++- .../unreal/plugins/create/create_render.py | 111 +++--------------- 3 files changed, 113 insertions(+), 97 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py index 52dcb3a7556..231cb6d4403 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py @@ -29,6 +29,7 @@ from plugin import ( create_look, + create_render, create_unique_asset_name, add_level_to_world, list_assets, @@ -85,6 +86,7 @@ "containerise", "instantiate", "create_look", + "create_render", "create_unique_asset_name", "add_level_to_world", "list_assets", diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py index 1e5491e3327..a1cfc35d924 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py @@ -6,6 +6,7 @@ get_params, format_string, get_asset, + get_subsequences ) from pipeline import ( UNREAL_VERSION, @@ -53,6 +54,98 @@ def create_look(params): return {"return": members} +def create_render(params): + """ + Args: + params (str): string containing a dictionary with parameters: + path (str): path to the instance + selected_asset (str): path to the selected asset + """ + selected_asset_path = get_params(params, 'path', 'selected_asset') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + selected_asset = ar.get_asset_by_object_path( + selected_asset_path).get_asset() + + if selected_asset.get_class().get_name() != "LevelSequence": + unreal.log_error( + f"Skipping {selected_asset.get_name()}. It isn't a Level " + "Sequence.") + + # 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.") + + # The asset name is 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/OpenPype/", "").split("/")[0] + + # Get the master sequence and the master level. + # There should be only one sequence and one level in the directory. + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"/Game/OpenPype/{asset_name}"], + 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=[f"/Game/OpenPype/{asset_name}"], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() + + # 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: + return master_seq, master_lvl, master_seq_data + + 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: + return master_seq, master_lvl, master_seq_data + + seq_data_list.append(curr_data) + + return None, None, None + + def create_unique_asset_name(params): """ Args: @@ -114,8 +207,8 @@ def list_assets(params): directory_path, recursive, include_folder = get_params( params, 'directory_path', 'recursive', 'include_folder') - return {"return": unreal.EditorAssetLibrary.list_assets( - directory_path, recursive, include_folder)} + return {"return": list(unreal.EditorAssetLibrary.list_assets( + directory_path, recursive, include_folder))} def get_assets_of_class(params): diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 5834d2e7a7a..6bf8b7b076a 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -import unreal - from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api.pipeline import ( - get_subsequences -) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) +from openpype.hosts.unreal.api.pipeline import ( + send_request +) from openpype.lib import UILabelDef @@ -20,103 +18,26 @@ class CreateRender(UnrealAssetCreator): icon = "eye" def create(self, subset_name, instance_data, pre_create_data): - ar = unreal.AssetRegistryHelpers.get_asset_registry() + sel_objects = send_request("get_selected_assets") - 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 not selection: + if not sel_objects: raise CreatorError("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.") - - # The asset name is 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/OpenPype/", "").split("/")[0] - - # Get the master sequence and the master level. - # There should be only one sequence and one level in the directory. - ar_filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - 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=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - levels = ar.get_assets(ar_filter) - master_lvl = levels[0].get_asset().get_path_name() - - # 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: - 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 + for selected_asset_path in sel_objects: + master_lvl, master_seq, seq_data = send_request( + "create_render", params={"sequence_path": selected_asset_path}) # 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.") + send_request( + "log", + params={ + "message": f"Skipping {selected_asset_path}." + "It isn't a sub-sequence of the master " + "sequence.", + "level": "warning" + }) continue instance_data["members"] = [selected_asset_path] From b043654365217e58680c3fb0186c7a246d6cbf31 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 May 2023 11:34:05 +0100 Subject: [PATCH 42/55] Added check for null parameters in communication --- .../Private/OpenPypeCommunication.cpp | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp index 4cb02cd74ae..41d9f7a4caa 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommunication.cpp @@ -223,6 +223,10 @@ void FOpenPypeCommunication::RunMethod(TSharedPtr Root) str = std::regex_replace(str, true_regex, "True"); str = std::regex_replace(str, false_regex, "False"); + // Fix null for python + std::regex null_regex("\\bnull\\b"); + str = std::regex_replace(str, null_regex, "None"); + // We also need to remove the new line characters from the string, because // they will cause an error in the python command. std::string FormattedParamsStr; @@ -271,3 +275,36 @@ void FOpenPypeCommunication::RunMethod(TSharedPtr Root) Socket->Send(StringResponse); } + +// void HandleJsonRpcRequest(const FString& JsonRpcRequest) +// { +// TSharedPtr JsonObject; +// TSharedRef> JsonReader = TJsonReaderFactory<>::Create(JsonRpcRequest); + +// if (FJsonSerializer::Deserialize(JsonReader, JsonObject)) +// { +// FString MethodName; +// if (JsonObject->TryGetStringField("method", MethodName)) +// { +// if (MethodName == "subtract") +// { +// TSharedPtr ParamsObject = JsonObject->GetObjectField("params"); +// int32 Minuend = ParamsObject->GetIntegerField("minuend"); +// int32 Subtrahend = ParamsObject->GetIntegerField("subtrahend"); + +// int32 Difference = Minuend - Subtrahend; + +// TSharedPtr ResponseObject = MakeShareable(new FJsonObject); +// ResponseObject->SetStringField("jsonrpc", "2.0"); +// ResponseObject->SetObjectField("result", MakeShareable(new FJsonValueNumber(Difference))); +// ResponseObject->SetNumberField("id", JsonObject->GetNumberField("id")); + +// FString JsonRpcResponse; +// TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&JsonRpcResponse); +// FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), JsonWriter); + +// // Send the JSON-RPC 2.0 response back to the client +// } +// } +// } +// } From 4090afb97ee80890088d1fd1a9bff101a0e04099 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 29 Jun 2023 11:55:04 +0100 Subject: [PATCH 43/55] Fix layout not loading --- .../integration/UE_5.0/OpenPype/Content/Python/plugin.py | 3 +++ .../hosts/unreal/plugins/load/load_alembic_animation.py | 6 ++++++ openpype/hosts/unreal/plugins/load/load_animation.py | 6 ++++++ openpype/hosts/unreal/plugins/load/load_camera.py | 6 ++++++ .../hosts/unreal/plugins/load/load_geometrycache_abc.py | 6 ++++++ openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py | 6 ++++++ openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py | 6 ++++++ openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py | 6 ++++++ openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py | 6 ++++++ openpype/hosts/unreal/plugins/load/load_uasset.py | 6 ++++++ 10 files changed, 57 insertions(+) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py index a1cfc35d924..627d0632741 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/plugin.py @@ -645,6 +645,9 @@ def process_family(params): 'instance_name', 'transform', 'basis', 'sequence_path') + basis = eval(basis) + transform = eval(transform) + actors = [] bindings = [] diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index b544f8805c4..0d18ac3952a 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -102,6 +102,12 @@ def load(self, context, name=None, namespace=None, options=None): containerise(asset_dir, container_name, data) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index a77330365dc..42f7ef74df1 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -215,6 +215,12 @@ def load(self, context, name=None, namespace=None, options=None): send_request("save_current_level") send_request("load_level", params={"level_path": master_level}) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 438bb5f5f03..c7139ee0561 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -263,6 +263,12 @@ def load(self, context, name=None, namespace=None, options=None): send_request("save_all_dirty_levels") send_request("load_level", params={"level_path": master_level}) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): context = representation.get("context") asset = container.get('asset') diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 2993745f5b9..aac39667c3a 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -125,6 +125,12 @@ def load(self, context, name=None, namespace=None, options=None): containerise(asset_dir, container_name, data) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 2bfe7f2ac0a..e9793e921d7 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -112,6 +112,12 @@ def load(self, context, name=None, namespace=None, options=None): containerise(asset_dir, container_name, data) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index aae62ca1778..23ce82a098b 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -113,6 +113,12 @@ def load(self, context, name=None, namespace=None, options=None): containerise(asset_dir, container_name, data) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index d2fd24b01a7..d7d57bb9d23 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -114,6 +114,12 @@ def load(self, context, name=None, namespace=None, options=None): containerise(asset_dir, container_name, data) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 5746b000eb0..e1217d951cb 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -99,6 +99,12 @@ def load(self, context, name=None, namespace=None, options=None): containerise(asset_dir, container_name, data) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 71dbc5231e5..7df75ec8dd6 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -72,6 +72,12 @@ def load(self, context, name=None, namespace=None, options=None): containerise(asset_dir, container_name, data) + return send_request( + "list_assets", params={ + "directory_path": asset_dir, + "recursive": True, + "include_folder": True}) + def update(self, container, representation): filename = get_representation_path(representation) asset_dir = container["namespace"] From 02e653addd83c4ffd7760f288a835f1e7139fc50 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jul 2023 14:21:36 +0100 Subject: [PATCH 44/55] Rendering implementation --- openpype/hosts/unreal/api/pipeline.py | 1 + openpype/hosts/unreal/api/rendering.py | 50 ++++++++++++++++++++++++++ openpype/hosts/unreal/integration | 2 +- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/unreal/api/rendering.py diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index f301b856e9b..4304e034d32 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -180,6 +180,7 @@ def unreal_log(message, level): """ send_request("log", params={"message": message, "level": level}) + def imprint(node, data): """Imprint data to container. diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py new file mode 100644 index 00000000000..07c79456229 --- /dev/null +++ b/openpype/hosts/unreal/api/rendering.py @@ -0,0 +1,50 @@ +import os + +from openpype.settings import get_project_settings +from openpype.pipeline import Anatomy +from openpype.hosts.unreal.api.pipeline import ( + send_request, +) +from openpype.widgets.message_window import Window + + +def start_rendering(): + """ + Start the rendering process. + """ + + # Get selected sequences + selection = send_request("get_selected_assets") + + if not selection: + Window( + parent=None, + 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.") + + try: + project = os.environ.get("AVALON_PROJECT") + anatomy = Anatomy(project) + root = anatomy.roots['renders'] + except Exception as e: + raise RuntimeError( + "Could not find render root in anatomy settings.") from e + + render_dir = f"{root}/{project}" + + data = get_project_settings(project) + config_path = data.get("unreal").get("render_config_path") + render_format = data.get("unreal").get("render_format", "png") + preroll_frames = data.get("unreal").get("preroll_frames", 0) + + send_request( + "start_rendering", + params={ + "selection": selection, + "render_dir": render_dir, + "config_path": config_path, + "render_format": render_format, + "preroll_frames": preroll_frames}) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index dae3447ef32..5a9092e5b42 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit dae3447ef3241d3856c022af36b81c187062e1c6 +Subproject commit 5a9092e5b42be953dc9ecd54fe673de5c5541deb From 2228b65814a692c9860f9f793b17eabd1c90b1a5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jul 2023 14:46:13 +0100 Subject: [PATCH 45/55] Fix missing class name updates --- openpype/hosts/unreal/integration | 2 +- openpype/hosts/unreal/plugins/load/load_layout.py | 4 ++-- openpype/hosts/unreal/plugins/publish/extract_layout.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 5a9092e5b42..04dc699b11c 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 5a9092e5b42be953dc9ecd54fe673de5c5541deb +Subproject commit 04dc699b11c209810bf72591aff93450590ae186 diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5011c1b4e80..ede1a8c2636 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -418,9 +418,9 @@ def _load_representation( "get_assets_of_class", params={ "asset_list": assets, - "class_name": "AssetContainer"}) + "class_name": "AyonAssetContainer"}) assert len(asset_containers) == 1, ( - "There should be only one AssetContainer in " + "There should be only one AyonAssetContainer in " "the loaded assets.") container = asset_containers[0] diff --git a/openpype/hosts/unreal/plugins/publish/extract_layout.py b/openpype/hosts/unreal/plugins/publish/extract_layout.py index 57e79575759..77ccc942d42 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_layout.py +++ b/openpype/hosts/unreal/plugins/publish/extract_layout.py @@ -53,7 +53,7 @@ def process(self, instance): try: asset_container = ar.get_assets(filter)[0].get_asset() except IndexError: - self.log.error("AssetContainer not found.") + self.log.error("AyonAssetContainer not found.") return parent_id = eal.get_metadata_tag(asset_container, "parent") From 10d84a186c85fbc6b2f392c9776d6d8b0c289a78 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 18 Jul 2023 15:57:24 +0100 Subject: [PATCH 46/55] Restore camera and layout updated loading --- openpype/hosts/unreal/api/plugin.py | 2 +- openpype/hosts/unreal/integration | 2 +- .../hosts/unreal/plugins/load/load_camera.py | 115 ++++++---------- .../hosts/unreal/plugins/load/load_layout.py | 130 +++++++----------- 4 files changed, 94 insertions(+), 155 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index c670782ee14..36b576b6c1d 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -224,7 +224,7 @@ def get_pre_create_attr_defs(self): @six.add_metaclass(ABCMeta) class UnrealBaseLoader(LoaderPlugin): """Base class for Unreal loader plugins.""" - root = "/Game/OpenPype" + root = "/Game/Ayon" suffix = "_CON" def update(self, container, representation): diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 04dc699b11c..48d2fcf7f35 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 04dc699b11c209810bf72591aff93450590ae186 +Subproject commit 48d2fcf7f354f2cbbe5619dc5e886e7379330ed3 diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 9d2c6247a9e..30bf5607e5b 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -5,7 +5,7 @@ from openpype.client import get_assets, get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, - legacy_io, + get_current_project_name, ) from openpype.hosts.unreal.api.plugin import UnrealBaseLoader from openpype.hosts.unreal.api.pipeline import ( @@ -25,7 +25,7 @@ class CameraLoader(UnrealBaseLoader): @staticmethod def _create_levels( - hierarchy_dir_list, hierarchy, asset_path_parent, asset + hierarchy_dir_list, hierarchy, asset_dir, asset ): # Create map for the shot, and create hierarchy of map. If the maps # already exist, we will use them. @@ -38,12 +38,12 @@ def _create_levels( "new_level", params={"level_path": f"{h_dir}/{h_asset}_map"}) - level = f"{asset_path_parent}/{asset}_map.{asset}_map" + level = f"{asset_dir}/{asset}_map_camera.{asset}_map_camera" if not send_request( "does_asset_exist", params={"asset_path": level}): send_request( "new_level", - params={"level_path": f"{asset_path_parent}/{asset}_map"}) + params={"level_path": f"{asset_dir}/{asset}_map_camera"}) send_request("load_level", params={"level_path": master_level}) send_request("add_level_to_world", params={"level_path": level}) @@ -51,11 +51,11 @@ def _create_levels( send_request("save_all_dirty_levels") send_request("load_level", params={"level_path": level}) - return master_level + return master_level, level @staticmethod def _get_frame_info(h_dir): - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_data = get_asset_by_name( project_name, h_dir.split('/')[-1], @@ -86,23 +86,8 @@ def _get_frame_info(h_dir): return min_frame, max_frame, asset_data.get('data').get("fps") def _get_sequences(self, hierarchy_dir_list, hierarchy): - # TODO refactor - # - Creationg of hierarchy should be a function in unreal integration - # - it's used in multiple loaders but must not be loader's logic - # - hard to say what is purpose of the loop - # - variables does not match their meaning - # - why scene is stored to sequences? - # - asset documents vs. elements - # - cleanup variable names in whole function - # - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc' - # - really inefficient queries of asset documents - # - existing asset in scene is considered as "with correct values" - # - variable 'elements' is modified during it's loop - # Get all the sequences in the hierarchy. It will create them, if - # they don't exist. frame_ranges = [] sequences = [] - frame_ranges = [] for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): root_content = send_request( "list_assets", params={ @@ -124,7 +109,7 @@ def _get_sequences(self, hierarchy_dir_list, hierarchy): else: start_frame, end_frame, fps = self._get_frame_info(h_dir) sequence = send_request( - "generate_master_sequence", + "generate_sequence", params={ "asset_name": h, "asset_path": h_dir, @@ -137,47 +122,6 @@ def _get_sequences(self, hierarchy_dir_list, hierarchy): return sequences, frame_ranges - @staticmethod - def _process(sequences, frame_ranges, asset, asset_dir, filename): - project_name = legacy_io.active_project() - data = get_asset_by_name(project_name, asset)["data"] - start_frame = 0 - end_frame = data.get('clipOut') - data.get('clipIn') + 1 - fps = data.get("fps") - - cam_sequence = send_request( - "generate_sequence", - params={ - "asset_name": f"{asset}_camera", - "asset_path": asset_dir, - "start_frame": start_frame, - "end_frame": end_frame, - "fps": fps}) - - # Add sequences data to hierarchy - for i in range(len(sequences) - 1): - send_request( - "set_sequence_hierarchy", - params={ - "parent_path": sequences[i], - "child_path": sequences[i + 1], - "child_start_frame": frame_ranges[i + 1][0], - "child_end_frame": frame_ranges[i + 1][1]}) - - send_request( - "set_sequence_hierarchy", - params={ - "parent_path": sequences[-1], - "child_path": cam_sequence, - "child_start_frame": data.get('clipIn'), - "child_end_frame": data.get('clipOut')}) - - send_request( - "import_camera", - params={ - "sequence_path": cam_sequence, - "import_filename": filename}) - def load(self, context, name=None, namespace=None, options=None): """ Load and containerise representation into Content Browser. @@ -234,17 +178,42 @@ def load(self, context, name=None, namespace=None, options=None): "name": name, "version": unique_number}) - asset_path_parent = Path(asset_dir).parent.as_posix() - send_request("make_directory", params={"directory_path": asset_dir}) - master_level = self._create_levels( - hierarchy_dir_list, hierarchy, asset_path_parent, asset) + master_level, level = self._create_levels( + hierarchy_dir_list, hierarchy, asset_dir, asset) sequences, frame_ranges = self._get_sequences( hierarchy_dir_list, hierarchy) - self._process(sequences, frame_ranges, asset, asset_dir, self.fname) + project_name = get_current_project_name() + data = get_asset_by_name(project_name, asset)["data"] + + cam_sequence = send_request( + "generate_camera_sequence", + params={ + "asset": asset, + "asset_dir": asset_dir, + "sequences": sequences, + "frame_ranges": frame_ranges, + "level": level, + "fps": data.get("fps"), + "clip_in": data.get("clipIn"), + "clip_out": data.get("clipOut")}) + + send_request( + "import_camera", + params={ + "sequence_path": cam_sequence, + "import_filename": self.filepath_from_context(context)}) + + send_request( + "set_sequences_range", + params={ + "sequence": cam_sequence, + "clip_in": data.get("clipIn"), + "clip_out": data.get("clipOut"), + "frame_start": data.get("frameStart")}) data = { "schema": "ayon:container-2.0", @@ -264,11 +233,15 @@ def load(self, context, name=None, namespace=None, options=None): send_request("save_all_dirty_levels") send_request("load_level", params={"level_path": master_level}) - return send_request( + assets = send_request( "list_assets", params={ - "directory_path": asset_dir, + "directory_path": hierarchy_dir_list[0], "recursive": True, - "include_folder": True}) + "include_folder": False}) + + send_request("save_listed_assets", params={"asset_list": assets}) + + return assets def update(self, container, representation): context = representation.get("context") diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index ede1a8c2636..ebc58b16c40 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -15,7 +15,7 @@ load_container, get_representation_path, AYON_CONTAINER_ID, - legacy_io, + get_current_project_name, ) from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings @@ -38,17 +38,17 @@ class LayoutLoader(UnrealBaseLoader): @staticmethod def _create_levels( - hierarchy_dir_list, hierarchy, asset_path_parent, asset, + hierarchy_dir_list, hierarchy, asset_dir, asset, create_sequences_option ): - level = f"{asset_path_parent}/{asset}_map.{asset}_map" + level = f"{asset_dir}/{asset}_map.{asset}_map" master_level = None if not send_request( "does_asset_exist", params={"asset_path": level}): send_request( "new_level", - params={"level_path": f"{asset_path_parent}/{asset}_map"}) + params={"level_path": f"{asset_dir}/{asset}_map"}) if create_sequences_option: # Create map for the shot, and create hierarchy of map. If the @@ -76,7 +76,7 @@ def _create_levels( @staticmethod def _get_frame_info(h_dir): - project_name = legacy_io.active_project() + project_name = get_current_project_name() asset_data = get_asset_by_name( project_name, h_dir.split('/')[-1], @@ -119,10 +119,9 @@ def _get_sequences(self, hierarchy_dir_list, hierarchy): "include_folder": False}) if existing_sequences := send_request( - "get_assets_of_class", - params={ - "asset_list": root_content, - "class_name": "LevelSequence"}, + "get_assets_of_class", + params={ + "asset_list": root_content, "class_name": "LevelSequence"}, ): for sequence in existing_sequences: sequences.append(sequence) @@ -133,7 +132,7 @@ def _get_sequences(self, hierarchy_dir_list, hierarchy): else: start_frame, end_frame, fps = self._get_frame_info(h_dir) sequence = send_request( - "generate_master_sequence", + "generate_sequence", params={ "asset_name": h, "asset_path": h_dir, @@ -144,67 +143,8 @@ def _get_sequences(self, hierarchy_dir_list, hierarchy): sequences.append(sequence) frame_ranges.append((start_frame, end_frame)) - send_request( - "save_listed_assets", - params={"asset_list": sequences}) - return sequences, frame_ranges - @staticmethod - def _process_sequences(sequences, frame_ranges, asset, asset_dir, level): - project_name = legacy_io.active_project() - data = get_asset_by_name(project_name, asset)["data"] - start_frame = 0 - end_frame = data.get('clipOut') - data.get('clipIn') + 1 - fps = data.get("fps") - - shot = send_request( - "generate_sequence", - params={ - "asset_name": asset, - "asset_path": asset_dir, - "start_frame": start_frame, - "end_frame": end_frame, - "fps": fps}) - - # sequences and frame_ranges have the same length - for i in range(len(sequences) - 1): - send_request( - "set_sequence_hierarchy", - params={ - "parent_path": sequences[i], - "child_path": sequences[i + 1], - "child_start_frame": frame_ranges[i + 1][0], - "child_end_frame": frame_ranges[i + 1][1]}) - send_request( - "set_sequence_visibility", - params={ - "parent_path": sequences[i], - "parent_end_frame": frame_ranges[i][1], - "child_start_frame": frame_ranges[i + 1][0], - "child_end_frame": frame_ranges[i + 1][1], - "map_paths": [level]}) - - if sequences: - send_request( - "set_sequence_hierarchy", - params={ - "parent_path": sequences[-1], - "child_path": shot, - "child_start_frame": data.get('clipIn'), - "child_end_frame": data.get('clipOut')}) - - send_request( - "set_sequence_visibility", - params={ - "parent_path": sequences[-1], - "parent_end_frame": frame_ranges[-1][1], - "child_start_frame": data.get('clipIn'), - "child_end_frame": data.get('clipOut'), - "map_paths": [level]}) - - return shot - @staticmethod def _get_fbx_loader(loaders, family): name = "" @@ -354,7 +294,7 @@ def _get_repre_docs_by_version_id(data): if not version_ids: return output - project_name = legacy_io.active_project() + project_name = get_current_project_name() repre_docs = get_representations( project_name, representation_names=["fbx", "abc"], @@ -419,9 +359,11 @@ def _load_representation( params={ "asset_list": assets, "class_name": "AyonAssetContainer"}) + assert len(asset_containers) == 1, ( "There should be only one AyonAssetContainer in " "the loaded assets.") + container = asset_containers[0] skeletons = send_request( @@ -433,7 +375,7 @@ def _load_representation( "There should be one skeleton at most in " "the loaded assets.") skeleton = skeletons[0] if skeletons else None - return assets, container, skeleton, loader + return assets, container, skeleton @staticmethod def _process_instances( @@ -515,8 +457,7 @@ def _process_assets(self, lib_path, asset_dir, sequence, repr_loaded=None): family = element.get('family') - (assets, container, - skeleton, loader) = self._load_representation( + assets, container, skeleton = self._load_representation( family, representation, repr_format, instance_name, all_loaders) @@ -564,7 +505,7 @@ def load(self, context, name=None, namespace=None, options=None): used now, data are imprinted by `containerise()`. """ data = get_current_project_settings() - create_sequences_option = data["unreal"]["level_sequences_for_layouts"] + create_sequences = data["unreal"]["level_sequences_for_layouts"] # Create directory for asset and Ayon container hierarchy = context.get('asset').get('data').get('parents') @@ -580,31 +521,44 @@ def load(self, context, name=None, namespace=None, options=None): asset_dir, container_name = send_request( "create_unique_asset_name", params={ - "root": root, + "root": hierarchy_dir, "asset": asset, "name": name}) - asset_path_parent = Path(asset_dir).parent.as_posix() - send_request("make_directory", params={"directory_path": asset_dir}) shot = None level, master_level = self._create_levels( - hierarchy_dir_list, hierarchy, asset_path_parent, asset, - create_sequences_option) + hierarchy_dir_list, hierarchy, asset_dir, asset, + create_sequences) - if create_sequences_option: + if create_sequences: sequences, frame_ranges = self._get_sequences( hierarchy_dir_list, hierarchy) - shot = self._process_sequences( - sequences, frame_ranges, asset, asset_dir, level) + project_name = get_current_project_name() + data = get_asset_by_name(project_name, asset)["data"] + + shot = send_request( + "generate_layout_sequence", + params={ + "asset": asset, + "asset_dir": asset_dir, + "sequences": sequences, + "frame_ranges": frame_ranges, + "level": level, + "fps": data.get("fps"), + "clip_in": data.get("clipIn"), + "clip_out": data.get("clipOut")}) send_request("load_level", params={"level_path": level}) loaded_assets = self._process_assets(self.fname, asset_dir, shot) + send_request( + "save_listed_assets", params={"asset_list": loaded_assets}) + send_request("save_current_level") data = { @@ -626,6 +580,18 @@ def load(self, context, name=None, namespace=None, options=None): if master_level: send_request("load_level", params={"level_path": master_level}) + save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir + + assets = send_request( + "list_assets", params={ + "directory_path": save_dir, + "recursive": True, + "include_folder": False}) + + send_request("save_listed_assets", params={"asset_list": assets}) + + return assets + @staticmethod def _remove_bound_assets(asset_dir): parent_path = Path(asset_dir).parent.as_posix() From e09516b0adac5b4da1c6f4adc450d69abea17031 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 19 Jul 2023 10:27:18 +0100 Subject: [PATCH 47/55] Reimplemented update for camera --- openpype/hosts/unreal/api/plugin.py | 2 +- openpype/hosts/unreal/integration | 2 +- .../hosts/unreal/plugins/load/load_camera.py | 67 ++++++++++++++----- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 36b576b6c1d..23dc3e420e5 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -236,7 +236,7 @@ def update(self, container, representation): "parent": str(representation["parent"]) } - containerise(asset_dir, container_name, data) + imprint(f"{asset_dir}/{container_name}", data) def remove(self, container): path = container["namespace"] diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 48d2fcf7f35..da70bbc02f0 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 48d2fcf7f354f2cbbe5619dc5e886e7379330ed3 +Subproject commit da70bbc02f06afb98eea55f39feace1b8d02cf44 diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 30bf5607e5b..e2aeedda7e7 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -244,30 +244,63 @@ def load(self, context, name=None, namespace=None, options=None): return assets def update(self, container, representation): - context = representation.get("context") - asset = container.get('asset') - asset_dir = container.get("namespace") - filename = representation["data"]["path"] + sequence_path, curr_time, is_cam_lock, vp_loc, vp_rot = send_request( + "get_current_sequence_and_level_info") - root = self.root - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + self.log.info(f"curr_time: {curr_time}") - parent_sequence = send_request( - "remove_camera", params={ - "root": root, "asset_dir": asset_dir}) + new_sequence = send_request( + "update_camera", + params={ + "asset_dir": container.get('namespace'), + "asset": container.get('asset'), + "root": self.root}) - sequences = [parent_sequence] - frame_ranges = [] + send_request( + "import_camera", + params={ + "sequence_path": new_sequence, + "import_filename": str(representation["data"]["path"])}) + + project_name = get_current_project_name() + asset = container.get('asset') + data = get_asset_by_name(project_name, asset)["data"] - self._process(sequences, frame_ranges, asset, asset_dir, filename) + send_request( + "set_sequences_range", + params={ + "sequence": new_sequence, + "clip_in": data.get("clipIn"), + "clip_out": data.get("clipOut"), + "frame_start": data.get("frameStart")}) - super(UnrealBaseLoader, self).update(container, representation) + super(CameraLoader, self).update(container, representation) send_request("save_all_dirty_levels") - send_request("load_level", params={"level_path": master_level}) + + namespace = container.get('namespace').replace(f"{self.root}/", "") + ms_asset = namespace.split('/')[0] + asset_path = f"{self.root}/{ms_asset}" + + assets = send_request( + "list_assets", params={ + "directory_path": asset_path, + "recursive": True, + "include_folder": False}) + + send_request("save_listed_assets", params={"asset_list": assets}) + + send_request( + "get_and_load_master_level", params={"path": asset_path}) + + send_request( + "set_current_sequence_and_level_info", + params={ + "sequence_path": sequence_path, + "curr_time": curr_time, + "is_cam_lock": is_cam_lock, + "vp_loc": vp_loc, + "vp_rot": vp_rot}) def remove(self, container): root = self.root From 18fb9ef34a4e31ab8b80939f6a4b0cf1de039426 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 19 Jul 2023 10:42:50 +0100 Subject: [PATCH 48/55] Reimplemente remove camera --- openpype/hosts/unreal/integration | 2 +- openpype/hosts/unreal/plugins/load/load_camera.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index da70bbc02f0..6998a38e40d 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit da70bbc02f06afb98eea55f39feace1b8d02cf44 +Subproject commit 6998a38e40de2a44b07461b41e4cf8a709dae6ac diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index e2aeedda7e7..162acaaa661 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -305,9 +305,10 @@ def update(self, container, representation): def remove(self, container): root = self.root path = container["namespace"] + asset = container.get('asset') send_request( "remove_camera", params={ - "root": root, "asset_dir": path}) + "asset_dir": path, "asset": asset, "root": root}) - send_request("remove_asset", params={"path": path}) + super(CameraLoader, self).remove(container) From 21cd0ebfda8617449113a4d10bc4543854cd2633 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 19 Jul 2023 12:08:05 +0100 Subject: [PATCH 49/55] Reimplemented update for layout --- openpype/hosts/unreal/integration | 2 +- .../hosts/unreal/plugins/load/load_camera.py | 2 - .../hosts/unreal/plugins/load/load_layout.py | 78 ++++++++----------- 3 files changed, 33 insertions(+), 49 deletions(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 6998a38e40d..8cb8949d509 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 6998a38e40de2a44b07461b41e4cf8a709dae6ac +Subproject commit 8cb8949d509068945c6649b5ee82cf76691e0b8b diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 162acaaa661..7ef01b9aaa1 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -247,8 +247,6 @@ def update(self, container, representation): sequence_path, curr_time, is_cam_lock, vp_loc, vp_rot = send_request( "get_current_sequence_and_level_info") - self.log.info(f"curr_time: {curr_time}") - new_sequence = send_request( "update_camera", params={ diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index ebc58b16c40..a09cab35854 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -23,6 +23,7 @@ from openpype.hosts.unreal.api.pipeline import ( send_request, containerise, + imprint, ) @@ -592,77 +593,62 @@ def load(self, context, name=None, namespace=None, options=None): return assets - @staticmethod - def _remove_bound_assets(asset_dir): - parent_path = Path(asset_dir).parent.as_posix() - - layout_level = send_request( - "get_first_asset_of_class", - params={ - "class_name": "World", - "path": parent_path, - "recursive": False}) - - send_request("load_level", params={"level_path": layout_level}) - - layout_sequence = send_request( - "get_first_asset_of_class", - params={ - "class_name": "LevelSequence", - "path": asset_dir, - "recursive": False}) - - send_request( - "delete_all_bound_assets", - params={"level_sequence_path": layout_sequence}) - def update(self, container, representation): - root = self.root asset_dir = container.get('namespace') - container_name = container['objectName'] context = representation.get("context") + hierarchy = context.get('hierarchy').split("/") data = get_current_project_settings() - create_sequences_option = data["unreal"]["level_sequences_for_layouts"] - - master_level = None - layout_sequence = None + create_sequences = data["unreal"]["level_sequences_for_layouts"] - if create_sequences_option: - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + sequence_path, curr_time, is_cam_lock, vp_loc, vp_rot = send_request( + "get_current_sequence_and_level_info") - self._remove_bound_assets(asset_dir) + sequence, master_level, prev_level = send_request( + "update_layout", params={ + "asset_dir": asset_dir, + "root": self.root, + "hierarchy": hierarchy, + "create_sequences": create_sequences}) - prev_level = None if master_level else send_request( - "get_current_level") source_path = get_representation_path(representation) - loaded_assets = self._process(source_path, asset_dir, layout_sequence) + loaded_assets = self._process_assets(source_path, asset_dir, sequence) data = { "representation": str(representation["_id"]), "parent": str(representation["parent"]), "loaded_assets": loaded_assets } - - containerise(asset_dir, container_name, data) + imprint(f"{asset_dir}/{container.get('objectName')}", data) send_request("save_current_level") + save_dir = ( + f"{self.root}/{hierarchy[0]}" if create_sequences else asset_dir) + if master_level: send_request("load_level", params={"level_path": master_level}) elif prev_level: send_request("load_level", params={"level_path": 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) + assets = send_request( + "list_assets", params={ + "directory_path": save_dir, + "recursive": True, + "include_folder": False}) + + send_request("save_listed_assets", params={"asset_list": assets}) + + send_request( + "set_current_sequence_and_level_info", + params={ + "sequence_path": sequence_path, + "curr_time": curr_time, + "is_cam_lock": is_cam_lock, + "vp_loc": vp_loc, + "vp_rot": vp_rot}) - editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) def remove(self, container): """ From 5f4445234ceeb13d6ecdcc34ec7d1d609c0f38c8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 21 Jul 2023 11:52:13 +0100 Subject: [PATCH 50/55] Reimplemented remove for layout --- openpype/hosts/unreal/integration | 2 +- openpype/hosts/unreal/plugins/load/load_layout.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 8cb8949d509..972368d4c64 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 8cb8949d509068945c6649b5ee82cf76691e0b8b +Subproject commit 972368d4c641194294ae9da0b1956f0f9df532ba diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index a09cab35854..fe0f71c6c96 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -671,5 +671,5 @@ def remove(self, container): "asset": asset, "asset_dir": asset_dir, "asset_name": asset_name, - "loaded_asset": loaded_assets, + "loaded_assets": loaded_assets, "create_sequences": create_sequences_option}) From a6cd72f25cee22dcc5d9b3d3015047d714a48721 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 21 Jul 2023 12:15:19 +0100 Subject: [PATCH 51/55] Hound fixes --- openpype/hosts/unreal/plugins/load/load_layout.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index fe0f71c6c96..537f0d18418 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -67,9 +67,9 @@ def _create_levels( if master_level: send_request("load_level", - params={"level_path": master_level}) + params={"level_path": master_level}) send_request("add_level_to_world", - params={"level_path": level}) + params={"level_path": level}) send_request("save_all_dirty_levels") send_request("load_level", params={"level_path": level}) @@ -649,7 +649,6 @@ def update(self, container, representation): "vp_loc": vp_loc, "vp_rot": vp_rot}) - def remove(self, container): """ Delete the layout. First, check if the assets loaded with the layout From fe44afc9cd66b046a8e7f1ef62c98872b51d535d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 29 Aug 2023 15:08:54 +0100 Subject: [PATCH 52/55] Apply suggestions --- openpype/hosts/unreal/api/launch_script.py | 3 ++- openpype/hosts/unreal/api/pipeline.py | 7 ------- openpype/hosts/unreal/api/plugin.py | 4 ++-- .../unreal/plugins/load/load_alembic_animation.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_animation.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_camera.py | 4 ++-- .../unreal/plugins/load/load_geometrycache_abc.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_layout.py | 10 +++++----- .../unreal/plugins/load/load_layout_existing.py | 12 ++++++------ .../unreal/plugins/load/load_skeletalmesh_abc.py | 4 ++-- .../unreal/plugins/load/load_skeletalmesh_fbx.py | 4 ++-- .../hosts/unreal/plugins/load/load_staticmesh_abc.py | 4 ++-- .../hosts/unreal/plugins/load/load_staticmesh_fbx.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_uasset.py | 4 ++-- .../plugins/publish/collect_render_instances.py | 2 +- .../hosts/unreal/plugins/publish/extract_layout.py | 2 +- 16 files changed, 35 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/unreal/api/launch_script.py b/openpype/hosts/unreal/api/launch_script.py index 173929c3bf3..cc04728e550 100644 --- a/openpype/hosts/unreal/api/launch_script.py +++ b/openpype/hosts/unreal/api/launch_script.py @@ -13,6 +13,7 @@ ) from openpype.hosts.unreal.api import UnrealHost from openpype.pipeline import install_host +from openpype.tools.utils import get_openpype_qt_app from openpype import style logging.basicConfig(level=logging.DEBUG) @@ -29,7 +30,7 @@ def main(launch_args): # Create QtApplication for tools # - QApplicaiton is also main thread/event loop of the server - qt_app = QtWidgets.QApplication([]) + qt_app = get_openpype_qt_app() unreal_host = UnrealHost() install_host(unreal_host) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 4304e034d32..678d636d11d 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -154,13 +154,6 @@ def _register_events(): pass -def format_string(input_str): - string = input_str.replace('\\', '/') - string = string.replace('"', '\\"') - string = string.replace("'", "\\'") - return f'"{string}"' - - def send_request(request: str, params: dict = None): communicator = CommunicationWrapper.communicator if ret_value := ast.literal_eval( diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 23dc3e420e5..8d17ef403cd 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -232,8 +232,8 @@ def update(self, container, representation): container_name = container['objectName'] data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) + "representation_id": str(representation["_id"]), + "version_id": str(representation["parent"]) } imprint(f"{asset_dir}/{container_name}", data) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index d930f2f36d8..9429e16a92f 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -95,8 +95,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 373fcea4a7d..1c6fd1915a1 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -205,8 +205,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index ca9ed14da63..61883c5a673 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -224,8 +224,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 6b49292d1a7..9deb257cb6c 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -115,8 +115,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], "frame_start": context["asset"]["data"]["frameStart"], "frame_end": context["asset"]["data"]["frameEnd"], diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 537f0d18418..6fa8ed83c42 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -348,7 +348,7 @@ def _load_representation( if not loader: self.log.error( f"No valid loader found for {representation}") - return None, None, None, None + return None, None, None assets = load_container( loader, @@ -570,8 +570,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } @@ -616,8 +616,8 @@ def update(self, container, representation): loaded_assets = self._process_assets(source_path, asset_dir, sequence) data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]), + "representation_id": str(representation["_id"]), + "version_id": str(representation["parent"]), "loaded_assets": loaded_assets } imprint(f"{asset_dir}/{container.get('objectName')}", data) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index acb13b35419..039715e42ad 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -55,8 +55,8 @@ def _create_container( "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": representation, - "parent": parent, + "representation_id": representation, + "version_id": parent, "family": family } @@ -316,8 +316,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], "loaded_assets": containers } @@ -333,8 +333,8 @@ def update(self, container, representation): containers = self._process(source_path, project_name) data = { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]), + "representation_id": str(representation["_id"]), + "version_id": str(representation["parent"]), "loaded_assets": containers } diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 042be84a083..af3508bc9d8 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -104,8 +104,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], "default_conversion": default_conversion } diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index bac09641e3f..1f663073d72 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -106,8 +106,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index 057036a9516..9c5942ec1ce 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -106,8 +106,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"], "default_conversion": default_conversion } diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 5938ce715e4..d9bda84e89d 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -92,8 +92,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index edd2a99b699..b81dc642f16 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -65,8 +65,8 @@ def load(self, context, name=None, namespace=None, options=None): "container_name": container_name, "asset_name": asset_name, "loader": self.__class__.__name__, - "representation": str(context["representation"]["_id"]), - "parent": str(context["representation"]["parent"]), + "representation_id": str(context["representation"]["_id"]), + "version_id": str(context["representation"]["parent"]), "family": context["representation"]["context"]["family"] } diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index dad0310dfcc..e0497cec46a 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -68,7 +68,7 @@ def process(self, instance): new_data["setMembers"] = seq_name new_data["family"] = "render" new_data["families"] = ["render", "review"] - new_data["parent"] = data.get("parent") + new_data["version_id"] = data.get("version_id") new_data["subset"] = f"{data.get('subset')}_{seq_name}" new_data["level"] = data.get("level") new_data["output"] = s.get('output') diff --git a/openpype/hosts/unreal/plugins/publish/extract_layout.py b/openpype/hosts/unreal/plugins/publish/extract_layout.py index a3b70737b7b..1616aec7d1a 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_layout.py +++ b/openpype/hosts/unreal/plugins/publish/extract_layout.py @@ -56,7 +56,7 @@ def process(self, instance): self.log.error("AyonAssetContainer not found.") return - parent_id = eal.get_metadata_tag(asset_container, "parent") + parent_id = eal.get_metadata_tag(asset_container, "version_id") family = eal.get_metadata_tag(asset_container, "family") self.log.info("Parent: {}".format(parent_id)) From d0077803f03179f0c5e83e3471c969d3cd95b475 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 29 Aug 2023 15:11:21 +0100 Subject: [PATCH 53/55] Applied suggestion in the integration as well --- openpype/hosts/unreal/api/launch_script.py | 2 +- openpype/hosts/unreal/integration | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/api/launch_script.py b/openpype/hosts/unreal/api/launch_script.py index cc04728e550..c1bd82c3c05 100644 --- a/openpype/hosts/unreal/api/launch_script.py +++ b/openpype/hosts/unreal/api/launch_script.py @@ -6,7 +6,7 @@ import platform import logging -from Qt import QtWidgets, QtCore, QtGui +from Qt import QtCore, QtGui from openpype.hosts.unreal.api.communication_server import ( CommunicationWrapper diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 0fdca15f6a6..116e2b00a79 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 0fdca15f6a612d72ac5f01b0bf44ebe5a5f653c8 +Subproject commit 116e2b00a79ff5685ed212a3fe88f591d29a7827 From a0a62b5cce28c66ce8c4aeb640f72ce007649aa0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 31 Oct 2023 16:20:55 +0100 Subject: [PATCH 54/55] :bug: change import location --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index f63d8b875d8..ef59bcc6f21 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -241,6 +241,6 @@ def execute(self): f"\"{project_file.as_posix()}\"") def launch_script_path(self): - from openpype.hosts.unreal import get_launch_script_path + from openpype.hosts.unreal.addon import get_launch_script_path return get_launch_script_path() From c3fbfbee2e2fa8a48232e5d7eb0300cc25d638f0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 1 Feb 2024 18:15:57 +0000 Subject: [PATCH 55/55] Fixes post merge --- openpype/hosts/unreal/integration | 2 +- .../plugins/inventory/delete_unused_assets.py | 26 ++----- .../unreal/plugins/inventory/update_actors.py | 70 +++---------------- .../plugins/load/load_alembic_animation.py | 2 +- .../hosts/unreal/plugins/load/load_camera.py | 2 +- .../plugins/load/load_skeletalmesh_abc.py | 2 +- .../plugins/load/load_skeletalmesh_fbx.py | 5 +- .../plugins/load/load_staticmesh_abc.py | 2 +- .../plugins/load/load_staticmesh_fbx.py | 2 +- .../hosts/unreal/plugins/load/load_uasset.py | 1 - 10 files changed, 21 insertions(+), 93 deletions(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index 116e2b00a79..4d4438d7eb7 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit 116e2b00a79ff5685ed212a3fe88f591d29a7827 +Subproject commit 4d4438d7eb73a11c8d575c1dd7f343dd210b92d0 diff --git a/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py b/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py index 8320e3c92dc..df55f991b52 100644 --- a/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py +++ b/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py @@ -1,8 +1,6 @@ -import unreal - -from openpype.hosts.unreal.api.tools_ui import qt_app_context -from openpype.hosts.unreal.api.pipeline import delete_asset_if_unused from openpype.pipeline import InventoryAction +from openpype.hosts.unreal.api.pipeline import send_request +from openpype.hosts.unreal.api.tools_ui import qt_app_context class DeleteUnusedAssets(InventoryAction): @@ -16,22 +14,6 @@ class DeleteUnusedAssets(InventoryAction): 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 openpype.widgets import popup @@ -51,7 +33,9 @@ def _show_confirmation_dialog(self, containers): dialog.setButtonText("Delete") dialog.on_clicked.connect( - lambda: self._delete_unused_assets(containers) + lambda: send_request( + "delete_unused_assets", params={ + "containers": containers}) ) dialog.show() diff --git a/openpype/hosts/unreal/plugins/inventory/update_actors.py b/openpype/hosts/unreal/plugins/inventory/update_actors.py index b0d941ba80a..a09af7991d4 100644 --- a/openpype/hosts/unreal/plugins/inventory/update_actors.py +++ b/openpype/hosts/unreal/plugins/inventory/update_actors.py @@ -1,63 +1,5 @@ -import unreal - -from openpype.hosts.unreal.api.pipeline import ( - ls, - replace_static_mesh_actors, - replace_skeletal_mesh_actors, - replace_geometry_cache_actors, -) from openpype.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() +from openpype.hosts.unreal.api.pipeline import send_request class UpdateAllActors(InventoryAction): @@ -69,7 +11,10 @@ class UpdateAllActors(InventoryAction): icon = "arrow-up" def process(self, containers): - update_assets(containers, False) + send_request( + "update_assets", params={ + "containers": containers, + "selected": False}) class UpdateSelectedActors(InventoryAction): @@ -81,4 +26,7 @@ class UpdateSelectedActors(InventoryAction): icon = "arrow-up" def process(self, containers): - update_assets(containers, True) + send_request( + "update_assets", params={ + "containers": containers, + "selected": True}) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index e1296ba3470..f60bf645f8f 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -71,7 +71,7 @@ def load(self, context, name=None, namespace=None, options=None): root = AYON_ASSET_DIR asset = context.get('asset').get('name') asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version').get('name') + version = context.get('version') asset_dir, container_name = send_request( "create_unique_asset_name", params={ diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 61883c5a673..296ece25695 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -177,7 +177,7 @@ def load(self, context, name=None, namespace=None, options=None): "root": hierarchy_dir, "asset": asset, "name": name, - "version": unique_number}) + "version": {"name": unique_number}}) send_request("make_directory", params={"directory_path": asset_dir}) diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 37c06a4d154..062cffe9658 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -74,7 +74,7 @@ def load(self, context, name=None, namespace=None, options=None): root = options["asset_dir"] asset = context.get('asset').get('name') asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version').get('name') + version = context.get('version') default_conversion = options.get("default_conversion") or False diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 273c567e8cc..f1914b91ed0 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -83,7 +83,7 @@ def load(self, context, name=None, namespace=None, options=None): root = options["asset_dir"] asset = context.get('asset').get('name') asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version').get('name') + version = context.get('version') asset_dir, container_name = send_request( "create_unique_asset_name", params={ @@ -99,9 +99,6 @@ def load(self, context, name=None, namespace=None, options=None): self._import_fbx_task(self.fname, asset_dir, asset_name, False) - def imprint( - self, asset, asset_dir, container_name, asset_name, representation - ): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index 3805d024dfc..710e12f6599 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -78,7 +78,7 @@ def load(self, context, name=None, namespace=None, options=None): root = AYON_ASSET_DIR asset = context.get('asset').get('name') asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version').get('name') + version = context.get('version') default_conversion = options.get("default_conversion") or False diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 9e42e16711a..ccb7cd1964a 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -69,7 +69,7 @@ def load(self, context, name=None, namespace=None, options=None): root = options["asset_dir"] asset = context.get('asset').get('name') asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version').get('name') + version = context.get('version') asset_dir, container_name = send_request( "create_unique_asset_name", params={ diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 25332f95c88..7e5d0c6e82d 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -95,7 +95,6 @@ def load(self, context, name=None, namespace=None, options=None): "include_folder": True}) def update(self, container, representation): - filename = get_representation_path(representation) asset_dir = container["namespace"] name = representation["context"]["subset"]