From 442196dfd67d643316a6b44017bea64b18c6c724 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Tue, 24 Oct 2023 20:45:01 +0800 Subject: [PATCH 001/110] delete extra spaces --- xrfeitoria/renderer/renderer_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrfeitoria/renderer/renderer_base.py b/xrfeitoria/renderer/renderer_base.py index d86a2535..9c7d3eca 100644 --- a/xrfeitoria/renderer/renderer_base.py +++ b/xrfeitoria/renderer/renderer_base.py @@ -52,7 +52,7 @@ def render_jobs(cls): @classmethod def clear(cls): """Clear all rendering jobs in the renderer queue.""" - logger.warning('[red] Clearing Renderer jobs[/red]') + logger.warning('[red]Clearing Renderer jobs[/red]') cls._clear_queue_in_engine() cls.render_queue.clear() From bf9db43a4044f6cf3a81284e38650264171516f6 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 25 Oct 2023 10:49:59 +0800 Subject: [PATCH 002/110] [Fix] Bugs in rpc --- xrfeitoria/rpc/factory.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/xrfeitoria/rpc/factory.py b/xrfeitoria/rpc/factory.py index 76e184be..df4d75c7 100644 --- a/xrfeitoria/rpc/factory.py +++ b/xrfeitoria/rpc/factory.py @@ -103,14 +103,17 @@ def _get_callstack_references(cls, code, function): # this re.split is used to split the line by the following characters: . ( ) [ ] = # e.g. ret = bpy.data.objects['Cube'] -> ["bpy", "data", "objects", "'Cube'""] if key in re.split('\.|\(|\)|\[|\]|\=|\ = | ', line.strip()): - relative_path = function.__module__.replace('.', os.path.sep) - import_dir = cls.file_path.strip('.py').replace(relative_path, '').strip(os.sep) + __module__ = function.__module__ + if __module__ == '__main__': + __module__ = os.path.basename(cls.file_path).replace('.py', '') + relative_path = __module__.replace('.', os.sep) + import_dir = cls.file_path.replace('.py', '').replace(relative_path, '').strip(os.sep) # add the source file to the import code source_import_code = f'sys.path.append(r"{import_dir}")' 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 {function.__module__} import {key}' + relative_import_code = f'from {__module__} import {key}' if relative_import_code not in import_code: import_code.append(relative_import_code) From acfb5ff68c04aa0a10226cb8984855368dc12d0c Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 25 Oct 2023 17:46:05 +0800 Subject: [PATCH 003/110] [Fix] remove theme path for fixing search stuck --- docs/en/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/en/conf.py b/docs/en/conf.py index fe3e3643..53ae319b 100644 --- a/docs/en/conf.py +++ b/docs/en/conf.py @@ -133,7 +133,6 @@ # html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_css_files = ['override.css'] # override py property html_theme_options = { 'navigation_depth': 3, From c0534b5b91777e69065d6402ee6e2a1b986c68fa Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 27 Oct 2023 10:32:46 +0800 Subject: [PATCH 004/110] [Fix] typo --- samples/anim/motion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/anim/motion.py b/samples/anim/motion.py index 5e527adc..623e307e 100644 --- a/samples/anim/motion.py +++ b/samples/anim/motion.py @@ -627,4 +627,3 @@ def _get_from_smpl_x(key, shape, *, smpl_x_data, dtype=np.float32, required=True _data = _data[:, :n_dims] # XXX: handle the case that n_dims > data.shape[1] return _data return np.zeros(shape, dtype=dtype) - return np.zeros(shape, dtype=dtype) From 312cc34d2972bd806aa9a776a6f411d351efb554 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 30 Oct 2023 18:54:58 +0800 Subject: [PATCH 005/110] [Fix] Bugs in camera param --- xrfeitoria/camera/camera_base.py | 2 +- xrfeitoria/camera/camera_parameter.py | 5 ++--- xrfeitoria/utils/projector.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/xrfeitoria/camera/camera_base.py b/xrfeitoria/camera/camera_base.py index 1a01e075..f6263186 100644 --- a/xrfeitoria/camera/camera_base.py +++ b/xrfeitoria/camera/camera_base.py @@ -85,7 +85,7 @@ def dump_params(self, output_path: PathLike) -> None: # dump K, R, T = self.get_KRT() - camera_param = CameraParameter(K=K, R=R, T=T) + camera_param = CameraParameter(K=K, R=R, T=T, world2cam=True) camera_param.dump(output_path.as_posix()) logger.debug(f'Camera parameters dumped to "{output_path.as_posix()}"') diff --git a/xrfeitoria/camera/camera_parameter.py b/xrfeitoria/camera/camera_parameter.py index 0ffb4a18..a63aab7d 100644 --- a/xrfeitoria/camera/camera_parameter.py +++ b/xrfeitoria/camera/camera_parameter.py @@ -122,7 +122,6 @@ def fromfile(cls, file: PathLike) -> 'CameraParameter': """ file = str(file) ret_cam = PinholeCameraParameter.fromfile(file) - ret_cam.load(file) return cls._from_pinhole(ret_cam) @classmethod @@ -158,11 +157,11 @@ def from_bin(cls, file: PathLike) -> 'CameraParameter': # extrinsic matrix RT x, y, z = -rotation[1], -rotation[2], -rotation[0] R = rotation_matrix([x, y, z], order='xyz', degrees=True) - T = np.array([location[1], -location[2], location[0]]) / 100.0 # unit: meter + _T = np.array([location[1], -location[2], location[0]]) / 100.0 # unit: meter + T = -R @ _T # construct camera parameter cam_param = cls(K=K, R=R, T=T, world2cam=True) - cam_param.inverse_extrinsic() return cam_param @classmethod diff --git a/xrfeitoria/utils/projector.py b/xrfeitoria/utils/projector.py index fa9f7227..d580a92e 100644 --- a/xrfeitoria/utils/projector.py +++ b/xrfeitoria/utils/projector.py @@ -24,7 +24,7 @@ def project_points3d(points3d: np.ndarray, camera_param: CameraParameter) -> np. # convert to opencv convention, and cam2world _camera_param = camera_param.clone() - if _camera_param.world2cam: + if not _camera_param.world2cam: _camera_param.inverse_extrinsic() if _camera_param.convention != 'opencv': _camera_param.convert_convention(dst='opencv') From 9b2644af9dc5b58289f87513f24e7bfbcb677b16 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 30 Oct 2023 18:55:45 +0800 Subject: [PATCH 006/110] [Fix] Typos and improve docstring of `motion.py` --- samples/anim/motion.py | 12 ++++++++---- samples/unreal/02_add_cameras.py | 2 +- samples/unreal/03_basic_render.py | 2 +- samples/unreal/04_staticmesh_render.py | 2 +- samples/unreal/05_skeletalmesh_render.py | 2 +- samples/unreal/06_custom_usage.py | 2 +- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/samples/anim/motion.py b/samples/anim/motion.py index 623e307e..dde6cdce 100644 --- a/samples/anim/motion.py +++ b/samples/anim/motion.py @@ -210,7 +210,8 @@ def slice_motion(self, frame_interval: int): self.smplx_data = self.convert_fps_smplx_data(self.smplx_data, frame_interval) def sample_motion(self, n_frames: int): - """Randomly sample motion to n_frames. + """Randomly sample motions, picking n_frames from the original motion sequence. + The indices are totally random using `np.random.choice`. Args: n_frames (int): The number of frames to sample. Randomly sampled from the original motion sequence. @@ -235,7 +236,10 @@ def sample_motion(self, n_frames: int): self.insert_rest_pose() def cut_transl(self): - """Cut the transl to zero.""" + """Cut the transl to zero. + + This will make the animation stay in place, like root motion. + """ self.transl = np.zeros_like(self.transl) if hasattr(self, 'smpl_data'): self.smpl_data['transl'] = np.zeros_like(self.smpl_data['transl']) @@ -260,8 +264,8 @@ def insert_rest_pose(self): self.smplx_data[key] = np.insert(arr, 0, 0, axis=0) def get_motion_data(self) -> List[Dict[str, Dict[str, List[float]]]]: - """Returns a list of dictionaries containing motion data for each frame of the - animation. + """Returns a list of dictionaries containing `rotation` and `location` for each + bone of each frame in the animation. Each dictionary contains bone names as keys and a nested dictionary as values. The nested dictionary contains 'rotation' and 'location' keys, which correspond to the rotation and location of the bone in that frame. diff --git a/samples/unreal/02_add_cameras.py b/samples/unreal/02_add_cameras.py index cae2104c..acf232af 100644 --- a/samples/unreal/02_add_cameras.py +++ b/samples/unreal/02_add_cameras.py @@ -14,7 +14,7 @@ root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') -log_path = output_path / 'blender.log' +log_path = output_path / 'unreal.log' default_level = '/Game/Levels/Default' diff --git a/samples/unreal/03_basic_render.py b/samples/unreal/03_basic_render.py index 6ffa7c6e..743b6aa0 100644 --- a/samples/unreal/03_basic_render.py +++ b/samples/unreal/03_basic_render.py @@ -14,7 +14,7 @@ root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') -log_path = output_path / 'blender.log' +log_path = output_path / 'unreal.log' seq_name = 'seq_preset' diff --git a/samples/unreal/04_staticmesh_render.py b/samples/unreal/04_staticmesh_render.py index d3fb785a..54b6df18 100644 --- a/samples/unreal/04_staticmesh_render.py +++ b/samples/unreal/04_staticmesh_render.py @@ -16,7 +16,7 @@ root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') -log_path = output_path / 'blender.log' +log_path = output_path / 'unreal.log' # pre-defined level default_level_path = '/Game/Levels/Default' diff --git a/samples/unreal/05_skeletalmesh_render.py b/samples/unreal/05_skeletalmesh_render.py index 43f3f1d1..092bee9e 100644 --- a/samples/unreal/05_skeletalmesh_render.py +++ b/samples/unreal/05_skeletalmesh_render.py @@ -14,7 +14,7 @@ root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') -log_path = output_path / 'blender.log' +log_path = output_path / 'unreal.log' level_path = '/Game/Levels/Playground' # pre-defined level seq_name = 'seq_skeletalmesh' diff --git a/samples/unreal/06_custom_usage.py b/samples/unreal/06_custom_usage.py index 0b4bb689..7c15d56f 100644 --- a/samples/unreal/06_custom_usage.py +++ b/samples/unreal/06_custom_usage.py @@ -14,7 +14,7 @@ root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') -log_path = output_path / 'blender.log' +log_path = output_path / 'unreal.log' @remote_unreal() From d4a9392efd656a2b3c8373333812b5c44f9dbc3f Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 1 Nov 2023 14:29:03 +0800 Subject: [PATCH 007/110] add detailed error --- xrfeitoria/utils/runner.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 8e92c7d6..93f9ca2e 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -275,7 +275,14 @@ def wait_for_start(self, process: subprocess.Popen) -> None: _num = 0 while True: if process.poll() is not None: - logger.error(self.get_process_output(process)) + error_msg = self.get_process_output(process) + logger.error(error_msg) + + if 'Unable to open a display' in error_msg: + raise RuntimeError( + 'Failed to start RPC server. Please run blender in background mode. ' + 'Set `background=True` in `init_blender`. ' + ) raise RuntimeError('RPC server failed to start. Check the engine output above.') try: self.test_connection(debug=self.debug) From fbd92dc097e601501c8463413522ac71a8dda5ab Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 1 Nov 2023 14:29:24 +0800 Subject: [PATCH 008/110] [Fix] SMPLMotion BONE_NAMES --- samples/anim/motion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/anim/motion.py b/samples/anim/motion.py index dde6cdce..078d0af5 100644 --- a/samples/anim/motion.py +++ b/samples/anim/motion.py @@ -293,7 +293,7 @@ class SMPLMotion(Motion): NAME_TO_SMPL_IDX = OrderedDict([(v, k) for k, v in SMPL_IDX_TO_NAME.items() if v]) NAMES = [x for x in SMPL_IDX_TO_NAME.values() if x] PARENTS = list(SMPL_PARENT_IDX) - BONE_NAMES = SMPLX_JOINT_NAMES[:NUM_SMPLX_BODYJOINTS] + BONE_NAMES = SMPLX_JOINT_NAMES[1 : NUM_SMPLX_BODYJOINTS + 1] BONE_NAME_TO_IDX: Dict[str, int] = {bone_name: idx for idx, bone_name in enumerate(BONE_NAMES)} # In order to make the smpl head up to +z From 75cc595f45b85f0370302ace9c356e1fe1022e1b Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Wed, 1 Nov 2023 14:46:37 +0800 Subject: [PATCH 009/110] [Fix] bug --- xrfeitoria/rpc/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrfeitoria/rpc/factory.py b/xrfeitoria/rpc/factory.py index df4d75c7..b5a832b7 100644 --- a/xrfeitoria/rpc/factory.py +++ b/xrfeitoria/rpc/factory.py @@ -107,7 +107,7 @@ def _get_callstack_references(cls, code, function): if __module__ == '__main__': __module__ = os.path.basename(cls.file_path).replace('.py', '') relative_path = __module__.replace('.', os.sep) - import_dir = cls.file_path.replace('.py', '').replace(relative_path, '').strip(os.sep) + import_dir = cls.file_path.replace('.py', '').replace(relative_path, '').rstrip() # add the source file to the import code source_import_code = f'sys.path.append(r"{import_dir}")' if source_import_code not in import_code: From ccd3fd5df0e25730ca2557102f515833ca063b40 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Wed, 1 Nov 2023 15:51:24 +0800 Subject: [PATCH 010/110] [Fix] bug --- xrfeitoria/rpc/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrfeitoria/rpc/factory.py b/xrfeitoria/rpc/factory.py index b5a832b7..4d8f58bc 100644 --- a/xrfeitoria/rpc/factory.py +++ b/xrfeitoria/rpc/factory.py @@ -107,7 +107,7 @@ def _get_callstack_references(cls, code, function): if __module__ == '__main__': __module__ = os.path.basename(cls.file_path).replace('.py', '') relative_path = __module__.replace('.', os.sep) - import_dir = cls.file_path.replace('.py', '').replace(relative_path, '').rstrip() + import_dir = cls.file_path.replace('.py', '').replace(relative_path, '').rstrip(os.sep) # add the source file to the import code source_import_code = f'sys.path.append(r"{import_dir}")' if source_import_code not in import_code: From 9ebc0e375dff15c467d20631ac4da430696a0653 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Thu, 2 Nov 2023 14:47:12 +0800 Subject: [PATCH 011/110] update CLI: - move cmd to cmd/blender - add `xf-install-plugin` --- pyproject.toml | 3 +- xrfeitoria/cmd/blender/__init__.py | 0 xrfeitoria/cmd/blender/install_plugin.py | 78 +++++++++++++++++++ xrfeitoria/cmd/{ => blender}/render.py | 0 .../utils/functions/blender_functions.py | 19 ++++- 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 xrfeitoria/cmd/blender/__init__.py create mode 100644 xrfeitoria/cmd/blender/install_plugin.py rename xrfeitoria/cmd/{ => blender}/render.py (100%) diff --git a/pyproject.toml b/pyproject.toml index 29aac5ff..c4581022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,8 @@ vis=[ "Documentation" = "https://xrfeitoria.readthedocs.io/en/latest/" [project.scripts] -xf-render = "xrfeitoria.cmd.render:app" +xf-render = "xrfeitoria.cmd.blender.render:app" +xf-install-plugin = "xrfeitoria.cmd.blender.install_plugin:app" [tool.black] line-length = 120 diff --git a/xrfeitoria/cmd/blender/__init__.py b/xrfeitoria/cmd/blender/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/xrfeitoria/cmd/blender/install_plugin.py b/xrfeitoria/cmd/blender/install_plugin.py new file mode 100644 index 00000000..33f0dd57 --- /dev/null +++ b/xrfeitoria/cmd/blender/install_plugin.py @@ -0,0 +1,78 @@ +"""Install a blender plugin with a command line interface. + +>>> xf-install-plugin --help +>>> xf-render {} [-o {output_path}] +""" + +from pathlib import Path +from textwrap import dedent + +from typer import Argument, Option, Typer +from typing_extensions import Annotated + +import xrfeitoria as xf +from xrfeitoria.utils.tools import Logger + +app = Typer(pretty_exceptions_show_locals=False) + + +@app.command() +def main( + # path config + plugin_path: Annotated[ + Path, + Argument( + ..., + exists=True, + file_okay=True, + dir_okay=False, + resolve_path=True, + help='filepath of the mesh to be renderer', + ), + ], + plugin_name_blender: Annotated[ + str, + Option( + '--plugin-name-blender', + '-n', + help='name of the plugin in blender', + ), + ] = None, + # engine config + blender_exec: Annotated[ + Path, + Option('--blender-exec', help='path to blender executable, e.g. /usr/bin/blender'), + ] = None, + # misc + debug: Annotated[ + bool, + Option('--debug/--no-debug', help='log in debug mode'), + ] = False, +): + """Install a blender plugin with a command line interface.""" + logger = Logger.setup_logging(level='DEBUG' if debug else 'INFO') + logger.info( + dedent( + f"""\ + :rocket: Starting: + Executing install plugin with the following parameters: + --------------------------------------------------------- + [yellow]# path config[/yellow] + - plugin_path: {plugin_path} + - plugin_name_blender: {plugin_name_blender} + [yellow]# engine config[/yellow] + - blender_exec: {blender_exec} + [yellow]# misc[/yellow] + - debug: {debug} + ---------------------------------------------------------""" + ) + ) + + with xf.init_blender(exec_path=blender_exec, background=True) as xf_runner: + xf_runner.utils.install_plugin(plugin_path, plugin_name_blender) + + logger.info(':tada: [green]Installation of plugin completed![/green]') + + +if __name__ == '__main__': + app() diff --git a/xrfeitoria/cmd/render.py b/xrfeitoria/cmd/blender/render.py similarity index 100% rename from xrfeitoria/cmd/render.py rename to xrfeitoria/cmd/blender/render.py diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index 9b1cfaee..b0498369 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -1,7 +1,7 @@ """Remote functions for blender.""" from pathlib import Path -from typing import Dict, List, Literal, Tuple +from typing import Dict, List, Literal, Optional, Tuple from ...data_structure.constants import ImportFileFormatEnum, PathLike, Vector from ...rpc import remote_blender @@ -314,3 +314,20 @@ def enable_gpu(gpu_num: int = 1): gpu_num (int, optional): Number of GPUs to use. Defaults to 1. """ XRFeitoriaBlenderFactory.enable_gpu(gpu_num=gpu_num) + + +@remote_blender() +def install_plugin(plugin_path: 'PathLike', plugin_name_blender: 'Optional[str]' = None): + """Install plugin in blender. + + Args: + path (PathLike): Path to the plugin. + """ + bpy.ops.preferences.addon_install(filepath=Path(plugin_path).resolve().as_posix()) + if plugin_name_blender is None: + plugin_name_blender = Path(plugin_path).stem + logger.warning(f'Plugin name not specified, use {plugin_name_blender} as default.') + bpy.ops.preferences.addon_enable(module=plugin_name_blender) + bpy.ops.wm.save_userpref() + + logger.info(f'Plugin {plugin_name_blender} installed successfully.') From 9a7884d776187addfeb6943c1419bd2ceb616797 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 2 Nov 2023 23:13:30 +0800 Subject: [PATCH 012/110] default: `console_variables = {'r.MotionBlurQuality': 0}` --- xrfeitoria/data_structure/models.py | 2 +- xrfeitoria/renderer/renderer_unreal.py | 16 +++++++++++----- xrfeitoria/sequence/sequence_base.py | 13 +++++++++++++ xrfeitoria/sequence/sequence_unreal.py | 8 ++++---- xrfeitoria/sequence/sequence_unreal.pyi | 10 +++++----- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/xrfeitoria/data_structure/models.py b/xrfeitoria/data_structure/models.py index cfc49da0..0f93c4ec 100644 --- a/xrfeitoria/data_structure/models.py +++ b/xrfeitoria/data_structure/models.py @@ -242,7 +242,7 @@ class AntiAliasSetting(BaseModel): description='File name format of the render job.', ) console_variables: Dict[str, float] = Field( - default={}, + default={'r.MotionBlurQuality': 0}, description='Additional console variables of the render job. Ref to :ref:`FAQ-console-variables` for details.', ) anti_aliasing: AntiAliasSetting = Field( diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 2b3de96b..3fd8dd9d 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -39,7 +39,7 @@ def add_job( resolution: Tuple[int, int], render_passes: 'List[RenderPass]', file_name_format: str = '{sequence_name}/{render_pass}/{camera_name}/{frame_number}', - console_variables: Dict[str, float] = {}, + console_variables: Dict[str, float] = {'r.MotionBlurQuality': 0}, anti_aliasing: 'Optional[RenderJob.AntiAliasSetting]' = None, export_vertices: bool = False, export_skeleton: bool = False, @@ -53,7 +53,7 @@ def add_job( resolution (Tuple[int, int]): Resolution of the output image. render_passes (List[RenderPass]): Render passes to render. file_name_format (str, optional): File name format of the output image. Defaults to ``{sequence_name}/{render_pass}/{camera_name}/{frame_number}``. - console_variables (Dict[str, float], optional): Console variables to set. Defaults to {}. + console_variables (Dict[str, float], optional): Console variables to set. Defaults to ``{'r.MotionBlurQuality': 0}``. Ref to :ref:`FAQ-console-variables` for details. anti_aliasing (Optional[RenderJob.AntiAliasSetting], optional): Anti aliasing setting. Defaults to None. export_vertices (bool, optional): Whether to export vertices. Defaults to False. @@ -67,7 +67,11 @@ def add_job( # turn off motion blur by default if 'r.MotionBlurQuality' not in console_variables.keys(): - console_variables['r.MotionBlurQuality'] = 0 + logger.warning( + "Seems you gave a console variable dict in ``add_to_renderer(console_variables=...)``, " + 'and it replaces the default ``r.MotionBlurQuality`` setting, which would open the motion blur in rendering. ' + "If you want to turn off the motion blur the same as default, set ``console_variables={..., 'r.MotionBlurQuality': 0}``." + ) job = RenderJob( map_path=map_path, @@ -182,7 +186,7 @@ def convert_camera(camera_file: Path) -> None: camera_file.unlink() def convert_vertices(folder: Path) -> None: - """Convert vertices from `.bin` to `.npz`. Merge all vertices files into one + """Convert vertices from `.dat` to `.npz`. Merge all vertices files into one `.npz` file. Args: @@ -247,13 +251,15 @@ def convert_actor_infos(folder: Path) -> None: # 2. convert actor infos from `.dat` to `.json` convert_actor_infos(folder=seq_path / RenderOutputEnumUnreal.actor_infos.value) - # 3. convert vertices from `.bin` to `.npz` + # 3. convert vertices from `.dat` to `.npz` if job.export_vertices: # glob actors in {seq_path}/vertices/* actor_folders = sorted(seq_path.glob(f'{RenderOutputEnumUnreal.vertices.value}/*')) for actor_folder in actor_folders: convert_vertices(actor_folder) + # 4. convert skeleton from `.dat` to `.json` + @staticmethod def _add_job_in_engine(job: 'Dict[str, Any]') -> None: _job = XRFeitoriaUnrealFactory.constants.RenderJobUnreal(**job) diff --git a/xrfeitoria/sequence/sequence_base.py b/xrfeitoria/sequence/sequence_base.py index 24e265ad..554a3e0e 100644 --- a/xrfeitoria/sequence/sequence_base.py +++ b/xrfeitoria/sequence/sequence_base.py @@ -97,6 +97,19 @@ def import_actor( scale: 'Vector' = None, stencil_value: int = 1, ) -> ActorBase: + """Imports an actor from a file and adds it to the sequence. + + Args: + file_path (PathLike): The path to the file containing the actor to import. + actor_name (Optional[str], optional): The name to give the imported actor. If not provided, a name will be generated automatically. Defaults to None. + location (Vector, optional): The initial location of the actor. Defaults to None. + rotation (Vector, optional): The initial rotation of the actor. Defaults to None. + scale (Vector, optional): The initial scale of the actor. Defaults to None. + stencil_value (int, optional): The stencil value to use for the actor. Defaults to 1. + + Returns: + ActorBase: The imported actor. + """ if actor_name is None: actor_name = cls._object_utils.generate_obj_name(obj_type='actor') # judge file path diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index e705cee8..81b0a183 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -41,7 +41,7 @@ def add_to_renderer( resolution: Tuple[int, int], render_passes: 'List[RenderPass]', file_name_format: str = '{sequence_name}/{render_pass}/{camera_name}/{frame_number}', - console_variables: Dict[str, float] = {}, + console_variables: Dict[str, float] = {'r.MotionBlurQuality': 0}, anti_aliasing: 'Optional[RenderJobUnreal.AntiAliasSetting]' = None, export_vertices: bool = False, export_skeleton: bool = False, @@ -57,7 +57,7 @@ def add_to_renderer( render_passes (List[RenderPass]): The list of render passes to be rendered. file_name_format (str, optional): The format of the output file name. Defaults to ``{sequence_name}/{render_pass}/{camera_name}/{frame_number}``. - console_variables (Dict[str, float], optional): The console variables to be set before rendering. Defaults to {}. + console_variables (Dict[str, float], optional): The console variables to be set before rendering. Defaults to {'r.MotionBlurQuality': 0}. Ref to :ref:`FAQ-stencil-value` for details. anti_aliasing (Optional[RenderJobUnreal.AntiAliasSetting], optional): The anti-aliasing settings for the render job. Defaults to None. @@ -101,8 +101,8 @@ def add_to_renderer( def spawn_actor( cls, actor_asset_path: str, - location: 'Vector', - rotation: 'Vector', + location: 'Vector' = None, + rotation: 'Vector' = None, scale: 'Optional[Vector]' = None, actor_name: Optional[str] = None, stencil_value: int = 1, diff --git a/xrfeitoria/sequence/sequence_unreal.pyi b/xrfeitoria/sequence/sequence_unreal.pyi index 2c2ad6c2..938ac92b 100644 --- a/xrfeitoria/sequence/sequence_unreal.pyi +++ b/xrfeitoria/sequence/sequence_unreal.pyi @@ -30,11 +30,11 @@ class SequenceUnreal(SequenceBase): output_path: PathLike, resolution: Tuple[int, int], render_passes: List[RenderPass], - file_name_format: str = ..., - console_variables: Dict[str, float] = ..., - anti_aliasing: 'Optional[RenderJobUnreal.AntiAliasSetting]' = ..., - export_vertices: bool = ..., - export_skeleton: bool = ..., + file_name_format: str = '{sequence_name}/{render_pass}/{camera_name}/{frame_number}', + console_variables: Dict[str, float] = {'r.MotionBlurQuality': 0}, + anti_aliasing: 'Optional[RenderJobUnreal.AntiAliasSetting]' = None, + export_vertices: bool = False, + export_skeleton: bool = False, ) -> None: ... @classmethod def spawn_camera( From ad1deb4ebdbd30e19238895c1184a5dcc9bf4b5d Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 2 Nov 2023 23:14:05 +0800 Subject: [PATCH 013/110] [Fix] bugs --- .../Content/Python/custom_movie_pipeline.py | 2 +- .../Content/Python/sequence.py | 7 ++-- xrfeitoria/renderer/renderer_unreal.py | 34 ++++++++++++++++--- xrfeitoria/utils/tools.py | 12 ++++++- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py b/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py index 4eaceeee..30f8f8b7 100644 --- a/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py +++ b/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py @@ -224,7 +224,7 @@ def add_anti_alias( anti_alias_config.override_anti_aliasing = True if anti_alias.warmup_frames: anti_alias_config.use_camera_cut_for_warm_up = True - anti_alias_config.render_warm_up_count = anti_alias['warmup_frames'] + anti_alias_config.render_warm_up_count = anti_alias.warmup_frames if anti_alias.render_warmup_frame: anti_alias_config.render_warm_up_frames = True diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 2b09e277..873fb77e 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -46,9 +46,10 @@ def get_animation_length(animation_asset: unreal.AnimSequence, seq_fps: float = # TODO: check if this is true anim_frame_rate = animation_asset.get_editor_property('target_frame_rate') anim_frame_rate = convert_frame_rate_to_fps(anim_frame_rate) - assert ( - anim_frame_rate == seq_fps - ), f'anim fps {anim_frame_rate} != seq fps {seq_fps}, this would cause animation interpolation.' + if anim_frame_rate == seq_fps: + unreal.log_warning( + f'anim fps {anim_frame_rate} != seq fps {seq_fps}, this would cause animation interpolation.' + ) anim_len = animation_asset.get_editor_property('number_of_sampled_frames') diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 3fd8dd9d..6fa46c0c 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -68,7 +68,7 @@ def add_job( # turn off motion blur by default if 'r.MotionBlurQuality' not in console_variables.keys(): logger.warning( - "Seems you gave a console variable dict in ``add_to_renderer(console_variables=...)``, " + 'Seems you gave a console variable dict in ``add_to_renderer(console_variables=...)``, ' 'and it replaces the default ``r.MotionBlurQuality`` setting, which would open the motion blur in rendering. ' "If you want to turn off the motion blur the same as default, set ``console_variables={..., 'r.MotionBlurQuality': 0}``." ) @@ -164,7 +164,6 @@ def render_jobs(cls) -> None: # cls.clear() server.close() - # post process, including: convert cam params. cls._post_process() # clear render queue @@ -172,7 +171,17 @@ def render_jobs(cls) -> None: @classmethod def _post_process(cls) -> None: + """Post-processes the rendered output by: + - converting camera parameters: from `.dat` to `.json` + - convert actor infos: from `.dat` to `.json` + - convert vertices: from `.dat` to `.npz` + - convert skeleton: from `.dat` to `.npz` + + This method is called after rendering is complete. + """ import numpy as np # isort:skip + from rich import get_console # isort:skip + from rich.spinner import Spinner # isort:skip from ..camera.camera_parameter import CameraParameter # isort:skip def convert_camera(camera_file: Path) -> None: @@ -186,8 +195,7 @@ def convert_camera(camera_file: Path) -> None: camera_file.unlink() def convert_vertices(folder: Path) -> None: - """Convert vertices from `.dat` to `.npz`. Merge all vertices files into one - `.npz` file. + """Convert vertices from `.dat` to `.npz`. Merge all vertices files into one `.npz` file with structures of: {'verts': np.ndarray, 'faces': None} Args: folder (Path): Path to the folder containing vertices files. @@ -238,10 +246,21 @@ def convert_actor_infos(folder: Path) -> None: # Remove the folder shutil.rmtree(folder) - for job in cls.render_queue: + console = get_console() + try: + spinner: Spinner = console._live.renderable + except AttributeError: + status = console.status('[bold green]:rocket: Rendering...[/bold green]') + status.start() + spinner: Spinner = status.renderable + + for idx, job in enumerate(cls.render_queue): seq_name = job.sequence_path.split('/')[-1] seq_path = Path(job.output_path).resolve() / seq_name + text = f'job {idx + 1}/{len(cls.render_queue)}: seq_name="{seq_name}", post-processing...' + spinner.update(text=text) + # 1. convert camera parameters from `.bat` to `.json` with xrprimer # glob camera files in {seq_path}/{cam_param_dir}/* camera_files = sorted(seq_path.glob(f'{RenderOutputEnumUnreal.camera_params.value}/*.dat')) @@ -259,6 +278,11 @@ def convert_actor_infos(folder: Path) -> None: convert_vertices(actor_folder) # 4. convert skeleton from `.dat` to `.json` + if job.export_skeleton: + # glob actors in {seq_path}/skeleton/* + actor_folders = sorted(seq_path.glob(f'{RenderOutputEnumUnreal.skeleton.value}/*')) + for actor_folder in actor_folders: + convert_vertices(actor_folder) @staticmethod def _add_job_in_engine(job: 'Dict[str, Any]') -> None: diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index 9ef265a1..b4d7ac3d 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -68,7 +68,11 @@ def filter_unique(cls, record: 'loguru.Record', level_name: str = 'WARNING') -> @classmethod def setup_logging( - cls, level: str = 'INFO', log_path: 'Optional[PathLike]' = None, replace: bool = True + cls, + level: str = 'INFO', + log_path: 'Optional[PathLike]' = None, + replace: bool = True, + log_rpc_code: bool = False, ) -> 'loguru.Logger': """Setup logging to file and console. @@ -77,7 +81,13 @@ def setup_logging( "ERROR", "CRITICAL". log_path (Path, optional): path to save the log file. Defaults to None. replace (bool, optional): replace the log file if exists. Defaults to True. + log_rpc_code (bool, optional): print the rpc code sending to engine. Defaults to False. """ + if log_rpc_code: + import os + + os.environ['RPC_DEBUG'] = '1' + if cls.is_setup: return logger From 254d7ec81e3e533fb244d37dfa8c51f092c64c58 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 2 Nov 2023 23:14:29 +0800 Subject: [PATCH 014/110] Optimize Unreal Mesh Operator --- .../Private/MoviePipelineMeshOperator.cpp | 151 ++++++++++-------- .../Public/MoviePipelineMeshOperator.h | 5 +- .../XRFeitoriaUnreal.Build.cs | 1 + src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin | 2 +- 4 files changed, 91 insertions(+), 68 deletions(-) diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp index c96de843..eb9f5195 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp @@ -5,6 +5,8 @@ #include "XF_BlueprintFunctionLibrary.h" #include "Engine/StaticMeshActor.h" #include "Animation/SkeletalMeshActor.h" +#include "LevelSequenceEditorBlueprintLibrary.h" +#include "MovieSceneObjectBindingID.h" #include "Camera/CameraActor.h" #include "Camera/CameraComponent.h" #include "Misc/FileHelper.h" @@ -21,86 +23,103 @@ void UMoviePipelineMeshOperator::SetupForPipelineImpl(UMoviePipeline* InPipeline ULevelSequence* LevelSequence = GetPipeline()->GetTargetSequence(); UMovieSceneSequence* MovieSceneSequence = GetPipeline()->GetTargetSequence(); UMovieScene* MovieScene = LevelSequence->GetMovieScene(); - TArray bindings = MovieScene->GetBindings(); - TArray bindingProxies; - for (FMovieSceneBinding binding : bindings) + TMap bindingMap; + for (int idx = 0; idx < MovieScene->GetSpawnableCount(); idx++) { - FGuid guid = binding.GetObjectGuid(); - bindingProxies.Add(FSequencerBindingProxy(guid, MovieSceneSequence)); + FMovieSceneSpawnable spawnable = MovieScene->GetSpawnable(idx); + FGuid guid = spawnable.GetGuid(); + FString name = spawnable.GetName(); + + bindingMap.Add(name, guid); } - boundObjects = USequencerToolsFunctionLibrary::GetBoundObjects( - GetPipeline()->GetWorld(), - LevelSequence, - bindingProxies, - FSequencerScriptingRange::FromNative( - MovieScene->GetPlaybackRange(), - MovieScene->GetDisplayRate() - ) - ); + for (int idx = 0; idx < MovieScene->GetPossessableCount(); idx++) + { + FMovieScenePossessable possessable = MovieScene->GetPossessable(idx); + FGuid guid = possessable.GetGuid(); + FString name = possessable.GetName(); + + bindingMap.Add(name, guid); + } - for (FSequencerBoundObjects boundObject : boundObjects) + for (TPair pair : bindingMap) { - // loop over bound objects - UObject* BoundObject = boundObject.BoundObjects[0]; // only have one item + FString name = pair.Key; + FGuid guid = pair.Value; + + TArray _boundObjects_ = USequencerToolsFunctionLibrary::GetBoundObjects( + GetPipeline()->GetWorld(), + LevelSequence, + TArray({ FSequencerBindingProxy(guid, MovieSceneSequence) }), + FSequencerScriptingRange::FromNative( + MovieScene->GetPlaybackRange(), + MovieScene->GetDisplayRate() + ) + ); + + UObject* BoundObject = _boundObjects_[0].BoundObjects[0]; // only have one item if (BoundObject->IsA(ASkeletalMeshActor::StaticClass())) { ASkeletalMeshActor* SkeletalMeshActor = Cast(BoundObject); - SkeletalMeshComponents.Add(SkeletalMeshActor->GetSkeletalMeshComponent()); + SkeletalMeshComponents.Add(name, SkeletalMeshActor->GetSkeletalMeshComponent()); } else if (BoundObject->IsA(AStaticMeshActor::StaticClass())) { AStaticMeshActor* StaticMeshActor = Cast(BoundObject); - StaticMeshComponents.Add(StaticMeshActor->GetStaticMeshComponent()); - } - else if (BoundObject->IsA(USkeletalMeshComponent::StaticClass())) - { - USkeletalMeshComponent* SkeletalMeshComponent = Cast(BoundObject); - // check if it's already in the list - bool bFound = false; - for (USkeletalMeshComponent* SkeletalMeshComponentInList : SkeletalMeshComponents) - { - if (SkeletalMeshComponentInList == SkeletalMeshComponent) - { - bFound = true; - break; - } - } - if (!bFound) SkeletalMeshComponents.Add(SkeletalMeshComponent); - } - else if (BoundObject->IsA(UStaticMeshComponent::StaticClass())) - { - UStaticMeshComponent* StaticMeshComponent = Cast(BoundObject); - // check if it's already in the list - bool bFound = false; - for (UStaticMeshComponent* StaticMeshComponentInList : StaticMeshComponents) - { - if (StaticMeshComponentInList == StaticMeshComponent) - { - bFound = true; - break; - } - } - if (!bFound) - StaticMeshComponents.Add(StaticMeshComponent); + StaticMeshComponents.Add(name, StaticMeshActor->GetStaticMeshComponent()); } + //else if (BoundObject->IsA(USkeletalMeshComponent::StaticClass())) + //{ + // USkeletalMeshComponent* SkeletalMeshComponent = Cast(BoundObject); + // // check if it's already in the list + // bool bFound = false; + // for (TPair SKMPair : SkeletalMeshComponents) + // { + // USkeletalMeshComponent* SkeletalMeshComponentInList = SKMPair.Value; + // if (SkeletalMeshComponentInList == SkeletalMeshComponent) + // { + // bFound = true; + // break; + // } + // } + // if (!bFound) SkeletalMeshComponents.Add(name, SkeletalMeshComponent); + //} + //else if (BoundObject->IsA(UStaticMeshComponent::StaticClass())) + //{ + // UStaticMeshComponent* StaticMeshComponent = Cast(BoundObject); + // // check if it's already in the list + // bool bFound = false; + // for (TPair SKMPair : StaticMeshComponents) + // { + // UStaticMeshComponent* StaticMeshComponentInList = SKMPair.Value; + // if (StaticMeshComponentInList == StaticMeshComponent) + // { + // bFound = true; + // break; + // } + // } + // if (!bFound) StaticMeshComponents.Add(name, StaticMeshComponent); + //} } } void UMoviePipelineMeshOperator::OnReceiveImageDataImpl(FMoviePipelineMergerOutputFrame* InMergedOutputFrame) { - for (USkeletalMeshComponent* SkeletalMeshComponent : SkeletalMeshComponents) + for (TPair SKMPair : SkeletalMeshComponents) { // loop over Skeletal mesh components if (!SkeletalMeshOperatorOption.bEnabled) continue; - // Actor in level - FString MeshNameFromLabel = SkeletalMeshComponent->GetOwner()->GetActorNameOrLabel(); - // Actor spawned from sequence - FString MeshNameFromName = SkeletalMeshComponent->GetOwner()->GetFName().GetPlainNameString(); - // Judge which name is correct - FString MeshName = MeshNameFromName.StartsWith("SkeletalMesh") ? MeshNameFromLabel : MeshNameFromName; + FString MeshName = SKMPair.Key; + USkeletalMeshComponent* SkeletalMeshComponent = SKMPair.Value; + + //// Actor in level + //FString MeshNameFromLabel = SkeletalMeshComponent->GetOwner()->GetActorNameOrLabel(); + //// Actor spawned from sequence + //FString MeshNameFromName = SkeletalMeshComponent->GetOwner()->GetFName().GetPlainNameString(); + //// Judge which name is correct + //FString MeshName = MeshNameFromName.StartsWith("SkeletalMesh") ? MeshNameFromLabel : MeshNameFromName; if (SkeletalMeshOperatorOption.bSaveVerticesPosition) { @@ -198,17 +217,20 @@ void UMoviePipelineMeshOperator::OnReceiveImageDataImpl(FMoviePipelineMergerOutp // // TODO: export to npz //} } - for (UStaticMeshComponent* StaticMeshComponent : StaticMeshComponents) + for (TPair SKMPair : StaticMeshComponents) { // loop over static mesh components if (!StaticMeshOperatorOption.bEnabled) continue; - // Actor in level - FString MeshNameFromLabel = StaticMeshComponent->GetOwner()->GetActorNameOrLabel(); - // Actor spawned from sequence - FString MeshNameFromName = StaticMeshComponent->GetOwner()->GetFName().GetPlainNameString(); - // Judge which name is correct - FString MeshName = MeshNameFromName.StartsWith("StaticMesh") ? MeshNameFromLabel : MeshNameFromName; + UStaticMeshComponent* StaticMeshComponent = SKMPair.Value; + FString MeshName = SKMPair.Key; + + //// Actor in level + //FString MeshNameFromLabel = StaticMeshComponent->GetOwner()->GetActorNameOrLabel(); + //// Actor spawned from sequence + //FString MeshNameFromName = StaticMeshComponent->GetOwner()->GetFName().GetPlainNameString(); + //// Judge which name is correct + //FString MeshName = MeshNameFromName.StartsWith("StaticMesh") ? MeshNameFromLabel : MeshNameFromName; if (StaticMeshOperatorOption.bSaveVerticesPosition) { @@ -236,6 +258,7 @@ void UMoviePipelineMeshOperator::OnReceiveImageDataImpl(FMoviePipelineMergerOutp StaticMeshOperatorOption.DirectoryVertices / MeshName, "dat", &InMergedOutputFrame->FrameOutputState)); } } + if (bIsFirstFrame) bIsFirstFrame = false; } diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h index bcc5e398..c8e7ed77 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h @@ -101,8 +101,7 @@ class XRFEITORIAUNREAL_API UMoviePipelineMeshOperator : public UMoviePipelineOut FSkeletalMeshOperatorOption SkeletalMeshOperatorOption = FSkeletalMeshOperatorOption(); private: - TArray boundObjects; - TArray StaticMeshComponents; - TArray SkeletalMeshComponents; + TMap StaticMeshComponents; + TMap SkeletalMeshComponents; bool bIsFirstFrame = true; }; diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs index ffad6ba7..999501ed 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs @@ -45,6 +45,7 @@ public XRFeitoriaUnreal(ReadOnlyTargetRules Target) : base(Target) "MovieSceneTools", "MovieSceneTracks", "LevelSequence", + "LevelSequenceEditor", "SequencerScripting", "SequencerScriptingEditor", } diff --git a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin index 77b63de2..bbbdc06b 100644 --- a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin +++ b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin @@ -37,6 +37,6 @@ { "Name": "SequencerScripting", "Enabled": true - }, + } ] } From 9d94e4f8073e6dc39f0da0c5104ca889cdda9a3f Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 2 Nov 2023 23:14:53 +0800 Subject: [PATCH 015/110] pre-commit --- xrfeitoria/renderer/renderer_unreal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 6fa46c0c..00f0e627 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -195,7 +195,8 @@ def convert_camera(camera_file: Path) -> None: camera_file.unlink() def convert_vertices(folder: Path) -> None: - """Convert vertices from `.dat` to `.npz`. Merge all vertices files into one `.npz` file with structures of: {'verts': np.ndarray, 'faces': None} + """Convert vertices from `.dat` to `.npz`. Merge all vertices files into one + `.npz` file with structure of: {'verts': np.ndarray, 'faces': None} Args: folder (Path): Path to the folder containing vertices files. From 8c0b7f920b40646fbc8f7e7f3bcae6929e60327c Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 6 Nov 2023 18:05:13 +0800 Subject: [PATCH 016/110] move anim to utils --- pyproject.toml | 7 +++++-- samples/blender/07_amass.py | 3 +-- xrfeitoria/utils/anim/__init__.py | 1 + {samples => xrfeitoria/utils}/anim/constants.py | 0 {samples => xrfeitoria/utils}/anim/motion.py | 0 {samples => xrfeitoria/utils}/anim/transform3d.py | 0 {samples => xrfeitoria/utils}/anim/utils.py | 0 7 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 xrfeitoria/utils/anim/__init__.py rename {samples => xrfeitoria/utils}/anim/constants.py (100%) rename {samples => xrfeitoria/utils}/anim/motion.py (100%) rename {samples => xrfeitoria/utils}/anim/transform3d.py (100%) rename {samples => xrfeitoria/utils}/anim/utils.py (100%) diff --git a/pyproject.toml b/pyproject.toml index c4581022..5ee9fcb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,10 @@ dependencies = [ dynamic = ['version'] [project.optional-dependencies] -all = ["xrfeitoria[doc,vis]"] +all = ["xrfeitoria[anim,doc,vis]"] +anim = [ + "scipy>1,<2" +] doc = [ "autodoc_pydantic==2.0.1", "docutils", @@ -60,7 +63,7 @@ doc = [ "sphinx-tabs==3.4.1", "enum-tools[sphinx]", ] -vis=[ +vis = [ "matplotlib>=3.4,<4", "opencv-python>=4,<5", "flow_vis==0.1", diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index f228a016..eb11b80f 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -12,10 +12,9 @@ """ import xrfeitoria as xf from xrfeitoria.rpc import remote_blender +from xrfeitoria.utils.anim import load_amass_motion from xrfeitoria.utils.tools import Logger -from ..anim.utils import load_amass_motion - @remote_blender() def apply_scale(actor_name: str): diff --git a/xrfeitoria/utils/anim/__init__.py b/xrfeitoria/utils/anim/__init__.py new file mode 100644 index 00000000..835692ca --- /dev/null +++ b/xrfeitoria/utils/anim/__init__.py @@ -0,0 +1 @@ +from .utils import load_amass_motion diff --git a/samples/anim/constants.py b/xrfeitoria/utils/anim/constants.py similarity index 100% rename from samples/anim/constants.py rename to xrfeitoria/utils/anim/constants.py diff --git a/samples/anim/motion.py b/xrfeitoria/utils/anim/motion.py similarity index 100% rename from samples/anim/motion.py rename to xrfeitoria/utils/anim/motion.py diff --git a/samples/anim/transform3d.py b/xrfeitoria/utils/anim/transform3d.py similarity index 100% rename from samples/anim/transform3d.py rename to xrfeitoria/utils/anim/transform3d.py diff --git a/samples/anim/utils.py b/xrfeitoria/utils/anim/utils.py similarity index 100% rename from samples/anim/utils.py rename to xrfeitoria/utils/anim/utils.py From 0e731d2a132e6dca63aa8c557039b227cef7e1a3 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 7 Nov 2023 17:04:38 +0800 Subject: [PATCH 017/110] clear `RPCFactory` when close --- xrfeitoria/rpc/factory.py | 8 ++++++++ xrfeitoria/utils/runner.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/xrfeitoria/rpc/factory.py b/xrfeitoria/rpc/factory.py index 4d8f58bc..75820e66 100644 --- a/xrfeitoria/rpc/factory.py +++ b/xrfeitoria/rpc/factory.py @@ -31,6 +31,14 @@ class RPCFactory: default_imports = [] registered_function_names = [] + @classmethod + def clear(cls): + cls.rpc_client = None + cls.file_path = None + cls.remap_pairs = [] + cls.default_imports = [] + cls.registered_function_names.clear() + @classmethod def setup(cls, port: int, remap_pairs: List[str] = None, default_imports: List[str] = None): """Sets up the RPC factory. diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 93f9ca2e..ea2c0666 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -165,7 +165,8 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: def stop(self) -> None: """Stop rpc server.""" - import psutil + import psutil # isort:skip + from ..rpc.factory import RPCFactory # isort:skip process = self.engine_process if process is not None: @@ -180,6 +181,9 @@ def stop(self) -> None: else: logger.info(':bell: [bold red]Exiting runner[/bold red], reused engine process remains') + # clear rpc server + RPCFactory.clear() + def reuse(self) -> bool: """Try to reuse existing engine process. From d21d817a06f5ae0b4d270a2616358e738d4b3288 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 10 Nov 2023 14:41:38 +0800 Subject: [PATCH 018/110] add comment --- xrfeitoria/utils/functions/blender_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index b0498369..efe9f6cb 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -101,6 +101,7 @@ def cleanup_unused(): @remote_blender() def save_blend(save_path: 'PathLike' = None, pack: bool = False): """Save the current blend file to the given path. + If no path is given, save to the current blend file path. Args: save_path (PathLike, optional): Path to save the blend file. Defaults to None. From 56a9c7bb171f8f988e04b3e2d1b140d592f5296e Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 10 Nov 2023 14:43:01 +0800 Subject: [PATCH 019/110] Update comment --- src/XRFeitoriaBpy/core/factory.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 052edfea..897e4027 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -999,7 +999,8 @@ def import_glb(glb_file: str) -> None: raise Exception(f'Failed to import glb: {glb_file}\n{e}') def import_mo_json(mo_json_file: Path, actor_name: str) -> None: - """Import an animation from json, and apply the animation to the given actor. + """Import an animation from json, and apply the animation to the given actor. In + form of quaternion. Args: mo_json_file (Path): json file path. @@ -1052,14 +1053,15 @@ def apply_action_to_actor(action: 'bpy.types.Action', actor: 'bpy.types.Object') actor.animation_data.action = action def apply_motion_data_to_action( - motion_data: 'List[Dict[str, Dict]]', + motion_data: 'List[Dict[str, Dict[str, List[float]]]]', action: 'bpy.types.Action', scale: float = 1.0, ) -> None: """Apply motion data in dict to object. Args: - motion_data (List[Dict[str, Dict]]): Motion data in the form of dict, normally imported from json. + motion_data (List[Dict[str, Dict]]): Motion data in the form of dict, + containing rotation (quaternion) and location. action (bpy.types.Action): Action. scale (float, optional): Scale of movement in location of animation. Defaults to 1.0. """ @@ -1109,7 +1111,7 @@ def apply_motion_data_to_actor(motion_data: 'List[Dict[str, Dict[str, List[float """Applies motion data to a given actor. Args: - motion_data: A list of dictionaries containing motion data for the actor. + motion_data: A list of dictionaries containing motion data (quaternion) for the actor. actor_name: The name of the actor to apply the motion data to. """ action = bpy.data.actions.new('Action') From d7762e3c34e8376f92b8d4ddd6595e0cf92aa4c7 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 10 Nov 2023 14:45:15 +0800 Subject: [PATCH 020/110] pre-commit --- xrfeitoria/utils/functions/blender_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index efe9f6cb..2b08780e 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -100,8 +100,8 @@ def cleanup_unused(): @remote_blender() def save_blend(save_path: 'PathLike' = None, pack: bool = False): - """Save the current blend file to the given path. - If no path is given, save to the current blend file path. + """Save the current blend file to the given path. If no path is given, save to the + current blend file path. Args: save_path (PathLike, optional): Path to save the blend file. Defaults to None. From d112af0438ef7c6943f376bae888133687b5a411 Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Mon, 13 Nov 2023 11:18:35 +0800 Subject: [PATCH 021/110] add material class --- src/XRFeitoriaBpy/core/factory.py | 28 ++++++ xrfeitoria/actor/actor_blender.py | 30 +++++++ xrfeitoria/data_structure/constants.py | 8 ++ xrfeitoria/factory.py | 3 + xrfeitoria/material/__init__.py | 0 xrfeitoria/material/material_base.py | 96 ++++++++++++++++++++ xrfeitoria/material/material_blender.py | 113 ++++++++++++++++++++++++ xrfeitoria/material/material_unreal.py | 0 8 files changed, 278 insertions(+) create mode 100644 xrfeitoria/material/__init__.py create mode 100644 xrfeitoria/material/material_base.py create mode 100644 xrfeitoria/material/material_blender.py create mode 100644 xrfeitoria/material/material_unreal.py diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 897e4027..dd2708bc 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -934,6 +934,19 @@ def get_active_cameras(scene: 'bpy.types.Scene') -> List[str]: ##################################### ############### Import ############## ##################################### + def import_texture(texture_file: str) -> bpy.types.Image: + """Import an image as a texture + + Args: + texture_file (str): File path of the image. + Returns: + bpy.types.Image: The imported texture. + """ + try: + texture = bpy.data.images.load(filepath=str(texture_file)) + except Exception: + raise Exception(f"Failed to import texture: {texture_file}") + return texture def import_fbx(fbx_file: str) -> None: """Import an fbx file. Only support binary fbx. @@ -1259,3 +1272,18 @@ def get_bound_box_in_world_space( return bbox_min, bbox_max else: raise ValueError(f'Invalid object type: {obj.type}') + + ##################################### + ############# Material ############## + ##################################### + def get_material(mat_name: str) -> 'bpy.types.Material': + if mat_name not in bpy.data.materials.keys(): + raise ValueError(f"Material '{mat_name}' does not exists in this blend file.") + return bpy.data.materials[mat_name] + + def new_mat_node(mat: 'bpy.types.Material', type: str, name: Optional[str] = None) -> bpy.types.Node: + _nodes = mat.node_tree.nodes + node = _nodes.new(type=type) + if name: + node.name = name + return node \ No newline at end of file diff --git a/xrfeitoria/actor/actor_blender.py b/xrfeitoria/actor/actor_blender.py index cb294f89..75eb4579 100644 --- a/xrfeitoria/actor/actor_blender.py +++ b/xrfeitoria/actor/actor_blender.py @@ -9,6 +9,7 @@ from ..utils import Validator from ..utils.functions import blender_functions from .actor_base import ActorBase +from ..material.material_blender import MaterialBlender try: import bpy # isort:skip @@ -42,6 +43,10 @@ def set_transform_keys(self, transform_keys: 'TransformKeys') -> None: transform_keys = [transform_keys] transform_keys = [i.model_dump() for i in transform_keys] self._object_utils.set_transform_keys(name=self.name, transform_keys=transform_keys) + + def set_material(self, mat: MaterialBlender) -> None: + self._set_material_in_engine(actor_name=self.name, mat_name=mat._name) + ##################################### ###### RPC METHODS (Private) ######## @@ -127,6 +132,31 @@ def _import_animation_from_file_in_engine(animation_path: str, actor_name: str, XRFeitoriaBlenderFactory.import_mo_fbx(mo_fbx_file=animation_path, actor_name=actor_name) else: raise TypeError(f"Invalid anim file, expected 'json', 'blend', or 'fbx' (got {anim_file_ext[1:]} instead).") + + @staticmethod + def _set_material_in_engine(actor_name: str, mat_name: str) -> None: + """Set material to an actor. If the actor has multiple meshes, set material to the 1st mesh. + + Args: + actor_name (str): Name of the actor. + mat_name (str): Name of the material. + """ + actor = bpy.data.objects[actor_name] + material = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) + + if actor.type == "ARMATURE": + if len(actor.children) == 0: + raise TypeError(f"Actor {actor_name} has no meshes, thus cannot set material to it.") + mesh = actor.children[0] + elif actor.type == "MESH": + mesh = actor + + if mesh.data.materials: + # assign to 1-st material slot + mesh.data.materials[0] = material + else: + # no existing slot + mesh.data.materials.append(material) @remote_blender(dec_class=True, suffix='_in_engine') diff --git a/xrfeitoria/data_structure/constants.py b/xrfeitoria/data_structure/constants.py index d02c3856..343f264a 100644 --- a/xrfeitoria/data_structure/constants.py +++ b/xrfeitoria/data_structure/constants.py @@ -175,3 +175,11 @@ class ShapeTypeEnumUnreal(EnumBase): cylinder = 'cylinder' plane = 'plane' sphere = 'sphere' + + +class BSDFNodeLinkEnumBlender(EnumBase): + """Shader node link enum of Blender""" + + diffuse = "Base Color" + normal = "Normal" + roughness = "Roughness" \ No newline at end of file diff --git a/xrfeitoria/factory.py b/xrfeitoria/factory.py index 4d52a815..d8913a49 100644 --- a/xrfeitoria/factory.py +++ b/xrfeitoria/factory.py @@ -44,6 +44,7 @@ def __init__( from .object.object_utils import ObjectUtilsBlender # isort:skip from .camera.camera_blender import CameraBlender # isort:skip from .actor.actor_blender import ActorBlender, ShapeBlenderWrapper # isort:skip + from .material.material_blender import MaterialBlender # isort:skip from .renderer.renderer_blender import RendererBlender # isort:skip from .sequence.sequence_wrapper import SequenceWrapperBlender # isort:skip from .utils.runner import BlenderRPCRunner # isort:skip @@ -54,6 +55,7 @@ def __init__( self.ObjectUtils = ObjectUtilsBlender self.Camera = CameraBlender self.Actor = ActorBlender + self.Material = MaterialBlender self.Shape = ShapeBlenderWrapper self.Renderer = RendererBlender self.render = self.Renderer.render_jobs @@ -188,6 +190,7 @@ def __init__( pip install -e . python -c "import xrfeitoria as xf; xf.init_blender(replace_plugin=True, dev_plugin=True)" """ + _tls.cache = {'platform': None, 'engine_process': None, 'unreal_project_path': None} _tls.cache['platform'] = EngineEnum.blender self._cleanup = cleanup super().__init__( diff --git a/xrfeitoria/material/__init__.py b/xrfeitoria/material/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/xrfeitoria/material/material_base.py b/xrfeitoria/material/material_base.py new file mode 100644 index 00000000..7536b17f --- /dev/null +++ b/xrfeitoria/material/material_base.py @@ -0,0 +1,96 @@ +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Optional + +from loguru import logger +from typing_extensions import Self + +from ..data_structure.constants import PathLike +from ..object.object_utils import ObjectUtilsBase + + +class MaterialBase(ABC): + """Base material class.""" + + _object_utils = ObjectUtilsBase + + def __init__(self, name: str) -> None: + """ + Args: + name (str): name of the object + """ + self._name = name + + @classmethod + def new(cls, mat_name: str) -> Self: + cls._new_material_in_engine(mat_name) + return cls(mat_name) + + + def add_diffuse_texture( + self, + texture_file: PathLike, + texture_name: Optional[str] = None, + ) -> None: + """Add a diffuse texture to the material""" + if texture_name is None: + texture_name = Path(texture_file).stem + self._add_diffuse_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) + + + def add_normal_texture( + self, + texture_file: PathLike, + texture_name: Optional[str] = None, + ) -> None: + """Add a normal texture to the material""" + if texture_name is None: + texture_name = Path(texture_file).stem + self._add_normal_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) + + + def add_roughness_texture( + self, + texture_file: PathLike, + texture_name: Optional[str] = None, + ) -> None: + """Add a normal texture to the material""" + if texture_name is None: + texture_name = Path(texture_file).stem + self._add_roughness_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) + + ################################# + #### RPC METHODS (Private) #### + ################################# + + @staticmethod + @abstractmethod + def _new_material_in_engine(mat_name: str) -> None: + pass + + @staticmethod + @abstractmethod + def _add_diffuse_texture_in_engine( + mat_name: str, + texture_file: str, + texture_name: str, + ) -> None: + pass + + @staticmethod + @abstractmethod + def _add_normal_texture_in_engine( + mat_name: str, + texture_file: str, + texture_name: str, + ) -> None: + pass + + @staticmethod + @abstractmethod + def _add_roughness_texture_in_engine( + mat_name: str, + texture_file: str, + texture_name: str, + ) -> None: + pass \ No newline at end of file diff --git a/xrfeitoria/material/material_blender.py b/xrfeitoria/material/material_blender.py new file mode 100644 index 00000000..cefc0222 --- /dev/null +++ b/xrfeitoria/material/material_blender.py @@ -0,0 +1,113 @@ +import math +from typing import Dict, List, Optional, Tuple, Union + +from ..data_structure.constants import Vector, BSDFNodeLinkEnumBlender +from ..object.object_utils import ObjectUtilsBlender +from ..rpc import remote_blender +from ..utils import Validator +from .material_base import MaterialBase + +try: + import bpy # isort:skip + from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py +except ModuleNotFoundError: + pass + +try: + from ..data_structure.models import TransformKeys # isort:skip +except ModuleNotFoundError: + pass + + +@remote_blender(dec_class=True, suffix='_in_engine') +class MaterialBlender(MaterialBase): + """Material class for Blender.""" + + _object_utils = ObjectUtilsBlender + + ##################################### + ###### RPC METHODS (Private) ######## + ##################################### + + ###### Getter ###### + @staticmethod + def _new_material_in_engine(mat_name: str) -> None: + mat = bpy.data.materials.new(name=mat_name) + mat.use_nodes = True + + + @staticmethod + def _add_diffuse_texture_in_engine( + mat_name: str, + texture_file: str, + texture_name: str, + ) -> None: + mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) + tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) + tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) + + # HACK: move nodes to the left, for better visibility + tex_node.location.x -= 400 + tex_node.location.y += 200 + + bsdf_node = mat.node_tree.nodes["Principled BSDF"] + mat.node_tree.links.new(tex_node.outputs["Color"], bsdf_node.inputs[BSDFNodeLinkEnumBlender.diffuse]) + + + @staticmethod + def _add_normal_texture_in_engine( + mat_name: str, + texture_file: str, + texture_name: str, + ) -> None: + mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) + tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) + tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) + + normal_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeNormalMap") + gamma_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeGamma") + gamma_node.inputs["Gamma"].default_value = 0.454 # 2.2 gamma + + # HACK: move nodes to the left, for better visibility + tex_node.location.x -= 800 + gamma_node.location.x -= 400 + normal_node.location.x -= 200 + tex_node.location.y -= 600 + gamma_node.location.y -= 600 + normal_node.location.y -= 600 + + bsdf_node = mat.node_tree.nodes["Principled BSDF"] + mat.node_tree.links.new(tex_node.outputs["Color"], gamma_node.inputs["Color"]) + mat.node_tree.links.new(gamma_node.outputs["Color"], normal_node.inputs["Color"]) + mat.node_tree.links.new(normal_node.outputs["Normal"], bsdf_node.inputs[BSDFNodeLinkEnumBlender.normal]) + + + @staticmethod + def _add_roughness_texture_in_engine( + mat_name: str, + texture_file: str, + texture_name: str, + ) -> None: + mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) + tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) + tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) + + math_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeMath") + math_node.operation = "SUBTRACT" + math_node.inputs[0].default_value = 1.0 + + map_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeMapRange") + map_node.inputs[3].default_value = 0.5 + + # HACK: move nodes to the left, for better visibility + tex_node.location.x -= 800 + math_node.location.x -= 400 + map_node.location.x -= 200 + tex_node.location.y -= 200 + math_node.location.y -= 200 + map_node.location.y -= 200 + + bsdf_node = mat.node_tree.nodes["Principled BSDF"] + mat.node_tree.links.new(tex_node.outputs["Color"], math_node.inputs[1]) + mat.node_tree.links.new(math_node.outputs["Value"], map_node.inputs[0]) + mat.node_tree.links.new(map_node.outputs["Result"], bsdf_node.inputs[BSDFNodeLinkEnumBlender.roughness]) \ No newline at end of file diff --git a/xrfeitoria/material/material_unreal.py b/xrfeitoria/material/material_unreal.py new file mode 100644 index 00000000..e69de29b From 648f989d2bcf14a845dc3e7fe18dacc3f1e28063 Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Wed, 15 Nov 2023 10:45:45 +0800 Subject: [PATCH 022/110] fix bug --- src/XRFeitoriaBpy/core/factory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index dd2708bc..64ef4873 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -414,8 +414,11 @@ def set_collection_active(collection: 'bpy.types.Collection') -> None: Args: collection (bpy.types.Collection): The collection to be set as the active collection. """ - layer_collection = bpy.context.view_layer.layer_collection.children[collection.name] - bpy.context.view_layer.active_layer_collection = layer_collection + if collection.name in bpy.context.view_layer.layer_collection.children.keys(): + layer_collection = bpy.context.view_layer.layer_collection.children[collection.name] + bpy.context.view_layer.active_layer_collection = layer_collection + elif collection.name == bpy.context.view_layer.layer_collection.name: + bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection def set_frame_range(scene: 'bpy.types.Scene', start: int, end: int) -> None: """Set the frame range of the given scene. From 861bc309f1827985f14bff9d9b83c150152f63b4 Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Wed, 15 Nov 2023 11:55:54 +0800 Subject: [PATCH 023/110] add docstrings --- xrfeitoria/material/material_base.py | 29 ++++++++++++++++++++++--- xrfeitoria/material/material_blender.py | 26 ++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/xrfeitoria/material/material_base.py b/xrfeitoria/material/material_base.py index 7536b17f..46d6f9aa 100644 --- a/xrfeitoria/material/material_base.py +++ b/xrfeitoria/material/material_base.py @@ -23,6 +23,14 @@ def __init__(self, name: str) -> None: @classmethod def new(cls, mat_name: str) -> Self: + """Add a new material. + + Args: + mat_name (str): Name of the material. + + Returns: + Self: The instance of the material. + """ cls._new_material_in_engine(mat_name) return cls(mat_name) @@ -32,7 +40,12 @@ def add_diffuse_texture( texture_file: PathLike, texture_name: Optional[str] = None, ) -> None: - """Add a diffuse texture to the material""" + """Add a diffuse texture to the material + + Args: + texture_file (PathLike): File path of the texture. + texture_name (Optional[str]): Name of the texture. Defaults to None. + """ if texture_name is None: texture_name = Path(texture_file).stem self._add_diffuse_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) @@ -43,7 +56,12 @@ def add_normal_texture( texture_file: PathLike, texture_name: Optional[str] = None, ) -> None: - """Add a normal texture to the material""" + """Add a normal texture to the material + + Args: + texture_file (PathLike): File path of the texture. + texture_name (Optional[str]): Name of the texture. Defaults to None. + """ if texture_name is None: texture_name = Path(texture_file).stem self._add_normal_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) @@ -54,7 +72,12 @@ def add_roughness_texture( texture_file: PathLike, texture_name: Optional[str] = None, ) -> None: - """Add a normal texture to the material""" + """Add a roughness texture to the material + + Args: + texture_file (PathLike): File path of the texture. + texture_name (Optional[str]): Name of the texture. Defaults to None. + """ if texture_name is None: texture_name = Path(texture_file).stem self._add_roughness_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) diff --git a/xrfeitoria/material/material_blender.py b/xrfeitoria/material/material_blender.py index cefc0222..77532276 100644 --- a/xrfeitoria/material/material_blender.py +++ b/xrfeitoria/material/material_blender.py @@ -32,6 +32,11 @@ class MaterialBlender(MaterialBase): ###### Getter ###### @staticmethod def _new_material_in_engine(mat_name: str) -> None: + """Add a new material. + + Args: + mat_name (str): Name of the material in Blender. + """ mat = bpy.data.materials.new(name=mat_name) mat.use_nodes = True @@ -42,6 +47,13 @@ def _add_diffuse_texture_in_engine( texture_file: str, texture_name: str, ) -> None: + """Add a diffuse texture to the material + + Args: + mat_name (str): Name of the material in Blender. + texture_file (PathLike): File path of the texture. + texture_name (Optional[str]): Name of the texture. Defaults to None. + """ mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) @@ -60,6 +72,13 @@ def _add_normal_texture_in_engine( texture_file: str, texture_name: str, ) -> None: + """Add a normal texture to the material + + Args: + mat_name (str): Name of the material in Blender. + texture_file (PathLike): File path of the texture. + texture_name (Optional[str]): Name of the texture. Defaults to None. + """ mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) @@ -88,6 +107,13 @@ def _add_roughness_texture_in_engine( texture_file: str, texture_name: str, ) -> None: + """Add a roughness texture to the material + + Args: + mat_name (str): Name of the material in Blender. + texture_file (PathLike): File path of the texture. + texture_name (Optional[str]): Name of the texture. Defaults to None. + """ mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) From 949a23eaa3373579f1c2b9b44c7ccbb2870fbf16 Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Wed, 15 Nov 2023 12:02:35 +0800 Subject: [PATCH 024/110] pre-commit --- src/XRFeitoriaBpy/core/factory.py | 8 ++-- xrfeitoria/actor/actor_blender.py | 16 ++++---- xrfeitoria/data_structure/constants.py | 8 ++-- xrfeitoria/material/material_base.py | 11 ++---- xrfeitoria/material/material_blender.py | 49 ++++++++++++------------- 5 files changed, 43 insertions(+), 49 deletions(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 64ef4873..285f317c 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -938,7 +938,7 @@ def get_active_cameras(scene: 'bpy.types.Scene') -> List[str]: ############### Import ############## ##################################### def import_texture(texture_file: str) -> bpy.types.Image: - """Import an image as a texture + """Import an image as a texture. Args: texture_file (str): File path of the image. @@ -948,7 +948,7 @@ def import_texture(texture_file: str) -> bpy.types.Image: try: texture = bpy.data.images.load(filepath=str(texture_file)) except Exception: - raise Exception(f"Failed to import texture: {texture_file}") + raise Exception(f'Failed to import texture: {texture_file}') return texture def import_fbx(fbx_file: str) -> None: @@ -1283,10 +1283,10 @@ def get_material(mat_name: str) -> 'bpy.types.Material': if mat_name not in bpy.data.materials.keys(): raise ValueError(f"Material '{mat_name}' does not exists in this blend file.") return bpy.data.materials[mat_name] - + def new_mat_node(mat: 'bpy.types.Material', type: str, name: Optional[str] = None) -> bpy.types.Node: _nodes = mat.node_tree.nodes node = _nodes.new(type=type) if name: node.name = name - return node \ No newline at end of file + return node diff --git a/xrfeitoria/actor/actor_blender.py b/xrfeitoria/actor/actor_blender.py index 75eb4579..f5056d41 100644 --- a/xrfeitoria/actor/actor_blender.py +++ b/xrfeitoria/actor/actor_blender.py @@ -4,12 +4,12 @@ from loguru import logger from ..data_structure.constants import Vector, default_level_blender +from ..material.material_blender import MaterialBlender from ..object.object_utils import ObjectUtilsBlender from ..rpc import remote_blender from ..utils import Validator from ..utils.functions import blender_functions from .actor_base import ActorBase -from ..material.material_blender import MaterialBlender try: import bpy # isort:skip @@ -43,11 +43,10 @@ def set_transform_keys(self, transform_keys: 'TransformKeys') -> None: transform_keys = [transform_keys] transform_keys = [i.model_dump() for i in transform_keys] self._object_utils.set_transform_keys(name=self.name, transform_keys=transform_keys) - + def set_material(self, mat: MaterialBlender) -> None: self._set_material_in_engine(actor_name=self.name, mat_name=mat._name) - ##################################### ###### RPC METHODS (Private) ######## ##################################### @@ -132,10 +131,11 @@ def _import_animation_from_file_in_engine(animation_path: str, actor_name: str, XRFeitoriaBlenderFactory.import_mo_fbx(mo_fbx_file=animation_path, actor_name=actor_name) else: raise TypeError(f"Invalid anim file, expected 'json', 'blend', or 'fbx' (got {anim_file_ext[1:]} instead).") - + @staticmethod def _set_material_in_engine(actor_name: str, mat_name: str) -> None: - """Set material to an actor. If the actor has multiple meshes, set material to the 1st mesh. + """Set material to an actor. If the actor has multiple meshes, set material to + the 1st mesh. Args: actor_name (str): Name of the actor. @@ -144,11 +144,11 @@ def _set_material_in_engine(actor_name: str, mat_name: str) -> None: actor = bpy.data.objects[actor_name] material = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) - if actor.type == "ARMATURE": + if actor.type == 'ARMATURE': if len(actor.children) == 0: - raise TypeError(f"Actor {actor_name} has no meshes, thus cannot set material to it.") + raise TypeError(f'Actor {actor_name} has no meshes, thus cannot set material to it.') mesh = actor.children[0] - elif actor.type == "MESH": + elif actor.type == 'MESH': mesh = actor if mesh.data.materials: diff --git a/xrfeitoria/data_structure/constants.py b/xrfeitoria/data_structure/constants.py index 343f264a..f2c129a7 100644 --- a/xrfeitoria/data_structure/constants.py +++ b/xrfeitoria/data_structure/constants.py @@ -178,8 +178,8 @@ class ShapeTypeEnumUnreal(EnumBase): class BSDFNodeLinkEnumBlender(EnumBase): - """Shader node link enum of Blender""" + """Shader node link enum of Blender.""" - diffuse = "Base Color" - normal = "Normal" - roughness = "Roughness" \ No newline at end of file + diffuse = 'Base Color' + normal = 'Normal' + roughness = 'Roughness' diff --git a/xrfeitoria/material/material_base.py b/xrfeitoria/material/material_base.py index 46d6f9aa..0c8c3e5d 100644 --- a/xrfeitoria/material/material_base.py +++ b/xrfeitoria/material/material_base.py @@ -34,13 +34,12 @@ def new(cls, mat_name: str) -> Self: cls._new_material_in_engine(mat_name) return cls(mat_name) - def add_diffuse_texture( self, texture_file: PathLike, texture_name: Optional[str] = None, ) -> None: - """Add a diffuse texture to the material + """Add a diffuse texture to the material. Args: texture_file (PathLike): File path of the texture. @@ -50,13 +49,12 @@ def add_diffuse_texture( texture_name = Path(texture_file).stem self._add_diffuse_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) - def add_normal_texture( self, texture_file: PathLike, texture_name: Optional[str] = None, ) -> None: - """Add a normal texture to the material + """Add a normal texture to the material. Args: texture_file (PathLike): File path of the texture. @@ -66,13 +64,12 @@ def add_normal_texture( texture_name = Path(texture_file).stem self._add_normal_texture_in_engine(mat_name=self._name, texture_file=texture_file, texture_name=texture_name) - def add_roughness_texture( self, texture_file: PathLike, texture_name: Optional[str] = None, ) -> None: - """Add a roughness texture to the material + """Add a roughness texture to the material. Args: texture_file (PathLike): File path of the texture. @@ -116,4 +113,4 @@ def _add_roughness_texture_in_engine( texture_file: str, texture_name: str, ) -> None: - pass \ No newline at end of file + pass diff --git a/xrfeitoria/material/material_blender.py b/xrfeitoria/material/material_blender.py index 77532276..4b5a2ef8 100644 --- a/xrfeitoria/material/material_blender.py +++ b/xrfeitoria/material/material_blender.py @@ -1,7 +1,7 @@ import math from typing import Dict, List, Optional, Tuple, Union -from ..data_structure.constants import Vector, BSDFNodeLinkEnumBlender +from ..data_structure.constants import BSDFNodeLinkEnumBlender, Vector from ..object.object_utils import ObjectUtilsBlender from ..rpc import remote_blender from ..utils import Validator @@ -40,14 +40,13 @@ def _new_material_in_engine(mat_name: str) -> None: mat = bpy.data.materials.new(name=mat_name) mat.use_nodes = True - @staticmethod def _add_diffuse_texture_in_engine( mat_name: str, texture_file: str, texture_name: str, ) -> None: - """Add a diffuse texture to the material + """Add a diffuse texture to the material. Args: mat_name (str): Name of the material in Blender. @@ -55,16 +54,15 @@ def _add_diffuse_texture_in_engine( texture_name (Optional[str]): Name of the texture. Defaults to None. """ mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) - tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) + tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type='ShaderNodeTexImage', name=texture_name) tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) # HACK: move nodes to the left, for better visibility tex_node.location.x -= 400 tex_node.location.y += 200 - bsdf_node = mat.node_tree.nodes["Principled BSDF"] - mat.node_tree.links.new(tex_node.outputs["Color"], bsdf_node.inputs[BSDFNodeLinkEnumBlender.diffuse]) - + bsdf_node = mat.node_tree.nodes['Principled BSDF'] + mat.node_tree.links.new(tex_node.outputs['Color'], bsdf_node.inputs[BSDFNodeLinkEnumBlender.diffuse]) @staticmethod def _add_normal_texture_in_engine( @@ -72,7 +70,7 @@ def _add_normal_texture_in_engine( texture_file: str, texture_name: str, ) -> None: - """Add a normal texture to the material + """Add a normal texture to the material. Args: mat_name (str): Name of the material in Blender. @@ -80,12 +78,12 @@ def _add_normal_texture_in_engine( texture_name (Optional[str]): Name of the texture. Defaults to None. """ mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) - tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) + tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type='ShaderNodeTexImage', name=texture_name) tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) - normal_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeNormalMap") - gamma_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeGamma") - gamma_node.inputs["Gamma"].default_value = 0.454 # 2.2 gamma + normal_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type='ShaderNodeNormalMap') + gamma_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type='ShaderNodeGamma') + gamma_node.inputs['Gamma'].default_value = 0.454 # 2.2 gamma # HACK: move nodes to the left, for better visibility tex_node.location.x -= 800 @@ -95,11 +93,10 @@ def _add_normal_texture_in_engine( gamma_node.location.y -= 600 normal_node.location.y -= 600 - bsdf_node = mat.node_tree.nodes["Principled BSDF"] - mat.node_tree.links.new(tex_node.outputs["Color"], gamma_node.inputs["Color"]) - mat.node_tree.links.new(gamma_node.outputs["Color"], normal_node.inputs["Color"]) - mat.node_tree.links.new(normal_node.outputs["Normal"], bsdf_node.inputs[BSDFNodeLinkEnumBlender.normal]) - + bsdf_node = mat.node_tree.nodes['Principled BSDF'] + mat.node_tree.links.new(tex_node.outputs['Color'], gamma_node.inputs['Color']) + mat.node_tree.links.new(gamma_node.outputs['Color'], normal_node.inputs['Color']) + mat.node_tree.links.new(normal_node.outputs['Normal'], bsdf_node.inputs[BSDFNodeLinkEnumBlender.normal]) @staticmethod def _add_roughness_texture_in_engine( @@ -107,7 +104,7 @@ def _add_roughness_texture_in_engine( texture_file: str, texture_name: str, ) -> None: - """Add a roughness texture to the material + """Add a roughness texture to the material. Args: mat_name (str): Name of the material in Blender. @@ -115,14 +112,14 @@ def _add_roughness_texture_in_engine( texture_name (Optional[str]): Name of the texture. Defaults to None. """ mat = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name) - tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeTexImage", name=texture_name) + tex_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type='ShaderNodeTexImage', name=texture_name) tex_node.image = XRFeitoriaBlenderFactory.import_texture(texture_file=texture_file) - math_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeMath") - math_node.operation = "SUBTRACT" + math_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type='ShaderNodeMath') + math_node.operation = 'SUBTRACT' math_node.inputs[0].default_value = 1.0 - map_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type="ShaderNodeMapRange") + map_node = XRFeitoriaBlenderFactory.new_mat_node(mat=mat, type='ShaderNodeMapRange') map_node.inputs[3].default_value = 0.5 # HACK: move nodes to the left, for better visibility @@ -133,7 +130,7 @@ def _add_roughness_texture_in_engine( math_node.location.y -= 200 map_node.location.y -= 200 - bsdf_node = mat.node_tree.nodes["Principled BSDF"] - mat.node_tree.links.new(tex_node.outputs["Color"], math_node.inputs[1]) - mat.node_tree.links.new(math_node.outputs["Value"], map_node.inputs[0]) - mat.node_tree.links.new(map_node.outputs["Result"], bsdf_node.inputs[BSDFNodeLinkEnumBlender.roughness]) \ No newline at end of file + bsdf_node = mat.node_tree.nodes['Principled BSDF'] + mat.node_tree.links.new(tex_node.outputs['Color'], math_node.inputs[1]) + mat.node_tree.links.new(math_node.outputs['Value'], map_node.inputs[0]) + mat.node_tree.links.new(map_node.outputs['Result'], bsdf_node.inputs[BSDFNodeLinkEnumBlender.roughness]) From 9345a2013c136d25e5de5600ac81d40645605358 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Thu, 16 Nov 2023 10:46:45 +0800 Subject: [PATCH 025/110] [Feature] add cli of vis_smplx --- pyproject.toml | 1 + xrfeitoria/cmd/blender/install_plugin.py | 4 +- xrfeitoria/cmd/blender/vis_smplx.py | 185 +++++++++++++++++++++++ 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 xrfeitoria/cmd/blender/vis_smplx.py diff --git a/pyproject.toml b/pyproject.toml index 5ee9fcb6..950e1682 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ vis = [ [project.scripts] xf-render = "xrfeitoria.cmd.blender.render:app" xf-install-plugin = "xrfeitoria.cmd.blender.install_plugin:app" +xf-smplx = "xrfeitoria.cmd.blender.vis_smplx:app" [tool.black] line-length = 120 diff --git a/xrfeitoria/cmd/blender/install_plugin.py b/xrfeitoria/cmd/blender/install_plugin.py index 33f0dd57..779b5add 100644 --- a/xrfeitoria/cmd/blender/install_plugin.py +++ b/xrfeitoria/cmd/blender/install_plugin.py @@ -2,6 +2,8 @@ >>> xf-install-plugin --help >>> xf-render {} [-o {output_path}] + +# TODO: install XRFeitoriaBpy """ from pathlib import Path @@ -27,7 +29,7 @@ def main( file_okay=True, dir_okay=False, resolve_path=True, - help='filepath of the mesh to be renderer', + help='filepath of the plugin (.zip) to be installed', ), ], plugin_name_blender: Annotated[ diff --git a/xrfeitoria/cmd/blender/vis_smplx.py b/xrfeitoria/cmd/blender/vis_smplx.py new file mode 100644 index 00000000..5aaaa7d2 --- /dev/null +++ b/xrfeitoria/cmd/blender/vis_smplx.py @@ -0,0 +1,185 @@ +"""Install a blender plugin with a command line interface. + +>>> xf-install-plugin --help +>>> xf-render {} [-o {output_path}] +""" + +from pathlib import Path +from textwrap import dedent +from typing import Tuple + +import numpy as np +from typer import Argument, Option, Typer +from typing_extensions import Annotated + +import xrfeitoria as xf +from xrfeitoria.rpc import remote_blender +from xrfeitoria.utils.anim.motion import Motion, SMPLMotion, SMPLXMotion +from xrfeitoria.utils.tools import Logger + +app = Typer(pretty_exceptions_show_locals=False) + + +@remote_blender() +def add_smplx(betas: "Tuple[float, ...]" = [0.0] * 10, gender: str = "neutral") -> str: + """Add smplx mesh to scene and return the name of the armature and the mesh + + Args: + betas (Tuple[float, ...]): betas of smplx model + gender (str): gender of smplx model + + Returns: + str: armature name + """ + import bpy + + assert hasattr(bpy.ops.scene, "smplx_add_gender"), "Please install smplx addon first" + + bpy.data.window_managers["WinMan"].smplx_tool.smplx_gender = gender + bpy.data.window_managers["WinMan"].smplx_tool.smplx_handpose = "flat" + bpy.ops.scene.smplx_add_gender() + + bpy.data.window_managers["WinMan"].smplx_tool.smplx_texture = "smplx_texture_f_alb.png" + bpy.ops.object.smplx_set_texture() + + smplx_mesh = bpy.context.selected_objects[0] + for index, beta in enumerate(betas): + smplx_mesh.data.shape_keys.key_blocks[f"Shape{index:03d}"].value = beta + bpy.ops.object.smplx_update_joint_locations() + bpy.ops.object.smplx_set_handpose() + + return smplx_mesh.parent.name + + +@remote_blender() +def export_fbx( + tgt_rig_name: str, + save_path: "Path", + with_mesh: bool = False, + use_better_fbx: bool = True, + **options, +) -> bool: + import bpy + + if use_better_fbx and not hasattr(bpy.ops, "better_export"): + raise RuntimeError("Unable to found better_fbx addon!") + + target_rig = bpy.data.objects[tgt_rig_name] + + save_path = Path(save_path).resolve() + bpy.ops.object.mode_set(mode="OBJECT") + # re-select the armature + # bpy.ops.object.select_all(action='DESELECT') + for obj in bpy.data.objects: + obj.select_set(False) + bpy.context.view_layer.objects.active = target_rig + if with_mesh: + # select the mesh for export + bpy.ops.object.select_grouped(type="CHILDREN_RECURSIVE") + target_rig.select_set(True) + + if save_path.exists(): + ctime = save_path.stat().st_ctime + else: + ctime = 0 + + save_path.parent.mkdir(exist_ok=True, parents=True) + save_path_str = str(save_path) + if use_better_fbx: + bpy.ops.better_export.fbx(filepath=save_path_str, use_selection=True, **options) + else: + bpy.ops.export_scene.fbx(filepath=save_path_str, use_selection=True, **options) + + if save_path.exists() and save_path.stat().st_ctime > ctime: + success = True + else: + success = False + return success + + +def read_smpl_x(path: Path) -> Motion: + """Reads a SMPL/SMPLX motion from a .npz file. + + Args: + path (Path): Path to the .npz file. + + Returns: + SMPLXMotion: The motion. + """ + smpl_x_data = np.load(path, allow_pickle=True) + if "smplx" in smpl_x_data: + smpl_x_data = smpl_x_data["smplx"].item() + motion = SMPLXMotion.from_smplx_data(smpl_x_data) + if "smpl" in smpl_x_data: + smpl_x_data = smpl_x_data["smpl"].item() + motion = SMPLMotion.from_smpl_data(smpl_x_data) + else: + try: + motion = SMPLXMotion.from_smplx_data(smpl_x_data) + except Exception: + raise ValueError(f"Unknown data format of {path}, got {smpl_x_data.keys()}, but expected smpl or smplx") + motion.insert_rest_pose() + return motion + + +@app.command() +def main( + # path config + smplx_path: Annotated[ + Path, + Argument( + ..., + exists=True, + file_okay=True, + dir_okay=False, + resolve_path=True, + help="filepath of the smplx motion (.npz) to be retargeted", + ), + ], + # engine config + blender_exec: Annotated[ + Path, + Option("--blender-exec", help="path to blender executable, e.g. /usr/bin/blender"), + ] = None, + # misc + debug: Annotated[ + bool, + Option("--debug/--no-debug", help="log in debug mode"), + ] = False, +): + """Visualize a SMPL-X motion with a command line interface.""" + logger = Logger.setup_logging(level="DEBUG" if debug else "INFO") + logger.info( + dedent( + f"""\ + :rocket: Starting: + Executing install plugin with the following parameters: + --------------------------------------------------------- + [yellow]# path config[/yellow] + - smplx_path: {smplx_path} + [yellow]# engine config[/yellow] + - blender_exec: {blender_exec} + [yellow]# misc[/yellow] + - debug: {debug} + ---------------------------------------------------------""" + ) + ) + + with xf.init_blender(exec_path=blender_exec, background=True) as xf_runner: + logger.info(f"Loading motion data: {smplx_path} ...") + motion = read_smpl_x(smplx_path) + + logger.info("Adding smplx actor using smplx_addon ...") + smplx_rig_name = add_smplx() + + logger.info("Applying motion data to actor ...") + xf_runner.utils.apply_motion_data_to_actor(motion_data=motion.get_motion_data(), actor_name=smplx_rig_name) + + logger.info("Saving it to blend file ...") + xf_runner.utils.save_blend(smplx_path.with_name(f"{smplx_path.stem}.blend")) + + logger.info(":tada: [green]Installation of plugin completed![/green]") + + +if __name__ == "__main__": + app() From 42f2b505d1725221285d460fa031cd05b77ae39a Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Thu, 16 Nov 2023 11:01:01 +0800 Subject: [PATCH 026/110] pre-commit --- xrfeitoria/cmd/blender/vis_smplx.py | 57 +++++++++++++++-------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/xrfeitoria/cmd/blender/vis_smplx.py b/xrfeitoria/cmd/blender/vis_smplx.py index 5aaaa7d2..c90e6c3f 100644 --- a/xrfeitoria/cmd/blender/vis_smplx.py +++ b/xrfeitoria/cmd/blender/vis_smplx.py @@ -21,8 +21,8 @@ @remote_blender() -def add_smplx(betas: "Tuple[float, ...]" = [0.0] * 10, gender: str = "neutral") -> str: - """Add smplx mesh to scene and return the name of the armature and the mesh +def add_smplx(betas: 'Tuple[float, ...]' = [0.0] * 10, gender: str = 'neutral') -> str: + """Add smplx mesh to scene and return the name of the armature and the mesh. Args: betas (Tuple[float, ...]): betas of smplx model @@ -33,18 +33,18 @@ def add_smplx(betas: "Tuple[float, ...]" = [0.0] * 10, gender: str = "neutral") """ import bpy - assert hasattr(bpy.ops.scene, "smplx_add_gender"), "Please install smplx addon first" + assert hasattr(bpy.ops.scene, 'smplx_add_gender'), 'Please install smplx addon first' - bpy.data.window_managers["WinMan"].smplx_tool.smplx_gender = gender - bpy.data.window_managers["WinMan"].smplx_tool.smplx_handpose = "flat" + bpy.data.window_managers['WinMan'].smplx_tool.smplx_gender = gender + bpy.data.window_managers['WinMan'].smplx_tool.smplx_handpose = 'flat' bpy.ops.scene.smplx_add_gender() - bpy.data.window_managers["WinMan"].smplx_tool.smplx_texture = "smplx_texture_f_alb.png" + bpy.data.window_managers['WinMan'].smplx_tool.smplx_texture = 'smplx_texture_f_alb.png' bpy.ops.object.smplx_set_texture() smplx_mesh = bpy.context.selected_objects[0] for index, beta in enumerate(betas): - smplx_mesh.data.shape_keys.key_blocks[f"Shape{index:03d}"].value = beta + smplx_mesh.data.shape_keys.key_blocks[f'Shape{index:03d}'].value = beta bpy.ops.object.smplx_update_joint_locations() bpy.ops.object.smplx_set_handpose() @@ -54,20 +54,20 @@ def add_smplx(betas: "Tuple[float, ...]" = [0.0] * 10, gender: str = "neutral") @remote_blender() def export_fbx( tgt_rig_name: str, - save_path: "Path", + save_path: 'Path', with_mesh: bool = False, use_better_fbx: bool = True, **options, ) -> bool: import bpy - if use_better_fbx and not hasattr(bpy.ops, "better_export"): - raise RuntimeError("Unable to found better_fbx addon!") + if use_better_fbx and not hasattr(bpy.ops, 'better_export'): + raise RuntimeError('Unable to found better_fbx addon!') target_rig = bpy.data.objects[tgt_rig_name] save_path = Path(save_path).resolve() - bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.mode_set(mode='OBJECT') # re-select the armature # bpy.ops.object.select_all(action='DESELECT') for obj in bpy.data.objects: @@ -75,7 +75,7 @@ def export_fbx( bpy.context.view_layer.objects.active = target_rig if with_mesh: # select the mesh for export - bpy.ops.object.select_grouped(type="CHILDREN_RECURSIVE") + bpy.ops.object.select_grouped(type='CHILDREN_RECURSIVE') target_rig.select_set(True) if save_path.exists(): @@ -107,17 +107,17 @@ def read_smpl_x(path: Path) -> Motion: SMPLXMotion: The motion. """ smpl_x_data = np.load(path, allow_pickle=True) - if "smplx" in smpl_x_data: - smpl_x_data = smpl_x_data["smplx"].item() + if 'smplx' in smpl_x_data: + smpl_x_data = smpl_x_data['smplx'].item() motion = SMPLXMotion.from_smplx_data(smpl_x_data) - if "smpl" in smpl_x_data: - smpl_x_data = smpl_x_data["smpl"].item() + if 'smpl' in smpl_x_data: + smpl_x_data = smpl_x_data['smpl'].item() motion = SMPLMotion.from_smpl_data(smpl_x_data) else: try: motion = SMPLXMotion.from_smplx_data(smpl_x_data) except Exception: - raise ValueError(f"Unknown data format of {path}, got {smpl_x_data.keys()}, but expected smpl or smplx") + raise ValueError(f'Unknown data format of {path}, got {smpl_x_data.keys()}, but expected smpl or smplx') motion.insert_rest_pose() return motion @@ -133,22 +133,22 @@ def main( file_okay=True, dir_okay=False, resolve_path=True, - help="filepath of the smplx motion (.npz) to be retargeted", + help='filepath of the smplx motion (.npz) to be retargeted', ), ], # engine config blender_exec: Annotated[ Path, - Option("--blender-exec", help="path to blender executable, e.g. /usr/bin/blender"), + Option('--blender-exec', help='path to blender executable, e.g. /usr/bin/blender'), ] = None, # misc debug: Annotated[ bool, - Option("--debug/--no-debug", help="log in debug mode"), + Option('--debug/--no-debug', help='log in debug mode'), ] = False, ): """Visualize a SMPL-X motion with a command line interface.""" - logger = Logger.setup_logging(level="DEBUG" if debug else "INFO") + logger = Logger.setup_logging(level='DEBUG' if debug else 'INFO') logger.info( dedent( f"""\ @@ -166,20 +166,21 @@ def main( ) with xf.init_blender(exec_path=blender_exec, background=True) as xf_runner: - logger.info(f"Loading motion data: {smplx_path} ...") + logger.info(f'Loading motion data: {smplx_path} ...') motion = read_smpl_x(smplx_path) - logger.info("Adding smplx actor using smplx_addon ...") + logger.info('Adding smplx actor using smplx_addon ...') smplx_rig_name = add_smplx() - logger.info("Applying motion data to actor ...") + logger.info('Applying motion data to actor ...') xf_runner.utils.apply_motion_data_to_actor(motion_data=motion.get_motion_data(), actor_name=smplx_rig_name) - logger.info("Saving it to blend file ...") - xf_runner.utils.save_blend(smplx_path.with_name(f"{smplx_path.stem}.blend")) + blend_file = smplx_path.with_name(f'{smplx_path.stem}.blend') + logger.info(f'Saving it to blend file "{blend_file}" ...') + xf_runner.utils.save_blend(blend_file) - logger.info(":tada: [green]Installation of plugin completed![/green]") + logger.info(':tada: [green]Visualization finished[/green]!') -if __name__ == "__main__": +if __name__ == '__main__': app() From 285a6f6b71434c199f3a6aff46ce5d7d8a2a4340 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 16 Nov 2023 15:05:57 +0800 Subject: [PATCH 027/110] Add try-except block --- xrfeitoria/cmd/blender/vis_smplx.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/xrfeitoria/cmd/blender/vis_smplx.py b/xrfeitoria/cmd/blender/vis_smplx.py index c90e6c3f..8f348708 100644 --- a/xrfeitoria/cmd/blender/vis_smplx.py +++ b/xrfeitoria/cmd/blender/vis_smplx.py @@ -8,15 +8,19 @@ from textwrap import dedent from typing import Tuple -import numpy as np -from typer import Argument, Option, Typer -from typing_extensions import Annotated - import xrfeitoria as xf from xrfeitoria.rpc import remote_blender -from xrfeitoria.utils.anim.motion import Motion, SMPLMotion, SMPLXMotion from xrfeitoria.utils.tools import Logger +try: + import numpy as np + from typer import Argument, Option, Typer + from typing_extensions import Annotated + + from xrfeitoria.utils.anim.motion import Motion, SMPLMotion, SMPLXMotion +except (ImportError, NameError): + pass + app = Typer(pretty_exceptions_show_locals=False) @@ -97,7 +101,7 @@ def export_fbx( return success -def read_smpl_x(path: Path) -> Motion: +def read_smpl_x(path: Path) -> 'Motion': """Reads a SMPL/SMPLX motion from a .npz file. Args: @@ -113,11 +117,13 @@ def read_smpl_x(path: Path) -> Motion: if 'smpl' in smpl_x_data: smpl_x_data = smpl_x_data['smpl'].item() motion = SMPLMotion.from_smpl_data(smpl_x_data) + if 'pose_body' in smpl_x_data: + motion = SMPLXMotion.from_amass_data(smpl_x_data, insert_rest_pose=False) else: try: motion = SMPLXMotion.from_smplx_data(smpl_x_data) except Exception: - raise ValueError(f'Unknown data format of {path}, got {smpl_x_data.keys()}, but expected smpl or smplx') + raise ValueError(f'Unknown data format of {path}, got {smpl_x_data.keys()}, but expected "smpl" or "smplx"') motion.insert_rest_pose() return motion From 771748790f823fd632feaa85c8a0121cd2993343 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Mon, 20 Nov 2023 11:30:16 +0800 Subject: [PATCH 028/110] add `dest_path` for `import_anim` --- src/XRFeitoriaUnreal/Content/Python/utils.py | 10 ++++++---- xrfeitoria/utils/functions/unreal_functions.py | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/utils.py b/src/XRFeitoriaUnreal/Content/Python/utils.py index 87afb604..a6f811cc 100644 --- a/src/XRFeitoriaUnreal/Content/Python/utils.py +++ b/src/XRFeitoriaUnreal/Content/Python/utils.py @@ -125,12 +125,13 @@ def import_asset(path: Union[str, List[str]], dst_dir_in_engine: Optional[str] = return asset_paths -def import_anim(path: str, skeleton_path: str) -> List[str]: +def import_anim(path: str, skeleton_path: str, dest_path: Optional[str] = None) -> List[str]: """Import animation to the default asset path. Args: path (str): a file path to import, e.g. "D:/assets/SMPL_XL.fbx" skeleton_path (str): a path to the skeleton, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL" + dest_path (str, optional): destination directory in the engine. Defaults to None falls back to {skeleton_path.parent}/Animation. Returns: str: a path to the imported animation, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL" @@ -139,8 +140,9 @@ def import_anim(path: str, skeleton_path: str) -> List[str]: import_task = unreal.AssetImportTask() import_task.set_editor_property('filename', path) # set destination path to {skeleton_path}/Animation - dst_path = unreal.Paths.combine([unreal.Paths.get_path(skeleton_path), 'Animation']) - import_task.set_editor_property('destination_path', dst_path) + if dest_path is None: + dest_path = unreal.Paths.combine([unreal.Paths.get_path(skeleton_path), 'Animation']) + import_task.set_editor_property('destination_path', dest_path) import_task.set_editor_property('replace_existing', True) import_task.set_editor_property('replace_existing_settings', True) import_task.set_editor_property('automated', True) @@ -161,7 +163,7 @@ def import_anim(path: str, skeleton_path: str) -> List[str]: unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([import_task]) # save assets - unreal.EditorAssetLibrary.save_directory(dst_path, False, True) + unreal.EditorAssetLibrary.save_directory(dest_path, False, True) # return paths return [path.split('.')[0] for path in import_task.get_editor_property('imported_object_paths')] diff --git a/xrfeitoria/utils/functions/unreal_functions.py b/xrfeitoria/utils/functions/unreal_functions.py index 1af434bd..5b187c65 100644 --- a/xrfeitoria/utils/functions/unreal_functions.py +++ b/xrfeitoria/utils/functions/unreal_functions.py @@ -87,17 +87,18 @@ def import_asset(path: 'Union[str, List[str]]', dst_dir_in_engine: 'Optional[str @remote_unreal() -def import_anim(path: str, skeleton_path: str) -> 'List[str]': +def import_anim(path: str, skeleton_path: str, dest_path: 'Optional[str]' = None) -> 'List[str]': """Import animation to the default asset path. Args: - path (str): a file path to import, e.g. "D:/assets/SMPL_XL-Animation.fbx" - skeleton_path (str): a path to the skeleton, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL_Skeleton" + path (str): The file path to import, e.g. "D:/assets/SMPL_XL-Animation.fbx". + skeleton_path (str): The path to the skeleton, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL_Skeleton". + dest_path (str, optional): The destination directory in the engine. Defaults to None, falls back to {skeleton_path.parent}/Animation. Returns: - str: a path to the imported animation, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL-Animation" + List[str]: A list of paths to the imported animations, e.g. ["/Game/XRFeitoriaUnreal/Assets/SMPL_XL-Animation"]. """ - return XRFeitoriaUnrealFactory.utils.import_anim(path, skeleton_path) + return XRFeitoriaUnrealFactory.utils.import_anim(path, skeleton_path, dest_path) @remote_unreal() From 5cb998b05018a4fefc53e68a54b8790a5ae72408 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 20 Nov 2023 15:52:38 +0800 Subject: [PATCH 029/110] Fix motion data insertion bug and set frame range --- xrfeitoria/cmd/blender/vis_smplx.py | 1 + xrfeitoria/utils/anim/motion.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/xrfeitoria/cmd/blender/vis_smplx.py b/xrfeitoria/cmd/blender/vis_smplx.py index 8f348708..5e6f52a7 100644 --- a/xrfeitoria/cmd/blender/vis_smplx.py +++ b/xrfeitoria/cmd/blender/vis_smplx.py @@ -180,6 +180,7 @@ def main( logger.info('Applying motion data to actor ...') xf_runner.utils.apply_motion_data_to_actor(motion_data=motion.get_motion_data(), actor_name=smplx_rig_name) + xf_runner.utils.set_frame_range(start=0, end=motion.n_frames) blend_file = smplx_path.with_name(f'{smplx_path.stem}.blend') logger.info(f'Saving it to blend file "{blend_file}" ...') diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index 078d0af5..dce80af7 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -256,7 +256,7 @@ def insert_rest_pose(self): for key, arr in self.smpl_data.items(): if key == 'betas': continue - self.smplx_dat[key] = np.insert(arr, 0, 0, axis=0) + self.smpl_data[key] = np.insert(arr, 0, 0, axis=0) if hasattr(self, 'smplx_data'): for key, arr in self.smplx_data.items(): if key == 'betas': @@ -339,7 +339,7 @@ def from_smpl_data( _get_smpl = partial(_get_from_smpl_x, smpl_x_data=smpl_data, dtype=np.float32) n_frames = smpl_data['body_pose'].shape[0] - betas = _get_smpl('betas', shape=[10]) + betas = _get_smpl('betas', shape=[1, 10]) transl = _get_smpl('transl', shape=[n_frames, 3], required=False) global_orient = _get_smpl('global_orient', shape=[n_frames, 3]) body_pose = _get_smpl('body_pose', shape=[n_frames, -1]) @@ -529,7 +529,7 @@ def from_smplx_data( return instance @classmethod - def from_amass_data(cls, amass_data, insert_rest_pose: bool) -> Self: + def from_amass_data(cls, amass_data, insert_rest_pose: bool, flat_hand_mean: bool = True) -> Self: """Create a Motion instance from AMASS data. Args: @@ -596,7 +596,7 @@ def from_amass_data(cls, amass_data, insert_rest_pose: bool) -> Self: smplx_data, insert_rest_pose=False, fps=fps, - flat_hand_mean=True, + flat_hand_mean=flat_hand_mean, ) def get_parent_bone_name(self, bone_name) -> Optional[str]: @@ -628,6 +628,7 @@ def _get_from_smpl_x(key, shape, *, smpl_x_data, dtype=np.float32, required=True _data = smpl_x_data[key].astype(dtype) n_frames, n_dims = shape _data = _data.reshape([n_frames, -1]) - _data = _data[:, :n_dims] # XXX: handle the case that n_dims > data.shape[1] + if not n_dims < 0: + _data = _data[:, :n_dims] # XXX: handle the case that n_dims > data.shape[1] return _data return np.zeros(shape, dtype=dtype) From 84d84b8f34b10a2905ec7d8d62a44cd46f249ff1 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 4 Dec 2023 12:58:15 +0800 Subject: [PATCH 030/110] Add Readme --- samples/README.md | 1 + samples/run_all.py | 1 + 2 files changed, 2 insertions(+) diff --git a/samples/README.md b/samples/README.md index 3fb285fd..b0a15a5a 100644 --- a/samples/README.md +++ b/samples/README.md @@ -26,6 +26,7 @@ python -m samples.blender.03_basic_render [-b] [--debug] python -m samples.blender.04_staticmesh_render [-b] [--debug] python -m samples.blender.05_skeletalmesh_render [-b] [--debug] python -m samples.blender.06_custom_usage [-b] [--debug] +python -m samples.blender.07_amass ``` ## Unreal diff --git a/samples/run_all.py b/samples/run_all.py index a60b590d..fd3819ec 100644 --- a/samples/run_all.py +++ b/samples/run_all.py @@ -16,6 +16,7 @@ def main(engine: Literal['unreal', 'blender'], debug: bool = False, background: '04_staticmesh_render', '05_skeletalmesh_render', '06_custom_usage', + '07_amass', ] for script in scripts: subprocess.check_call(['python', '-m', f'samples.{engine}.{script}'] + args) From 2960bafb171568552aa2a4bd6986cefd706d9a3a Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 4 Dec 2023 15:06:22 +0800 Subject: [PATCH 031/110] Add option to use flat hand mean pose in SMPLXMotion constructor --- xrfeitoria/utils/anim/motion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index dce80af7..78b7b0ce 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -535,6 +535,7 @@ def from_amass_data(cls, amass_data, insert_rest_pose: bool, flat_hand_mean: boo Args: amass_data (dict): A dictionary containing the AMASS data. insert_rest_pose (bool): Whether to insert a rest pose at the beginning of the motion. + flat_hand_mean (bool): Whether to use the flat hand mean pose. Returns: SMPLXMotion: A SMPLXMotion instance containing the AMASS data. From 5336af47567ac7ea397a573b36035cee61c00365 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 5 Dec 2023 17:23:29 +0800 Subject: [PATCH 032/110] polish setup_logger --- samples/blender/01_add_shapes.py | 4 +-- samples/blender/02_add_cameras.py | 4 +-- samples/blender/03_basic_render.py | 4 +-- samples/blender/04_staticmesh_render.py | 4 +-- samples/blender/05_skeletalmesh_render.py | 4 +-- samples/blender/06_custom_usage.py | 4 +-- samples/blender/07_amass.py | 4 +-- samples/setup.py | 4 +-- samples/unreal/01_add_shapes.py | 2 +- samples/unreal/02_add_cameras.py | 4 +-- samples/unreal/03_basic_render.py | 4 +-- samples/unreal/04_staticmesh_render.py | 4 +-- samples/unreal/05_skeletalmesh_render.py | 4 +-- samples/unreal/06_custom_usage.py | 4 +-- samples/utils.py | 11 +----- src/XRFeitoriaBpy/utils_logger.py | 7 +--- xrfeitoria/cmd/blender/install_plugin.py | 4 +-- xrfeitoria/cmd/blender/render.py | 4 +-- xrfeitoria/cmd/blender/vis_smplx.py | 4 +-- xrfeitoria/utils/__init__.py | 3 +- xrfeitoria/utils/tools.py | 44 ++++++++++++++++------- 21 files changed, 68 insertions(+), 63 deletions(-) diff --git a/samples/blender/01_add_shapes.py b/samples/blender/01_add_shapes.py index efa6e26b..046580b7 100644 --- a/samples/blender/01_add_shapes.py +++ b/samples/blender/01_add_shapes.py @@ -6,9 +6,9 @@ from pathlib import Path import xrfeitoria as xf +from xrfeitoria.utils import setup_logger from ..config import blender_exec -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' @@ -18,7 +18,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) # The blender will start in a separate process in RPC Server mode automatically, # and close when calling `xf_runner.close()`. diff --git a/samples/blender/02_add_cameras.py b/samples/blender/02_add_cameras.py index e7c86f1f..420dc046 100644 --- a/samples/blender/02_add_cameras.py +++ b/samples/blender/02_add_cameras.py @@ -7,9 +7,9 @@ from pathlib import Path import xrfeitoria as xf +from xrfeitoria.utils import setup_logger from ..config import blender_exec -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' @@ -19,7 +19,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) # Open blender xf_runner = xf.init_blender(exec_path=blender_exec, background=background, new_process=True) diff --git a/samples/blender/03_basic_render.py b/samples/blender/03_basic_render.py index 792dfd1e..9abf1add 100644 --- a/samples/blender/03_basic_render.py +++ b/samples/blender/03_basic_render.py @@ -7,9 +7,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass +from xrfeitoria.utils import setup_logger from ..config import assets_path, blender_exec -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' @@ -21,7 +21,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) ################################################################################################################################ # In XRFeitoria, a `level` is an editable space that can be used to place objects(3D models, lights, cameras, etc.), diff --git a/samples/blender/04_staticmesh_render.py b/samples/blender/04_staticmesh_render.py index a52bb7c5..5c2883db 100644 --- a/samples/blender/04_staticmesh_render.py +++ b/samples/blender/04_staticmesh_render.py @@ -9,9 +9,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey +from xrfeitoria.utils import setup_logger, visualize_vertices from ..config import assets_path, blender_exec -from ..utils import setup_logger, visualize_vertices root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' @@ -24,7 +24,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) ############################# #### use default level ###### diff --git a/samples/blender/05_skeletalmesh_render.py b/samples/blender/05_skeletalmesh_render.py index 92de7c9b..5e4082a9 100644 --- a/samples/blender/05_skeletalmesh_render.py +++ b/samples/blender/05_skeletalmesh_render.py @@ -8,9 +8,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey +from xrfeitoria.utils import setup_logger from ..config import assets_path, blender_exec -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' @@ -22,7 +22,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) ################################# #### Define your own level ###### diff --git a/samples/blender/06_custom_usage.py b/samples/blender/06_custom_usage.py index 94a120e9..6d1f560b 100644 --- a/samples/blender/06_custom_usage.py +++ b/samples/blender/06_custom_usage.py @@ -7,9 +7,9 @@ import xrfeitoria as xf from xrfeitoria.rpc import remote_blender +from xrfeitoria.utils import setup_logger from ..config import blender_exec -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' @@ -35,7 +35,7 @@ def add_cubes_in_blender(): def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) xf_runner = xf.init_blender(exec_path=blender_exec, background=background, new_process=True) # The function `add_cubes_in_blender` decorated with `@remote_blender` will be executed in blender. diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index eb11b80f..cd06d93c 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -12,8 +12,8 @@ """ import xrfeitoria as xf from xrfeitoria.rpc import remote_blender +from xrfeitoria.utils import setup_logger from xrfeitoria.utils.anim import load_amass_motion -from xrfeitoria.utils.tools import Logger @remote_blender() @@ -26,7 +26,7 @@ def apply_scale(actor_name: str): def main(): - logger = Logger.setup_logging() + logger = setup_logger() # Download Amass from https://amass.is.tue.mpg.de/download.php # For example, download ACCAD (SMPL-X N), and use any motion file from the uncompressed folder diff --git a/samples/setup.py b/samples/setup.py index 01c774f3..428ea05a 100644 --- a/samples/setup.py +++ b/samples/setup.py @@ -8,9 +8,9 @@ from rich.prompt import Prompt from xrfeitoria.data_structure.constants import tmp_dir +from xrfeitoria.utils import setup_logger from xrfeitoria.utils.downloader import download from xrfeitoria.utils.setup import Config, get_exec_path, guess_exec_path -from xrfeitoria.utils.tools import Logger # XXX: Hard-coded assets url assets_url = dict( @@ -94,7 +94,7 @@ def main(): except ImportError: blender_exec = unreal_exec = unreal_project = None - Logger.setup_logging() + setup_logger() engine = Prompt.ask('Which engine do you want to use?', choices=['blender', 'unreal'], default='blender') if engine == 'blender': blender_exec = get_exec('blender', exec_from_config=blender_exec) diff --git a/samples/unreal/01_add_shapes.py b/samples/unreal/01_add_shapes.py index 4ec7c30d..c5452554 100644 --- a/samples/unreal/01_add_shapes.py +++ b/samples/unreal/01_add_shapes.py @@ -9,9 +9,9 @@ from loguru import logger import xrfeitoria as xf +from xrfeitoria.utils import setup_logger from ..config import unreal_exec, unreal_project -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' diff --git a/samples/unreal/02_add_cameras.py b/samples/unreal/02_add_cameras.py index acf232af..bb34785b 100644 --- a/samples/unreal/02_add_cameras.py +++ b/samples/unreal/02_add_cameras.py @@ -7,9 +7,9 @@ from pathlib import Path import xrfeitoria as xf +from xrfeitoria.utils import setup_logger from ..config import unreal_exec, unreal_project -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' @@ -19,7 +19,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) xf_runner = xf.init_unreal(exec_path=unreal_exec, project_path=unreal_project, background=background) # open a level, this can be omitted if you don't need to open a level diff --git a/samples/unreal/03_basic_render.py b/samples/unreal/03_basic_render.py index 743b6aa0..1fef2518 100644 --- a/samples/unreal/03_basic_render.py +++ b/samples/unreal/03_basic_render.py @@ -7,9 +7,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass +from xrfeitoria.utils import setup_logger from ..config import unreal_exec, unreal_project -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' @@ -20,7 +20,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) xf_runner = xf.init_unreal(exec_path=unreal_exec, project_path=unreal_project, background=background) # There are many assets already made by others, including levels, sequences, cameras, meshes, etc. diff --git a/samples/unreal/04_staticmesh_render.py b/samples/unreal/04_staticmesh_render.py index 54b6df18..11f8e953 100644 --- a/samples/unreal/04_staticmesh_render.py +++ b/samples/unreal/04_staticmesh_render.py @@ -9,9 +9,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey +from xrfeitoria.utils import setup_logger, visualize_vertices from ..config import assets_path, unreal_exec, unreal_project -from ..utils import setup_logger, visualize_vertices root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' @@ -26,7 +26,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) xf_runner = xf.init_unreal(exec_path=unreal_exec, project_path=unreal_project, background=background) # duplicate the level to a new level diff --git a/samples/unreal/05_skeletalmesh_render.py b/samples/unreal/05_skeletalmesh_render.py index 092bee9e..0e120b8d 100644 --- a/samples/unreal/05_skeletalmesh_render.py +++ b/samples/unreal/05_skeletalmesh_render.py @@ -7,9 +7,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass +from xrfeitoria.utils import setup_logger from ..config import assets_path, unreal_exec, unreal_project -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' @@ -21,7 +21,7 @@ def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) xf_runner = xf.init_unreal(exec_path=unreal_exec, project_path=unreal_project, background=background) # import actor from file and its animation diff --git a/samples/unreal/06_custom_usage.py b/samples/unreal/06_custom_usage.py index 7c15d56f..adf56f1f 100644 --- a/samples/unreal/06_custom_usage.py +++ b/samples/unreal/06_custom_usage.py @@ -7,9 +7,9 @@ import xrfeitoria as xf from xrfeitoria.rpc import remote_unreal +from xrfeitoria.utils import setup_logger from ..config import unreal_exec, unreal_project -from ..utils import setup_logger root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' @@ -41,7 +41,7 @@ def add_cubes_in_unreal(): def main(debug=False, background=False): - logger = setup_logger(debug=debug, log_path=log_path) + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=log_path) xf_runner = xf.init_unreal(exec_path=unreal_exec, project_path=unreal_project, background=background) # The function `add_cubes_in_unreal` decorated with `@remote_unreal` will be executed in blender. diff --git a/samples/utils.py b/samples/utils.py index 16b67ac2..fab18615 100644 --- a/samples/utils.py +++ b/samples/utils.py @@ -11,7 +11,7 @@ import xrfeitoria as xf from xrfeitoria.camera.camera_parameter import CameraParameter from xrfeitoria.utils import projector -from xrfeitoria.utils.tools import Logger +from xrfeitoria.utils import setup_logger as _setup_logger try: from .config import blender_exec, unreal_exec, unreal_project @@ -64,15 +64,6 @@ def visualize_vertices(camera_name, actor_names: List[str], seq_output_path: Pat logger.info(f'Overlap image saved to: "{save_path.as_posix()}"') -def setup_logger(debug: bool = False, log_path: str = None, reload: bool = True): - if reload: - os.environ['RPC_RELOAD'] = '1' # reload modules on every call - logger = Logger.setup_logging(level='DEBUG' if debug else 'INFO', log_path=log_path) - if debug: - os.environ['RPC_DEBUG'] = '1' - return logger - - @contextmanager def __timer__(step_name: str): t1 = time.time() diff --git a/src/XRFeitoriaBpy/utils_logger.py b/src/XRFeitoriaBpy/utils_logger.py index 96084334..3025a34a 100644 --- a/src/XRFeitoriaBpy/utils_logger.py +++ b/src/XRFeitoriaBpy/utils_logger.py @@ -18,7 +18,7 @@ def filter(self, record): return False -def setup_logging(level: str = 'INFO') -> 'logging.Logger': +def setup_logger(level: str = 'INFO') -> 'logging.Logger': """Setup logging to file and console. Args: @@ -39,9 +39,4 @@ def setup_logging(level: str = 'INFO') -> 'logging.Logger': return logger -def setup_logger(level: str = 'INFO'): - logger = setup_logging(level=level) - return logger - - logger = setup_logger() diff --git a/xrfeitoria/cmd/blender/install_plugin.py b/xrfeitoria/cmd/blender/install_plugin.py index 779b5add..9bc33d83 100644 --- a/xrfeitoria/cmd/blender/install_plugin.py +++ b/xrfeitoria/cmd/blender/install_plugin.py @@ -13,7 +13,7 @@ from typing_extensions import Annotated import xrfeitoria as xf -from xrfeitoria.utils.tools import Logger +from xrfeitoria.utils import setup_logger app = Typer(pretty_exceptions_show_locals=False) @@ -52,7 +52,7 @@ def main( ] = False, ): """Install a blender plugin with a command line interface.""" - logger = Logger.setup_logging(level='DEBUG' if debug else 'INFO') + logger = setup_logger(level='DEBUG' if debug else 'INFO') logger.info( dedent( f"""\ diff --git a/xrfeitoria/cmd/blender/render.py b/xrfeitoria/cmd/blender/render.py index 1ddbcbaa..c0516710 100644 --- a/xrfeitoria/cmd/blender/render.py +++ b/xrfeitoria/cmd/blender/render.py @@ -20,7 +20,7 @@ import xrfeitoria as xf from xrfeitoria.data_structure.constants import ImageFileFormatEnum, RenderEngineEnumBlender, RenderOutputEnumBlender from xrfeitoria.data_structure.models import RenderPass -from xrfeitoria.utils.tools import Logger +from xrfeitoria.utils import setup_logger RENDER_SAMPLES = {'low': 64, 'medium': 256, 'high': 1024} app = Typer(pretty_exceptions_show_locals=False) @@ -117,7 +117,7 @@ def main( ] = False, ): """Render a mesh with blender to output_path.""" - logger = Logger.setup_logging(level='DEBUG' if debug else 'INFO') + logger = setup_logger(level='DEBUG' if debug else 'INFO') if render_engine == 'eevee': render_quality = 'low' diff --git a/xrfeitoria/cmd/blender/vis_smplx.py b/xrfeitoria/cmd/blender/vis_smplx.py index 5e6f52a7..48c5b431 100644 --- a/xrfeitoria/cmd/blender/vis_smplx.py +++ b/xrfeitoria/cmd/blender/vis_smplx.py @@ -10,7 +10,7 @@ import xrfeitoria as xf from xrfeitoria.rpc import remote_blender -from xrfeitoria.utils.tools import Logger +from xrfeitoria.utils import setup_logger try: import numpy as np @@ -154,7 +154,7 @@ def main( ] = False, ): """Visualize a SMPL-X motion with a command line interface.""" - logger = Logger.setup_logging(level='DEBUG' if debug else 'INFO') + logger = setup_logger(level='DEBUG' if debug else 'INFO') logger.info( dedent( f"""\ diff --git a/xrfeitoria/utils/__init__.py b/xrfeitoria/utils/__init__.py index 087d3959..27b04b37 100644 --- a/xrfeitoria/utils/__init__.py +++ b/xrfeitoria/utils/__init__.py @@ -1,3 +1,4 @@ +from .tools import setup_logger from .validations import Validator -__all__ = ['Validator'] +__all__ = ['Validator', 'setup_logger'] diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index b4d7ac3d..37644201 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -2,7 +2,7 @@ from pathlib import Path -from typing import Iterable, Optional, Sequence, Tuple, Union +from typing import Iterable, Literal, Optional, Sequence, Tuple, Union import loguru from loguru import logger @@ -23,10 +23,10 @@ from ..data_structure.constants import PathLike -__all__ = ['Logger'] +__all__ = ['setup_logger'] -class Logger: +class LoggerWrapper: """A wrapper for logger tools.""" is_setup = False @@ -69,28 +69,24 @@ def filter_unique(cls, record: 'loguru.Record', level_name: str = 'WARNING') -> @classmethod def setup_logging( cls, - level: str = 'INFO', + level: Literal['RPC', 'TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO', log_path: 'Optional[PathLike]' = None, replace: bool = True, - log_rpc_code: bool = False, ) -> 'loguru.Logger': """Setup logging to file and console. Args: - level (str, optional): logging level. Defaults to "INFO", can be "DEBUG", "INFO", "WARNING", - "ERROR", "CRITICAL". + level (Literal['RPC', 'TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'], optional): + logging level. Defaults to "INFO", find more in https://loguru.readthedocs.io/en/stable/api/logger.html. log_path (Path, optional): path to save the log file. Defaults to None. replace (bool, optional): replace the log file if exists. Defaults to True. - log_rpc_code (bool, optional): print the rpc code sending to engine. Defaults to False. """ - if log_rpc_code: - import os - - os.environ['RPC_DEBUG'] = '1' - if cls.is_setup: return logger + # add custom level called RPC, which is the minimum level + logger.level('RPC', no=1, color='', icon='📢') + logger.remove() # remove default logger logger.add(sink=lambda msg: rprint(msg, end=''), level=level, format=cls.logger_format) # logger.add(RichHandler(level=level, rich_tracebacks=True, markup=True), level=level, format='{message}') @@ -106,6 +102,28 @@ def setup_logging( return logger +def setup_logger( + level: Literal['RPC', 'TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO', + log_path: 'Optional[PathLike]' = None, + replace: bool = True, +) -> 'loguru.Logger': + """Setup logging to file and console. + + Args: + level (Literal['TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'], optional): logging level. + Defaults to 'INFO', find more in https://loguru.readthedocs.io/en/stable/api/logger.html. + The order of the levels is: + 'RPC' (custom level): logging RPC messages which are sent by RPC protocols. + 'TRACE': logging engine output like console output of blender. + 'DEBUG': logging debug messages. + 'INFO': logging info messages. + ... + log_path (Path, optional): path to save the log file. Defaults to None. + replace (bool, optional): replace the log file if exists. Defaults to True. + """ + return LoggerWrapper.setup_logging(level, log_path, replace) + + #### (rich) progress bar #### class SpeedColumn(ProgressColumn): """Renders the speed of a task.""" From 4d004780cfd39b1a4920db8ca941ebb4022c7dbe Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 5 Dec 2023 17:25:10 +0800 Subject: [PATCH 033/110] add feature of `reload_rpc_code` --- xrfeitoria/factory.py | 28 ++++++++++++++---- xrfeitoria/rpc/factory.py | 13 ++++----- xrfeitoria/utils/runner.py | 60 +++++++++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/xrfeitoria/factory.py b/xrfeitoria/factory.py index d8913a49..2ec9de16 100644 --- a/xrfeitoria/factory.py +++ b/xrfeitoria/factory.py @@ -2,6 +2,7 @@ from . import _tls from .data_structure.constants import EngineEnum, PathLike, default_level_blender +from .utils import setup_logger __all__ = ['init_blender', 'init_unreal'] @@ -25,6 +26,7 @@ def __init__( engine_exec: Optional[PathLike] = None, project_path: Optional[PathLike] = None, background: bool = False, + reload_rpc_code: bool = False, replace_plugin: bool = False, dev_plugin: bool = False, new_process: bool = False, @@ -36,6 +38,9 @@ def __init__( exec_path (Optional[PathLike], optional): Path to Blender executable. Defaults to None. project_path (Optional[PathLike], optional): Path to Blender project. Defaults to None. background (bool, optional): Whether to start Blender in background. Defaults to False. + reload_rpc_code (bool, optional): whether to reload the registered rpc functions and classes. + If you are developing the package or writing a custom remote function, set this to True to reload the code. + This will only be in effect when `new_process=False` if the engine process is reused. Defaults to False. replace_plugin (bool, optional): Whether to replace the plugin. Defaults to False. dev_plugin (bool, optional): Whether to use the plugin under local directory. If False, would use the plugin downloaded from a remote server. Defaults to False. @@ -49,9 +54,8 @@ def __init__( from .sequence.sequence_wrapper import SequenceWrapperBlender # isort:skip from .utils.runner import BlenderRPCRunner # isort:skip from .utils.functions import blender_functions # isort:skip - from .utils.tools import Logger # isort:skip - self.logger = Logger.setup_logging() # default level is INFO + self.logger = setup_logger() # default level is INFO self.ObjectUtils = ObjectUtilsBlender self.Camera = CameraBlender self.Actor = ActorBlender @@ -64,6 +68,7 @@ def __init__( self._rpc_runner = BlenderRPCRunner( engine_exec=engine_exec, project_path=project_path, + reload_rpc_code=reload_rpc_code, replace_plugin=replace_plugin, dev_plugin=dev_plugin, background=background, @@ -90,6 +95,7 @@ def __init__( engine_exec: Optional[PathLike] = None, project_path: Optional[PathLike] = None, background: bool = False, + reload_rpc_code: bool = False, replace_plugin: bool = False, dev_plugin: bool = False, new_process: bool = False, @@ -100,6 +106,9 @@ def __init__( exec_path (Optional[PathLike], optional): Path to Unreal executable. Defaults to None. project_path (Optional[PathLike], optional): Path to Unreal project. Defaults to None. background (bool, optional): Whether to start Unreal in background. Defaults to False. + reload_rpc_code (bool, optional): whether to reload the registered rpc functions and classes. + If you are developing the package or writing a custom remote function, set this to True to reload the code. + This will only be in effect when `new_process=False` if the engine process is reused. Defaults to False. replace_plugin (bool, optional): Whether to replace the plugin. Defaults to False. dev_plugin (bool, optional): Whether to use the plugin under local directory. If False, would use the plugin downloaded from a remote server. Defaults to False. @@ -112,9 +121,8 @@ def __init__( from .sequence.sequence_wrapper import SequenceWrapperUnreal # isort:skip from .utils.runner import UnrealRPCRunner # isort:skip from .utils.functions import unreal_functions # isort:skip - from .utils.tools import Logger # isort:skip - self.logger = Logger.setup_logging() # default level is INFO + self.logger = setup_logger() # default level is INFO self.ObjectUtils = ObjectUtilsUnreal self.Camera = CameraUnreal self.Actor = ActorUnreal @@ -126,6 +134,7 @@ def __init__( self._rpc_runner = UnrealRPCRunner( engine_exec=engine_exec, project_path=project_path, + reload_rpc_code=reload_rpc_code, replace_plugin=replace_plugin, dev_plugin=dev_plugin, background=background, @@ -161,6 +170,7 @@ def __init__( exec_path: Optional[PathLike] = None, project_path: Optional[PathLike] = None, background: bool = False, + reload_rpc_code: bool = False, replace_plugin: bool = False, dev_plugin: bool = False, cleanup: bool = True, @@ -172,6 +182,9 @@ def __init__( exec_path (Optional[PathLike], optional): Path to Blender executable. Defaults to None. project_path (Optional[PathLike], optional): Path to Blender project. Defaults to None. background (bool, optional): Whether to start Blender in background. Defaults to False. + reload_rpc_code (bool, optional): whether to reload the registered rpc functions and classes. + If you are developing the package or writing a custom remote function, set this to True to reload the code. + This will only be in effect when `new_process=False` if the engine process is reused. Defaults to False. replace_plugin (bool, optional): Whether to replace the plugin. Defaults to False. dev_plugin (bool, optional): Whether to use the plugin under local directory. If False, would use the plugin downloaded from a remote server. Defaults to False. @@ -190,12 +203,12 @@ def __init__( pip install -e . python -c "import xrfeitoria as xf; xf.init_blender(replace_plugin=True, dev_plugin=True)" """ - _tls.cache = {'platform': None, 'engine_process': None, 'unreal_project_path': None} _tls.cache['platform'] = EngineEnum.blender self._cleanup = cleanup super().__init__( engine_exec=exec_path, project_path=project_path, + reload_rpc_code=reload_rpc_code, replace_plugin=replace_plugin, dev_plugin=dev_plugin, background=background, @@ -243,6 +256,7 @@ def __init__( exec_path: Optional[PathLike] = None, project_path: Optional[PathLike] = None, background: bool = False, + reload_rpc_code: bool = False, replace_plugin: bool = False, dev_plugin: bool = False, new_process: bool = False, @@ -253,6 +267,9 @@ def __init__( exec_path (Optional[PathLike], optional): Path to Unreal executable. Defaults to None. project_path (Optional[PathLike], optional): Path to Unreal project. Defaults to None. background (bool, optional): Whether to start Unreal in background. Defaults to False. + reload_rpc_code (bool, optional): whether to reload the registered rpc functions and classes. + If you are developing the package or writing a custom remote function, set this to True to reload the code. + This will only be in effect when `new_process=False` if the engine process is reused. Defaults to False. replace_plugin (bool, optional): Whether to replace the plugin. Defaults to False. dev_plugin (bool, optional): Whether to use the plugin under local directory. If False, would use the plugin downloaded from a remote server. Defaults to False. @@ -276,6 +293,7 @@ def __init__( super().__init__( engine_exec=exec_path, project_path=project_path, + reload_rpc_code=reload_rpc_code, replace_plugin=replace_plugin, dev_plugin=dev_plugin, background=background, diff --git a/xrfeitoria/rpc/factory.py b/xrfeitoria/rpc/factory.py index 75820e66..83cf2e4f 100644 --- a/xrfeitoria/rpc/factory.py +++ b/xrfeitoria/rpc/factory.py @@ -26,6 +26,7 @@ class RPCFactory: rpc_client: RPCClient = None + reload_rpc_code: bool = False file_path = None remap_pairs = [] default_imports = [] @@ -52,9 +53,6 @@ def setup(cls, port: int, remap_pairs: List[str] = None, default_imports: List[s cls.rpc_client = RPCClient(port) cls.remap_pairs = remap_pairs cls.default_imports = default_imports or [] - if os.environ.get('RPC_RELOAD'): - # clear the registered functions, so they can be re-registered - cls.registered_function_names.clear() @staticmethod def _get_docstring(code: List[str], function_name: str) -> str: @@ -172,7 +170,7 @@ def _register(cls, function: Callable) -> List[str]: from loguru import logger # if function registered, skip it - if function.__name__ in cls.registered_function_names: + if function.__name__ in cls.registered_function_names and not cls.reload_rpc_code: logger.debug(f'Function "{function.__name__}" has already been registered with the server!') return [] @@ -188,10 +186,9 @@ def _register(cls, function: Callable) -> List[str]: response = cls.rpc_client.proxy.add_new_callable(function.__name__, '\n'.join(code), additional_paths) cls.registered_function_names.append(function.__name__) - if os.environ.get('RPC_DEBUG'): - _code = '\n'.join(code) - logger.debug(f'code:\n{_code}') - logger.debug(f'response: {response}') + _code = '\n'.join(code) + logger.log('RPC', f'code:\n{_code}') + logger.log('RPC', f'response: {response}') except ConnectionRefusedError: server_name = os.environ.get(f'RPC_SERVER_{cls.rpc_client.port}', cls.rpc_client.port) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index ea2c0666..e68e0086 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -24,7 +24,7 @@ from .. import __version__, _tls from ..data_structure.constants import EngineEnum, PathLike, plugin_name_blender, plugin_name_unreal, tmp_dir -from ..rpc import BLENDER_PORT, UNREAL_PORT, remote_blender, remote_unreal +from ..rpc import BLENDER_PORT, UNREAL_PORT, factory, remote_blender, remote_unreal from .downloader import download from .setup import get_exec_path @@ -70,6 +70,7 @@ def __init__( new_process: bool = False, engine_exec: Optional[PathLike] = None, project_path: PathLike = '', + reload_rpc_code: bool = False, replace_plugin: bool = False, dev_plugin: bool = False, background: bool = True, @@ -80,6 +81,9 @@ def __init__( new_process (bool, optional): whether to start a new process. Defaults to False. engine_exec (Optional[PathLike], optional): path to engine executable. Defaults to None. project_path (PathLike, optional): path to project. Defaults to ''. + reload_rpc_code (bool, optional): whether to reload the registered rpc functions and classes. + If you are developing the package or writing a custom remote function, set this to True to reload the code. + This will only be in effect when `new_process=False` if the engine process is reused. Defaults to False. replace_plugin (bool, optional): whether to replace the plugin installed for the engine. Defaults to False. dev_plugin (bool, optional): Whether to use the plugin under local directory. If False, would use the plugin downloaded from a remote server. Defaults to False. @@ -93,7 +97,12 @@ def __init__( self.replace_plugin = replace_plugin self.dev_plugin = dev_plugin self.background = background - self.debug = os.environ.get('RPC_DEBUG', '0') == '1' # logger.level('DEBUG') + self.debug = logger._core.min_level <= 10 # DEBUG level + + if reload_rpc_code: + # clear registered functions and classes for reloading + factory.RPCFactory.registered_function_names.clear() + factory.RPCFactory.reload_rpc_code = True if self.dev_plugin: self.replace_plugin = True @@ -227,22 +236,21 @@ def _popen(cmd: str) -> subprocess.Popen: process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) return process - def start(self): - """Start rpc server.""" - if not self.new_process: - return - - with self.console.status('Initializing RPC server...') as status: - status.update(status='[green bold]Installing plugin...') - self._install_plugin() - status.update(status=f'[green bold]Starting {" ".join(self.engine_info)} as RPC server...') - self.engine_process = self._start_rpc(background=self.background, project_path=self.project_path) - self.engine_running = True - _tls.cache['engine_process'] = self.engine_process - logger.info(f'RPC server started at port {self.port}') + def _receive_stdout(self) -> None: + """Receive output from the subprocess, and log it if `self.debug=True`. - # check if engine process is alive in a separate thread - threading.Thread(target=self.check_engine_alive, daemon=True).start() + This function should be called in a separate thread. + """ + # start receiving + while True: + try: + data = self.engine_process.stdout.readline().decode() + except AttributeError: + break + if not data: + break + # log in debug level + logger.trace(f'\[blender] {data.strip()}') def check_engine_alive(self) -> bool: """Check if the engine process is alive.""" @@ -264,6 +272,24 @@ def get_process_output(process: subprocess.Popen) -> str: ) return out + def start(self) -> None: + """Start rpc server.""" + if not self.new_process: + return + + with self.console.status('Initializing RPC server...') as status: + status.update(status='[green bold]Installing plugin...') + self._install_plugin() + status.update(status=f'[green bold]Starting {" ".join(self.engine_info)} as RPC server...') + self.engine_process = self._start_rpc(background=self.background, project_path=self.project_path) + self.engine_running = True + _tls.cache['engine_process'] = self.engine_process + logger.info(f'RPC server started at port {self.port}') + + # check if engine process is alive in a separate thread + threading.Thread(target=self._receive_stdout).start() + threading.Thread(target=self.check_engine_alive, daemon=True).start() + def wait_for_start(self, process: subprocess.Popen) -> None: """Wait 3 minutes for RPC server to start. From 1a3568261801f03b8cf19dc48353e9e8e6d33d30 Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Thu, 7 Dec 2023 16:32:58 +0800 Subject: [PATCH 034/110] fix bug of blender's mask --- src/XRFeitoriaBpy/core/factory.py | 4 ++++ xrfeitoria/sequence/sequence_blender.py | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 285f317c..6be879ce 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -205,6 +205,8 @@ def open_sequence(seq_name: str) -> 'bpy.types.Scene': for actor_data in seq_collection.sequence_properties.level_actors: actor = actor_data.actor actor.pass_index = actor_data.sequence_stencil_value + for child in actor.children: + child.pass_index = actor_data.sequence_stencil_value if actor_data.sequence_animation: XRFeitoriaBlenderFactory.apply_action_to_actor(action=actor_data.sequence_animation, actor=actor) @@ -233,6 +235,8 @@ def close_sequence() -> None: for actor_data in collection.sequence_properties.level_actors: actor = actor_data.actor actor.pass_index = actor_data.level_stencil_value + for child in actor.children: + child.pass_index = actor_data.level_stencil_value if actor_data.level_animation: XRFeitoriaBlenderFactory.apply_action_to_actor(action=actor_data.level_animation, actor=actor) else: diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index 756305a7..430d5010 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -146,10 +146,10 @@ def _import_actor_in_engine( """Import. Args: - file_path (PathLike): _description_ - transform_keys (Union[List[Dict], Dict]): _description_ - actor_name (str, optional): _description_. Defaults to 'Actor'. - stencil_value (int, optional): _description_. Defaults to 1. + file_path (PathLike): Path of the imported file. + transform_keys (Union[List[Dict], Dict]): Transform keys of the imported actor. + actor_name (str, optional): Name of the actor. Defaults to 'Actor'. + stencil_value (int, optional): Stencil value of the actor. Defaults to 1. """ if not isinstance(transform_keys, list): transform_keys = [transform_keys] @@ -157,7 +157,10 @@ def _import_actor_in_engine( ActorBlender._import_actor_from_file_in_engine(file_path=file_path, actor_name=actor_name) ObjectUtilsBlender._set_transform_keys_in_engine(obj_name=actor_name, transform_keys=transform_keys) # XXX: set stencil value. may use actor property - bpy.data.objects[actor_name].pass_index = stencil_value + actor = bpy.data.objects[actor_name] + actor.pass_index = stencil_value + for child in actor.children: + child.pass_index = stencil_value @staticmethod def _spawn_camera_in_engine( @@ -229,7 +232,10 @@ def _spawn_shape_in_engine( ) ObjectUtilsBlender._set_transform_keys_in_engine(obj_name=shape_name, transform_keys=transform_keys) # XXX: set stencil value. may use actor property - bpy.data.objects[shape_name].pass_index = stencil_value + actor = bpy.data.objects[shape_name] + actor.pass_index = stencil_value + for child in actor.children: + child.pass_index = stencil_value # -------- use methods -------- # @staticmethod @@ -307,7 +313,9 @@ def _use_actor_in_engine( # set level actor's properties actor.pass_index = stencil_value + for child in actor.children: + child.pass_index = stencil_value if action: XRFeitoriaBlenderFactory.apply_action_to_actor(action=action, actor=actor) ObjectUtilsBlender._set_transform_keys_in_engine(obj_name=actor_name, transform_keys=transform_keys) - level_actor_data.sequence_animation = actor.animation_data.action if actor.animation_data else None + level_actor_data.sequence_animation = actor.animation_data.action if actor.animation_data else None \ No newline at end of file From a1d65e29ffc6fe5b25b45294927b3a6e884e471d Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Thu, 7 Dec 2023 16:35:44 +0800 Subject: [PATCH 035/110] pre-commit --- xrfeitoria/sequence/sequence_blender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index 430d5010..b5e3539f 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -318,4 +318,4 @@ def _use_actor_in_engine( if action: XRFeitoriaBlenderFactory.apply_action_to_actor(action=action, actor=actor) ObjectUtilsBlender._set_transform_keys_in_engine(obj_name=actor_name, transform_keys=transform_keys) - level_actor_data.sequence_animation = actor.animation_data.action if actor.animation_data else None \ No newline at end of file + level_actor_data.sequence_animation = actor.animation_data.action if actor.animation_data else None From 6cab0c4a31ed32a495a4b61b92fd5776e7dddc2c Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Sat, 9 Dec 2023 16:40:15 +0800 Subject: [PATCH 036/110] add PrimDiffusion --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b598bee0..9163c027 100644 --- a/README.md +++ b/README.md @@ -87,11 +87,12 @@ Please follow the instructions [here](/samples/README.md). | Project | Teaser | Engine | | :---: | :---: | :---: | -| [Synbody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | -| [Zolly: Zoom Focal Length Correctly for Perspective-Distorted Human Mesh Reconstruction](https://wenjiawang0312.github.io/projects/zolly/) | | Blender | -| [SHERF: Generalizable Human NeRF from a Single Image](https://skhu101.github.io/SHERF/) | | Blender | -| [MatrixCity: A Large-scale City Dataset for City-scale Neural Rendering and Beyond](https://city-super.github.io/matrixcity/) | | Unreal Engine | +| [Synbody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | +| [Zolly: Zoom Focal Length Correctly for Perspective-Distorted Human Mesh Reconstruction](https://wenjiawang0312.github.io/projects/zolly/) | | Blender | +| [SHERF: Generalizable Human NeRF from a Single Image](https://skhu101.github.io/SHERF/) | | Blender | +| [MatrixCity: A Large-scale City Dataset for City-scale Neural Rendering and Beyond](https://city-super.github.io/matrixcity/) | | Unreal Engine | | [HumanLiff: Layer-wise 3D Human Generation with Diffusion Model](https://skhu101.github.io/HumanLiff/) | | Blender | +| [PrimDiffusion: Volumetric Primitives Diffusion for 3D Human Generation](https://frozenburning.github.io/projects/primdiffusion/) | | Blender | ## License From cf62d4fc8705953dbebc05637b64514cea5ac2ea Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Sat, 9 Dec 2023 23:25:18 +0800 Subject: [PATCH 037/110] SynBody --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9163c027..875cec8d 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Please follow the instructions [here](/samples/README.md). | Project | Teaser | Engine | | :---: | :---: | :---: | -| [Synbody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | +| [SynBody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | | [Zolly: Zoom Focal Length Correctly for Perspective-Distorted Human Mesh Reconstruction](https://wenjiawang0312.github.io/projects/zolly/) | | Blender | | [SHERF: Generalizable Human NeRF from a Single Image](https://skhu101.github.io/SHERF/) | | Blender | | [MatrixCity: A Large-scale City Dataset for City-scale Neural Rendering and Beyond](https://city-super.github.io/matrixcity/) | | Unreal Engine | From 6de395e5dd8807ef8615ca86c5fd0224f2ea31b5 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 11:20:35 +0800 Subject: [PATCH 038/110] add `dump_humandata` for `Motion` --- xrfeitoria/utils/anim/motion.py | 245 +++++++++++++++++++++++++++++++- xrfeitoria/utils/anim/utils.py | 58 +++++++- 2 files changed, 290 insertions(+), 13 deletions(-) diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index 78b7b0ce..d4695afc 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -1,11 +1,13 @@ from collections import OrderedDict from functools import partial -from typing import Callable, Dict, List, Optional +from pathlib import Path +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple import numpy as np from scipy.spatial.transform import Rotation as spRotation from typing_extensions import Self +from ...data_structure.constants import PathLike from .constants import ( NUM_SMPLX_BODYJOINTS, SMPL_IDX_TO_JOINTS, @@ -159,11 +161,11 @@ def convert_fps_smplx_data(self, smplx_data: Dict[str, np.ndarray], scaling: int smplx_data[key] = value[::scaling, :] return smplx_data - def convert_fps(self, fps): + def convert_fps(self, fps: float): """Converts the frames per second (fps) of the animation to the specified value. Args: - fps (int): The desired frames per second. + fps (float): The desired frames per second. Raises: NotImplementedError: @@ -287,6 +289,9 @@ def get_motion_data(self) -> List[Dict[str, Dict[str, List[float]]]]: motion_data.append(frame_motion_data) return motion_data + def __repr__(self) -> str: + return f'Motion(n_frames={self.n_frames}, fps={self.fps})' + class SMPLMotion(Motion): SMPL_IDX_TO_NAME: Dict[int, str] = OrderedDict(SMPL_IDX_TO_JOINTS) @@ -336,7 +341,7 @@ def from_smpl_data( SMPLMotion: An instance of SMPLMotion containing the smpl_data. """ smpl_data = dict(smpl_data) - _get_smpl = partial(_get_from_smpl_x, smpl_x_data=smpl_data, dtype=np.float32) + _get_smpl = partial(_get_from_smpl_x_, smpl_x_data=smpl_data, dtype=np.float32) n_frames = smpl_data['body_pose'].shape[0] betas = _get_smpl('betas', shape=[1, 10]) @@ -402,6 +407,58 @@ def get_parent_bone_name(self, bone_name) -> Optional[str]: else: return self.BONE_NAMES[parent_idx] + def dump_motion_data( + self, + filepath: PathLike, + betas: np.ndarray, + meta: Optional[Dict[str, Any]] = None, + global_orient_offset: np.ndarray = np.zeros(3), + transl_offset: np.ndarray = np.zeros(3), + root_location_t0: Optional[np.ndarray] = None, + pelvis_location_t0: Optional[np.ndarray] = None, + ) -> None: + """Dump the motion data to a motion data file at the given `filepath`. + + The motion data file is a npz file containing the following keys: + ``` + motion_data = { + '__data_len__': n_frames, + 'smpl': { + 'betas': betas, # (1, 10) + 'transl': transl, # (n_frames, 3) + 'global_orient': global_orient, # (n_frames, 3) + 'body_pose': body_pose, # (n_frames, 69) + }, + 'meta': {'gender': 'neutral'}, # optional + } + ``` + + Args: + filepath (PathLike): The filepath to dump the motion data to. + betas (np.ndarray): The betas array. + meta (Optional[Dict[str, Any]]): Additional metadata. Defaults to None. + global_orient_offset (np.ndarray): The global orientation offset. Defaults to np.zeros(3). + transl_offset (np.ndarray): The translation offset. Defaults to np.zeros(3). + root_location_t0 (Optional[np.ndarray]): The root location at time 0. Defaults to None. + pelvis_location_t0 (Optional[np.ndarray]): The pelvis location at time 0. Defaults to None. + """ + humandata = get_humandata( + smpl_x_data=self.smplx_data, + smpl_x_type='smpl', + betas=betas, + meta=meta, + global_orient_offset=global_orient_offset, + transl_offset=transl_offset, + root_location_t0=root_location_t0, + pelvis_location_t0=pelvis_location_t0, + ) + filepath = Path(filepath).resolve() + filepath.parent.mkdir(parents=True, exist_ok=True) + np.savez(filepath, **humandata) + + def __repr__(self) -> str: + return f'SMPLMotion(n_frames={self.n_frames}, fps={self.fps})' + class SMPLXMotion(Motion): SMPLX_IDX_TO_NAME: Dict[int, str] = OrderedDict(SMPLX_IDX_TO_JOINTS) @@ -459,7 +516,7 @@ def from_smplx_data( SMPLXMotion: An instance of SMPLXMotion containing the smplx_data. """ smplx_data = dict(smplx_data) - _get_smplx = partial(_get_from_smpl_x, smpl_x_data=smplx_data, dtype=np.float32) + _get_smplx = partial(_get_from_smpl_x_, smpl_x_data=smplx_data, dtype=np.float32) n_frames = smplx_data['body_pose'].shape[0] betas = _get_smplx('betas', shape=[1, 10]) transl = _get_smplx('transl', shape=[n_frames, 3], required=False) @@ -611,8 +668,66 @@ def get_parent_bone_name(self, bone_name) -> Optional[str]: else: return self.BONE_NAMES[parent_idx] + def dump_humandata( + self, + filepath: PathLike, + betas: np.ndarray, + meta: Optional[Dict[str, Any]] = None, + global_orient_offset: np.ndarray = np.zeros(3), + transl_offset: np.ndarray = np.zeros(3), + root_location_t0: Optional[np.ndarray] = None, + pelvis_location_t0: Optional[np.ndarray] = None, + ) -> None: + """Dump the motion data to a humandata file at the given `filepath`. + + The humandata file is a npz file containing the following keys: + ```python + humandata = { + '__data_len__': n_frames, + 'smplx': { + 'betas': betas, # (1, 10) + 'transl': transl, # (n_frames, 3) + 'global_orient': global_orient, # (n_frames, 3) + 'body_pose': body_pose, # (n_frames, 63) + 'jaw_pose': jaw_pose, # (n_frames, 3) + 'leye_pose': leye_pose, # (n_frames, 3) + 'reye_pose': reye_pose, # (n_frames, 3) + 'left_hand_pose': left_hand_pose, # (n_frames, 45) + 'right_hand_pose': right_hand_pose, # (n_frames, 45) + 'expression': expression, # (n_frames, 10) + }, + 'meta': {'gender': 'neutral'}, # optional + } + ``` + + Args: + filepath (PathLike): The filepath to dump the motion data to. + betas (np.ndarray): The betas array. + meta (Optional[Dict[str, Any]]): Additional metadata. Defaults to None. + global_orient_offset (np.ndarray): The global orientation offset. Defaults to np.zeros(3). + transl_offset (np.ndarray): The translation offset. Defaults to np.zeros(3). + root_location_t0 (Optional[np.ndarray]): The root location at time 0. Defaults to None. + pelvis_location_t0 (Optional[np.ndarray]): The pelvis location at time 0. Defaults to None. + """ + humandata = get_humandata( + smpl_x_data=self.smplx_data, + smpl_x_type='smplx', + betas=betas, + meta=meta, + global_orient_offset=global_orient_offset, + transl_offset=transl_offset, + root_location_t0=root_location_t0, + pelvis_location_t0=pelvis_location_t0, + ) + filepath = Path(filepath).resolve() + filepath.parent.mkdir(parents=True, exist_ok=True) + np.savez(filepath, **humandata) + + def __repr__(self) -> str: + return f'SMPLXMotion(n_frames={self.n_frames}, fps={self.fps})' + -def _get_from_smpl_x(key, shape, *, smpl_x_data, dtype=np.float32, required=True) -> np.ndarray: +def _get_from_smpl_x_(key, shape, *, smpl_x_data, dtype=np.float32, required=True) -> np.ndarray: """Get data from smpl-x data dict. Args: @@ -633,3 +748,121 @@ def _get_from_smpl_x(key, shape, *, smpl_x_data, dtype=np.float32, required=True _data = _data[:, :n_dims] # XXX: handle the case that n_dims > data.shape[1] return _data return np.zeros(shape, dtype=dtype) + + +def _transform_transl_global_orient_( + global_orient: np.ndarray, + transl: np.ndarray, + global_orient_offset: np.ndarray, + transl_offset: np.ndarray, + root_location_t0: Optional[np.ndarray] = None, + pelvis_location_t0: Optional[np.ndarray] = None, +) -> Tuple[np.ndarray, np.ndarray]: + """ + Transform the global orientation and translation based on the given offsets. + + Args: + global_orient (np.ndarray): Global orientation array. + transl (np.ndarray): Translation array. + global_orient_offset (np.ndarray): Global orientation offset array. + transl_offset (np.ndarray): Translation offset array. + root_location_t0 (Optional[np.ndarray]): Root location at time 0. Defaults to None. + pelvis_location_t0 (Optional[np.ndarray]): Pelvis location at time 0. Defaults to None. + + Returns: + Tuple[np.ndarray, np.ndarray]: Transformed global orientation and translation arrays. + """ + R_offset = spRotation.from_rotvec(global_orient_offset) * spRotation.from_rotvec(global_orient[0, :]).inv() + global_orient_ = (R_offset * spRotation.from_rotvec(global_orient)).as_rotvec() + + loc0 = transl[0, :] + + if pelvis_location_t0 is not None and root_location_t0 is not None: + transl_offset_t0 = pelvis_location_t0 - root_location_t0 + rot_pivot_offset = transl_offset_t0 + transl_offset - loc0 + transl_ = R_offset.apply(transl + rot_pivot_offset) - pelvis_location_t0 + else: + transl_ = transl + transl_offset - loc0 + + return global_orient_, transl_ + + +def get_humandata( + smpl_x_data: Dict[str, np.ndarray], + smpl_x_type: Literal['smpl', 'smplx'], + betas: np.ndarray, + meta: Optional[Dict[str, Any]] = None, + global_orient_offset: np.ndarray = np.zeros(3), + transl_offset: np.ndarray = np.zeros(3), + root_location_t0: Optional[np.ndarray] = None, + pelvis_location_t0: Optional[np.ndarray] = None, +) -> Dict[str, Any]: + """ + Get human data for a given set of parameters. + + Args: + smpl_x_data (Dict[str, np.ndarray]): Dictionary containing the SMPL-X data. + smpl_x_type (Literal['smpl', 'smplx']): Type of SMPL-X model. + betas (np.ndarray): Array of shape (n, 10) representing the shape parameters. + meta (Optional[Dict[str, Any]], optional): Additional metadata. Defaults to None. + global_orient_offset (np.ndarray): Array of shape (n, 3) representing the global orientation offset. + transl_offset (np.ndarray): Array of shape (3,) representing the translation offset. + root_location_t0 (Optional[np.ndarray], optional): Array of shape (3,) representing the root location at time t=0. Defaults to None. + pelvis_location_t0 (Optional[np.ndarray], optional): Array of shape (3,) representing the pelvis location at time t=0. Defaults to None. + + Returns: + dict: Dictionary containing the human data. + """ + global_orient = smpl_x_data['global_orient'].reshape(-1, 3) + n = global_orient.shape[0] + transl = smpl_x_data['transl'].reshape(n, 3) + body_pose = smpl_x_data['body_pose'].reshape(n, -1) + bone_len = body_pose.shape[1] + assert n > 0, f'Got n_frames={n}, should be > 0.' + assert bone_len in (63, 69), f'Got body_pose in [{n}, {bone_len}], should be in shape of [n, 63] or [n, 69].' + + # transform + global_orient_, transl_ = _transform_transl_global_orient_( + global_orient=global_orient, + transl=transl, + global_orient_offset=global_orient_offset, + transl_offset=transl_offset, + root_location_t0=root_location_t0, + pelvis_location_t0=pelvis_location_t0, + ) + + if smpl_x_type == 'smpl': + if bone_len == 69: + body_pose_ = body_pose + elif bone_len == 63: + body_pose_ = np.concatenate([body_pose, np.zeros([n, 6])], axis=1, dtype=np.float32) + else: + body_pose_ = body_pose[:, :63] + + smpl_x_data_ = { + 'betas': betas.astype(np.float32), + 'global_orient': global_orient_.astype(np.float32), + 'transl': transl_.astype(np.float32), + 'body_pose': body_pose_.astype(np.float32), + } + + if smpl_x_type == 'smplx': + extra = { + 'left_hand_pose': np.zeros([n, 45], dtype=np.float32), + 'right_hand_pose': np.zeros([n, 45], dtype=np.float32), + 'jaw_pose': np.zeros([n, 3], dtype=np.float32), + 'leye_pose': np.zeros([n, 3], dtype=np.float32), + 'reye_pose': np.zeros([n, 3], dtype=np.float32), + 'expression': np.zeros([n, 10], dtype=np.float32), + } + for k, v in extra.items(): + if k in smpl_x_data: + extra[k] = smpl_x_data[k].reshape(v.shape).astype(np.float32) + smpl_x_data_.update(extra) + + humandata = { + '__data_len__': global_orient_.shape[0], + smpl_x_type: smpl_x_data_, + 'meta': meta, + } + return humandata diff --git a/xrfeitoria/utils/anim/utils.py b/xrfeitoria/utils/anim/utils.py index 597682bb..079f9fb3 100644 --- a/xrfeitoria/utils/anim/utils.py +++ b/xrfeitoria/utils/anim/utils.py @@ -3,14 +3,15 @@ import numpy as np +from ...data_structure.constants import PathLike from .motion import Motion, SMPLMotion, SMPLXMotion -def load_amass_motion(input_amass_smplx_path: Union[Path, str]) -> Motion: +def load_amass_motion(input_amass_smplx_path: PathLike) -> SMPLXMotion: """Load AMASS SMPLX motion data. Args: - input_amass_smplx_path (Union[Path, str]): Path to AMASS SMPLX motion data. + input_amass_smplx_path (PathLike): Path to AMASS SMPLX motion data. Returns: Motion: Motion data, which consists of data read from AMASS file. @@ -22,16 +23,15 @@ def load_amass_motion(input_amass_smplx_path: Union[Path, str]) -> Motion: # src_actor_name = "SMPLX" amass_smplx_data = np.load(input_amass_smplx_path, allow_pickle=True) src_motion = SMPLXMotion.from_amass_data(amass_smplx_data, insert_rest_pose=True) - return src_motion -def load_humandata_motion(input_humandata_path: Union[Path, str]) -> Motion: +def load_humandata_motion(input_humandata_path: PathLike) -> Union[SMPLMotion, SMPLXMotion]: """Load humandata SMPL / SMPLX motion data. HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md Args: - input_humandata_path (Union[Path, str]): Path to humandata SMPL / SMPLX motion data. + input_humandata_path (PathLike): Path to humandata SMPL / SMPLX motion data. Returns: Motion: Motion data, which consists of data read from humandata file. @@ -49,10 +49,54 @@ def load_humandata_motion(input_humandata_path: Union[Path, str]) -> Motion: # src_actor_name = "SMPLX" smplx_data = humandata['smplx'].item() src_motion = SMPLXMotion.from_smplx_data(smplx_data=smplx_data, insert_rest_pose=False) - return src_motion +def dump_humandata(motion: SMPLXMotion, save_filepath: PathLike, meta_filepath: PathLike): + """ + Dump human data to a file. + + Args: + motion (SMPLXMotion): The SMPLXMotion object containing the motion data. + save_filepath (PathLike): The file path to save the dumped data. + meta_filepath (PathLike): The file path to the meta information, storing the parameters of the SMPL-XL model. + + The meta information is stored in the following format: + ```python + # meta = np.load(meta_filepath, allow_pickle=True) + meta = { + 'smplx': { + 'betas': betas, # (10,) + 'global_orient': global_orient, # (1, 3) + 'transl': transl, # (1, 3) + 'root_location_t0': root_location_t0, # (1, 3) + 'pelvis_location_t0': pelvis_location_t0, # (1, 3) + }, + 'meta': {'gender': 'neutral'} + } + ``` + + Returns: + None + """ + meta_info = np.load(meta_filepath, allow_pickle=True) + smplx = meta_info['smplx'].item() + motion.dump_humandata( + filepath=save_filepath, + betas=smplx['betas'], + meta=meta_info['meta'].item(), + global_orient_offset=smplx['global_orient'], + transl_offset=smplx['transl'], + root_location_t0=smplx['root_location_t0'], + pelvis_location_t0=smplx['pelvis_location_t0'], + ) + + if __name__ == '__main__': - motion = load_amass_motion('amass-smplx_n/ACCAD/s001/EricCamper04_stageii.npz') + """ + python -m xrfeitoria.utils.anim.utils + """ + motion = load_amass_motion('.cache/ACCAD/s001/EricCamper04_stageii.npz') motion_data = motion.get_motion_data() + dump_humandata(motion, '.cache/SMPL-XL_test.npz', '.cache/SMPL-XL-001.npz') + print('Done') From 2d4d160ac5b66866b0509faf33dd055b640eaec4 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 11:20:45 +0800 Subject: [PATCH 039/110] add output engine_out when error --- xrfeitoria/utils/runner.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index e68e0086..7534aa33 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -13,7 +13,7 @@ from http.client import RemoteDisconnected from pathlib import Path from textwrap import dedent -from typing import Dict, Optional, Tuple +from typing import Dict, List, Optional, Tuple from urllib.error import HTTPError, URLError from xmlrpc.client import ProtocolError @@ -93,6 +93,7 @@ def __init__( self.engine_type: EngineEnum = _tls.cache.get('platform', None) self.engine_process: Optional[subprocess.Popen] = None self.engine_running: bool = False + self.engine_outputs: List[str] = [] self.new_process = new_process self.replace_plugin = replace_plugin self.dev_plugin = dev_plugin @@ -250,7 +251,8 @@ def _receive_stdout(self) -> None: if not data: break # log in debug level - logger.trace(f'\[blender] {data.strip()}') + logger.trace(f'\[engine] {data.strip()}') + self.engine_outputs.append(data) def check_engine_alive(self) -> bool: """Check if the engine process is alive.""" @@ -261,14 +263,15 @@ def check_engine_alive(self) -> bool: os._exit(1) # exit main thread time.sleep(1) - @staticmethod - def get_process_output(process: subprocess.Popen) -> str: + def get_process_output(self, process: subprocess.Popen) -> str: """Get process output when process is exited with non-zero code.""" + # engine_out = process.stdout.read().decode("utf-8") + engine_out = ''.join(self.engine_outputs) out = ( f'Engine process exited with code {process.poll()}\n\n' - '>>>> Engine output >>>>\n\n' - f'{process.stdout.read().decode("utf-8")}' - '\n\n<<<< Engine output <<<<\n' + '[gray]>>>> Engine output >>>>\n\n[/gray]' + f'{engine_out}\n' + '[gray]<<<< Engine output <<<<\n[/gray]' ) return out From 494ff9c87a6a14f0eeb09c255c822822aefacc6e Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 11:21:11 +0800 Subject: [PATCH 040/110] polish sample07 --- samples/blender/07_amass.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index cd06d93c..4929b9ac 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -10,10 +10,19 @@ ** It is recommended to run this script with Blender >= 3.6 ** """ +from pathlib import Path + import xrfeitoria as xf from xrfeitoria.rpc import remote_blender from xrfeitoria.utils import setup_logger -from xrfeitoria.utils.anim import load_amass_motion +from xrfeitoria.utils.anim.utils import dump_humandata, load_amass_motion + +root = Path('.cache/sample-amass') +amass_file = root / 'EricCamper04_stageii.npz' +smpl_xl_file = root / 'SMPL-XL-001.fbx' +smpl_xl_meta_file = root / 'SMPL-XL-001.npz' +saved_blend_file = root / 'output.blend' +saved_humandata_file = root / 'output.npz' @remote_blender() @@ -25,16 +34,18 @@ def apply_scale(actor_name: str): bpy.ops.object.transform_apply(scale=True) -def main(): +def main(background: bool = False): logger = setup_logger() # Download Amass from https://amass.is.tue.mpg.de/download.php # For example, download ACCAD (SMPL-X N), and use any motion file from the uncompressed folder - motion = load_amass_motion('ACCAD/s001/EricCamper04_stageii.npz') # modify this to motion file in absolute path + motion = load_amass_motion(amass_file) # modify this to motion file in absolute path + motion.convert_fps(30) # convert the motion from 120fps (amass) to 30fps motion_data = motion.get_motion_data() xf_runner = xf.init_blender( - exec_path='C:/Program Files/Blender Foundation/Blender 3.3/blender.exe' + exec_path='C:/Program Files/Blender Foundation/Blender 3.6/blender.exe', + background=background, ) # modify this to your blender executable path # SMPL-XL model @@ -42,14 +53,20 @@ def main(): # or from https://openxlab.org.cn/datasets/OpenXDLab/SynBody/tree/main/Assets # With downloading this, you are agreeing to CC BY-NC-SA 4.0 License (https://creativecommons.org/licenses/by-nc-sa/4.0/) # 2. Import SMPL-XL model - actor = xf_runner.Actor.import_from_file('SMPL-XL-001.fbx') # modify this to SMPL-XL model file in absolute path + actor = xf_runner.Actor.import_from_file(smpl_xl_file) # modify this to SMPL-XL model file in absolute path apply_scale(actor.name) # SMPL-XL model is imported with scale, we need to apply scale to it + + logger.info('Applying motion data') xf_runner.utils.apply_motion_data_to_actor(motion_data=motion_data, actor_name=actor.name) + dump_humandata(motion, save_filepath=saved_humandata_file, meta_filepath=smpl_xl_meta_file) # Modify the frame range to the length of the motion frame_start, frame_end = xf_runner.utils.get_keys_range() xf_runner.utils.set_frame_range(frame_start, frame_end) + # Save the blend file + xf_runner.utils.save_blend(saved_blend_file, pack=True) + logger.info('🎉 [bold green]Success!') input('Press Any Key to Exit...') @@ -58,4 +75,10 @@ def main(): if __name__ == '__main__': - main() + from argparse import ArgumentParser + + args = ArgumentParser() + args.add_argument('--background', '-b', action='store_true') + args.parse_args() + + main(background=args.background) From ef4ec5b70e784da78e2cb252414a208626837186 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 11:21:19 +0800 Subject: [PATCH 041/110] bug fix --- samples/unreal/01_add_shapes.py | 4 +--- tests/blender/actor.py | 5 +++-- tests/blender/camera.py | 5 +++-- tests/blender/init.py | 5 +++-- tests/blender/level.py | 7 +++---- tests/blender/main.py | 4 +++- tests/blender/sequence.py | 5 +++-- tests/unreal/actor.py | 5 +++-- tests/unreal/camera.py | 5 +++-- tests/unreal/init.py | 7 +++---- tests/unreal/main.py | 4 +++- tests/unreal/sequence.py | 5 +++-- 12 files changed, 34 insertions(+), 27 deletions(-) diff --git a/samples/unreal/01_add_shapes.py b/samples/unreal/01_add_shapes.py index c5452554..3b8bd823 100644 --- a/samples/unreal/01_add_shapes.py +++ b/samples/unreal/01_add_shapes.py @@ -6,8 +6,6 @@ from pathlib import Path -from loguru import logger - import xrfeitoria as xf from xrfeitoria.utils import setup_logger @@ -20,7 +18,7 @@ def main(debug=False, background=False): - setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') # unreal will start in a separate process in RPC Server mode automatically, # and close using `xf_runner.close()` diff --git a/tests/blender/actor.py b/tests/blender/actor.py index a644a191..8cbd485c 100644 --- a/tests/blender/actor.py +++ b/tests/blender/actor.py @@ -7,9 +7,10 @@ from xrfeitoria.data_structure.constants import xf_obj_name from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey +from xrfeitoria.utils import setup_logger from ..config import assets_path -from ..utils import __timer__, _init_blender, setup_logger +from ..utils import __timer__, _init_blender root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' @@ -18,7 +19,7 @@ def actor_test(debug: bool = False, background: bool = False): - logger = setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') with _init_blender(background=background) as xf_runner: with __timer__('import_actor'): actor = xf_runner.Actor.import_from_file( diff --git a/tests/blender/camera.py b/tests/blender/camera.py index e93d1679..b55f1f9d 100644 --- a/tests/blender/camera.py +++ b/tests/blender/camera.py @@ -6,8 +6,9 @@ import numpy as np from xrfeitoria.data_structure.constants import xf_obj_name +from xrfeitoria.utils import setup_logger -from ..utils import __timer__, _init_blender, setup_logger +from ..utils import __timer__, _init_blender root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' @@ -15,7 +16,7 @@ def camera_test(debug: bool = False, background: bool = False): - logger = setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') with _init_blender(background=background) as xf_runner: with __timer__('spawn camera'): ## test spawn camera diff --git a/tests/blender/init.py b/tests/blender/init.py index aa096f04..7623e696 100644 --- a/tests/blender/init.py +++ b/tests/blender/init.py @@ -5,8 +5,9 @@ from loguru import logger from xrfeitoria.rpc import remote_blender +from xrfeitoria.utils import setup_logger -from ..utils import __timer__, _init_blender, setup_logger +from ..utils import __timer__, _init_blender @remote_blender() @@ -16,7 +17,7 @@ def test_blender(): def init_test(debug: bool = False, dev: bool = False, background: bool = False): - setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') with __timer__('init_blender'): with _init_blender(dev_plugin=dev, background=background) as xf_runner: test_blender() diff --git a/tests/blender/level.py b/tests/blender/level.py index 02e55cbc..44ab0d7b 100644 --- a/tests/blender/level.py +++ b/tests/blender/level.py @@ -3,14 +3,13 @@ """ from pathlib import Path -from loguru import logger - import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.factory import XRFeitoriaBlender +from xrfeitoria.utils import setup_logger from ..config import assets_path -from ..utils import __timer__, _init_blender, setup_logger +from ..utils import __timer__, _init_blender root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' @@ -43,7 +42,7 @@ def seq_simple(xf_runner: XRFeitoriaBlender, seq_name: str = 'seq_simple'): def level_test(debug=False, background=False): - setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') with _init_blender(background=background, new_process=True, cleanup=False, project_path=blend_sample) as xf_runner: with __timer__('create and sequence'): seq_simple(xf_runner) diff --git a/tests/blender/main.py b/tests/blender/main.py index 75d56ba8..046f69f6 100644 --- a/tests/blender/main.py +++ b/tests/blender/main.py @@ -3,7 +3,9 @@ """ from pathlib import Path -from ..utils import _init_blender, setup_logger +from xrfeitoria.utils import setup_logger + +from ..utils import _init_blender from .actor import actor_test from .camera import camera_test from .init import init_test diff --git a/tests/blender/sequence.py b/tests/blender/sequence.py index 08862f95..9b0c7e77 100644 --- a/tests/blender/sequence.py +++ b/tests/blender/sequence.py @@ -9,9 +9,10 @@ from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey from xrfeitoria.factory import XRFeitoriaBlender +from xrfeitoria.utils import setup_logger from ..config import assets_path -from ..utils import __timer__, _init_blender, setup_logger +from ..utils import __timer__, _init_blender root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' @@ -130,7 +131,7 @@ def seq_shape(xf_runner: XRFeitoriaBlender, seq_name='seq_shape'): def sequence_test(debug=False, background=False): - logger = setup_logger(debug=debug) + setup_logger(level='DEBUG' if debug else 'INFO') with _init_blender(background=background) as xf_runner: with __timer__('seq_actor'): seq_actor(xf_runner, seq_name='seq_actor') diff --git a/tests/unreal/actor.py b/tests/unreal/actor.py index bdc47120..144fe2c4 100644 --- a/tests/unreal/actor.py +++ b/tests/unreal/actor.py @@ -5,9 +5,10 @@ from loguru import logger from xrfeitoria.data_structure.constants import xf_obj_name +from xrfeitoria.utils import setup_logger from ..config import assets_path -from ..utils import __timer__, _init_unreal, setup_logger +from ..utils import __timer__, _init_unreal bunny_obj = assets_path['bunny'] kc_fbx = assets_path['koupen_chan'] @@ -15,7 +16,7 @@ def actor_test(debug: bool = False, background: bool = False): - setup_logger(debug=debug) + setup_logger(level='DEBUG' if debug else 'INFO') with _init_unreal(background=background) as xf_runner: with __timer__('import actor'): kc_path = xf_runner.utils.import_asset(path=kc_fbx) diff --git a/tests/unreal/camera.py b/tests/unreal/camera.py index 696be38f..1d96eb34 100644 --- a/tests/unreal/camera.py +++ b/tests/unreal/camera.py @@ -5,12 +5,13 @@ import numpy as np from xrfeitoria.data_structure.constants import xf_obj_name +from xrfeitoria.utils import setup_logger -from ..utils import __timer__, _init_unreal, setup_logger +from ..utils import __timer__, _init_unreal def camera_test(debug: bool = False, background: bool = False): - logger = setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') with _init_unreal(background=background) as xf_runner: with __timer__('spawn camera'): camera0 = xf_runner.Camera.spawn() diff --git a/tests/unreal/init.py b/tests/unreal/init.py index fd25eb8d..513be0b5 100644 --- a/tests/unreal/init.py +++ b/tests/unreal/init.py @@ -2,11 +2,10 @@ >>> python -m tests.unreal.init_test """ -from loguru import logger - from xrfeitoria.rpc import remote_unreal +from xrfeitoria.utils import setup_logger -from ..utils import __timer__, _init_unreal, setup_logger +from ..utils import __timer__, _init_unreal @remote_unreal() @@ -16,7 +15,7 @@ def test_unreal(): def init_test(debug: bool = False, dev: bool = False, background: bool = False): - setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') with __timer__('init unreal'): with _init_unreal(dev_plugin=dev, background=background) as xf_runner: test_unreal() diff --git a/tests/unreal/main.py b/tests/unreal/main.py index 001046b1..0f360d6e 100644 --- a/tests/unreal/main.py +++ b/tests/unreal/main.py @@ -3,7 +3,9 @@ """ from pathlib import Path -from ..utils import _init_unreal, setup_logger +from xrfeitoria.utils import setup_logger + +from ..utils import _init_unreal from .actor import actor_test from .camera import camera_test from .init import init_test diff --git a/tests/unreal/sequence.py b/tests/unreal/sequence.py index f70a9f97..fb2ac1ad 100644 --- a/tests/unreal/sequence.py +++ b/tests/unreal/sequence.py @@ -7,9 +7,10 @@ from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey from xrfeitoria.factory import XRFeitoriaUnreal +from xrfeitoria.utils import setup_logger from ..config import assets_path -from ..utils import __timer__, _init_unreal, setup_logger, visualize_vertices +from ..utils import __timer__, _init_unreal, visualize_vertices root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/tests/unreal/{file_name}' @@ -86,7 +87,7 @@ def new_seq(xf_runner: XRFeitoriaUnreal, level_path: str, seq_name: str): def sequence_test(debug: bool = False, background: bool = False): - logger = setup_logger(debug=debug) + logger = setup_logger(level='DEBUG' if debug else 'INFO') with _init_unreal(background=background) as xf_runner: xf_runner.Renderer.clear() From 9d965f752c45766b21b185ed57fd67b9718beb66 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 11:21:48 +0800 Subject: [PATCH 042/110] pre-commit --- xrfeitoria/utils/anim/motion.py | 6 ++---- xrfeitoria/utils/anim/utils.py | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index d4695afc..b1c87f2d 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -758,8 +758,7 @@ def _transform_transl_global_orient_( root_location_t0: Optional[np.ndarray] = None, pelvis_location_t0: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, np.ndarray]: - """ - Transform the global orientation and translation based on the given offsets. + """Transform the global orientation and translation based on the given offsets. Args: global_orient (np.ndarray): Global orientation array. @@ -797,8 +796,7 @@ def get_humandata( root_location_t0: Optional[np.ndarray] = None, pelvis_location_t0: Optional[np.ndarray] = None, ) -> Dict[str, Any]: - """ - Get human data for a given set of parameters. + """Get human data for a given set of parameters. Args: smpl_x_data (Dict[str, np.ndarray]): Dictionary containing the SMPL-X data. diff --git a/xrfeitoria/utils/anim/utils.py b/xrfeitoria/utils/anim/utils.py index 079f9fb3..ba92e3aa 100644 --- a/xrfeitoria/utils/anim/utils.py +++ b/xrfeitoria/utils/anim/utils.py @@ -53,8 +53,7 @@ def load_humandata_motion(input_humandata_path: PathLike) -> Union[SMPLMotion, S def dump_humandata(motion: SMPLXMotion, save_filepath: PathLike, meta_filepath: PathLike): - """ - Dump human data to a file. + """Dump human data to a file. Args: motion (SMPLXMotion): The SMPLXMotion object containing the motion data. @@ -93,9 +92,7 @@ def dump_humandata(motion: SMPLXMotion, save_filepath: PathLike, meta_filepath: if __name__ == '__main__': - """ - python -m xrfeitoria.utils.anim.utils - """ + """Python -m xrfeitoria.utils.anim.utils.""" motion = load_amass_motion('.cache/ACCAD/s001/EricCamper04_stageii.npz') motion_data = motion.get_motion_data() dump_humandata(motion, '.cache/SMPL-XL_test.npz', '.cache/SMPL-XL-001.npz') From 3e61b1319cc82bc264643b153f9ef0db0d06f19e Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 12:54:29 +0800 Subject: [PATCH 043/110] bug fix --- samples/blender/07_amass.py | 48 ++++++++++++++++++++------------- xrfeitoria/utils/anim/motion.py | 10 ++++--- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index 4929b9ac..f2d23442 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -17,10 +17,23 @@ from xrfeitoria.utils import setup_logger from xrfeitoria.utils.anim.utils import dump_humandata, load_amass_motion -root = Path('.cache/sample-amass') +# prepare the assets +#################### +root = Path('.cache/sample-amass') # modify this to your own path + +# 1. Download Amass from https://amass.is.tue.mpg.de/download.php +# For example, download ACCAD (SMPL-X N) from https://download.is.tue.mpg.de/download.php?domain=amass&sfile=amass_per_dataset/smplx/neutral/mosh_results/ACCAD.tar.bz2 +# and use `ACCAD/s001/EricCamper04_stageii.npz` from the uncompressed folder amass_file = root / 'EricCamper04_stageii.npz' + +# 2. Download SMPL-XL model from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.fbx +# or from https://openxlab.org.cn/datasets/OpenXDLab/SynBody/tree/main/Assets +# With downloading this, you are agreeing to CC BY-NC-SA 4.0 License (https://creativecommons.org/licenses/by-nc-sa/4.0/). +# Plus, download the meta information from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.npz smpl_xl_file = root / 'SMPL-XL-001.fbx' smpl_xl_meta_file = root / 'SMPL-XL-001.npz' + +# 3. Define the output file path saved_blend_file = root / 'output.blend' saved_humandata_file = root / 'output.npz' @@ -37,27 +50,23 @@ def apply_scale(actor_name: str): def main(background: bool = False): logger = setup_logger() - # Download Amass from https://amass.is.tue.mpg.de/download.php - # For example, download ACCAD (SMPL-X N), and use any motion file from the uncompressed folder - motion = load_amass_motion(amass_file) # modify this to motion file in absolute path + motion = load_amass_motion(amass_file) motion.convert_fps(30) # convert the motion from 120fps (amass) to 30fps motion_data = motion.get_motion_data() + # modify this to your blender executable path xf_runner = xf.init_blender( - exec_path='C:/Program Files/Blender Foundation/Blender 3.6/blender.exe', - background=background, - ) # modify this to your blender executable path - - # SMPL-XL model - # 1. Download SMPL-XL model from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.fbx - # or from https://openxlab.org.cn/datasets/OpenXDLab/SynBody/tree/main/Assets - # With downloading this, you are agreeing to CC BY-NC-SA 4.0 License (https://creativecommons.org/licenses/by-nc-sa/4.0/) - # 2. Import SMPL-XL model - actor = xf_runner.Actor.import_from_file(smpl_xl_file) # modify this to SMPL-XL model file in absolute path + exec_path='C:/Program Files/Blender Foundation/Blender 3.6/blender.exe', background=background + ) + + # Import SMPL-XL model + actor = xf_runner.Actor.import_from_file(smpl_xl_file) apply_scale(actor.name) # SMPL-XL model is imported with scale, we need to apply scale to it + # Apply motion data to the actor logger.info('Applying motion data') xf_runner.utils.apply_motion_data_to_actor(motion_data=motion_data, actor_name=actor.name) + # Save the motion data as annotation in humandata format defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md dump_humandata(motion, save_filepath=saved_humandata_file, meta_filepath=smpl_xl_meta_file) # Modify the frame range to the length of the motion @@ -68,17 +77,20 @@ def main(background: bool = False): xf_runner.utils.save_blend(saved_blend_file, pack=True) logger.info('🎉 [bold green]Success!') - input('Press Any Key to Exit...') + if not background: + input('You can check the result in the blender window. Press Any Key to Exit...') # Close the blender process xf_runner.close() + logger.info(f'You can use Blender to check the result in "{saved_blend_file.as_posix}"') + if __name__ == '__main__': from argparse import ArgumentParser - args = ArgumentParser() - args.add_argument('--background', '-b', action='store_true') - args.parse_args() + parser = ArgumentParser() + parser.add_argument('--background', '-b', action='store_true') + args = parser.parse_args() main(background=args.background) diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index b1c87f2d..25073ab1 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -407,7 +407,7 @@ def get_parent_bone_name(self, bone_name) -> Optional[str]: else: return self.BONE_NAMES[parent_idx] - def dump_motion_data( + def dump_humandata( self, filepath: PathLike, betas: np.ndarray, @@ -417,9 +417,11 @@ def dump_motion_data( root_location_t0: Optional[np.ndarray] = None, pelvis_location_t0: Optional[np.ndarray] = None, ) -> None: - """Dump the motion data to a motion data file at the given `filepath`. + """Dump the motion data to a humandata file at the given `filepath`. + + HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md - The motion data file is a npz file containing the following keys: + The humandata file is a npz file containing the following keys: ``` motion_data = { '__data_len__': n_frames, @@ -680,6 +682,8 @@ def dump_humandata( ) -> None: """Dump the motion data to a humandata file at the given `filepath`. + HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md + The humandata file is a npz file containing the following keys: ```python humandata = { From 8cc369f97ead6ddfaf41c21f7b6c3ff1184f484b Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 13:03:14 +0800 Subject: [PATCH 044/110] update docstring --- xrfeitoria/utils/tools.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index 37644201..55f3ad19 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -113,11 +113,12 @@ def setup_logger( level (Literal['TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'], optional): logging level. Defaults to 'INFO', find more in https://loguru.readthedocs.io/en/stable/api/logger.html. The order of the levels is: - 'RPC' (custom level): logging RPC messages which are sent by RPC protocols. - 'TRACE': logging engine output like console output of blender. - 'DEBUG': logging debug messages. - 'INFO': logging info messages. - ... + + - 'RPC' (custom level): logging RPC messages which are sent by RPC protocols. + - 'TRACE': logging engine output like console output of blender. + - 'DEBUG': logging debug messages. + - 'INFO': logging info messages. + - ... log_path (Path, optional): path to save the log file. Defaults to None. replace (bool, optional): replace the log file if exists. Defaults to True. """ From 92ac442999f9d3b3eeb7088c8079c8d5ea281548 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 13:04:45 +0800 Subject: [PATCH 045/110] update comments --- samples/blender/07_amass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index f2d23442..600f4c50 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -26,11 +26,11 @@ # and use `ACCAD/s001/EricCamper04_stageii.npz` from the uncompressed folder amass_file = root / 'EricCamper04_stageii.npz' -# 2. Download SMPL-XL model from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.fbx +# 2.1 Download SMPL-XL model from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.fbx # or from https://openxlab.org.cn/datasets/OpenXDLab/SynBody/tree/main/Assets # With downloading this, you are agreeing to CC BY-NC-SA 4.0 License (https://creativecommons.org/licenses/by-nc-sa/4.0/). -# Plus, download the meta information from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.npz smpl_xl_file = root / 'SMPL-XL-001.fbx' +# 2.2 Download the meta information from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.npz smpl_xl_meta_file = root / 'SMPL-XL-001.npz' # 3. Define the output file path From 4a97c8cafae1b6d374a668358c909693c6b935be Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 14:39:44 +0800 Subject: [PATCH 046/110] polish logger --- samples/utils.py | 4 ++-- src/XRFeitoriaBpy/utils_logger.py | 3 ++- tests/blender/init.py | 2 -- tests/blender/main.py | 5 +++-- tests/unreal/main.py | 5 +++-- xrfeitoria/rpc/factory.py | 2 +- xrfeitoria/utils/runner.py | 2 +- xrfeitoria/utils/tools.py | 3 ++- 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/samples/utils.py b/samples/utils.py index fab18615..aa5e66d0 100644 --- a/samples/utils.py +++ b/samples/utils.py @@ -87,7 +87,7 @@ def parse_args(): xf.init_blender, exec_path=blender_exec, new_process=False, - replace_plugin=True, + replace_plugin=False, dev_plugin=False, ) _init_unreal = partial( @@ -95,6 +95,6 @@ def parse_args(): exec_path=unreal_exec, project_path=unreal_project, new_process=False, - replace_plugin=True, + replace_plugin=False, dev_plugin=False, ) diff --git a/src/XRFeitoriaBpy/utils_logger.py b/src/XRFeitoriaBpy/utils_logger.py index 3025a34a..98668748 100644 --- a/src/XRFeitoriaBpy/utils_logger.py +++ b/src/XRFeitoriaBpy/utils_logger.py @@ -30,7 +30,8 @@ def setup_logger(level: str = 'INFO') -> 'logging.Logger': logger = logging.getLogger(__name__) logger.handlers.clear() logger.setLevel(level) - logger_format = '{asctime} | ' + '{levelname:^8} | ' + '[blender] {message}' + # logger_format = '{asctime} | ' + '{levelname:^8} | ' + '[blender] {message}' + logger_format = '{message}' formatter = logging.Formatter(logger_format, style='{', datefmt='%Y-%m-%d %H:%M:%S') console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) diff --git a/tests/blender/init.py b/tests/blender/init.py index 7623e696..ae905dc9 100644 --- a/tests/blender/init.py +++ b/tests/blender/init.py @@ -2,8 +2,6 @@ >>> python -m tests.blender.init """ -from loguru import logger - from xrfeitoria.rpc import remote_blender from xrfeitoria.utils import setup_logger diff --git a/tests/blender/main.py b/tests/blender/main.py index 046f69f6..7833683c 100644 --- a/tests/blender/main.py +++ b/tests/blender/main.py @@ -18,9 +18,10 @@ def main(debug: bool = False, background: bool = False): - logger = setup_logger(debug=debug, log_path=output_path / 'blender.log') + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=output_path / 'blender.log') + + init_test(debug=debug, background=background, dev=True) with _init_blender(background=background) as xf_runner: - init_test(debug=debug) actor_test(debug=debug) camera_test(debug=debug) sequence_test(debug=debug) diff --git a/tests/unreal/main.py b/tests/unreal/main.py index 0f360d6e..98a6ec75 100644 --- a/tests/unreal/main.py +++ b/tests/unreal/main.py @@ -15,9 +15,10 @@ def main(debug: bool = False, background: bool = False): - logger = setup_logger(debug=debug, log_path=root / 'unreal.log') + logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=root / 'unreal.log') + + init_test(debug=debug, background=background, dev=True) with _init_unreal(background=background) as xf_runner: - init_test(debug=debug, background=background) actor_test(debug=debug, background=background) camera_test(debug=debug, background=background) sequence_test(debug=debug, background=background) diff --git a/xrfeitoria/rpc/factory.py b/xrfeitoria/rpc/factory.py index 83cf2e4f..006ed52f 100644 --- a/xrfeitoria/rpc/factory.py +++ b/xrfeitoria/rpc/factory.py @@ -171,7 +171,7 @@ def _register(cls, function: Callable) -> List[str]: # if function registered, skip it if function.__name__ in cls.registered_function_names and not cls.reload_rpc_code: - logger.debug(f'Function "{function.__name__}" has already been registered with the server!') + logger.log('RPC', f'Function "{function.__name__}" has already been registered with the server!') return [] code = cls._get_code(function) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 7534aa33..0ce488e6 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -251,7 +251,7 @@ def _receive_stdout(self) -> None: if not data: break # log in debug level - logger.trace(f'\[engine] {data.strip()}') + logger.trace(f'(engine) {data.strip()}') self.engine_outputs.append(data) def check_engine_alive(self) -> bool: diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index 55f3ad19..74463883 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -96,7 +96,8 @@ def setup_logging( log_path.parent.mkdir(parents=True, exist_ok=True) if replace and log_path.exists(): log_path.unlink(missing_ok=True) - logger.add(log_path, level='DEBUG', filter=cls.filter_unique, format=cls.logger_format, encoding='utf-8') + _level = 'RPC' if level == 'RPC' else 'TRACE' + logger.add(log_path, level=_level, filter=cls.filter_unique, format=cls.logger_format, encoding='utf-8') logger.info(f'Python Logging to "{log_path.as_posix()}"') cls.is_setup = True return logger From e7b29c5994c9525f743faae3b6c241e6b72cc430 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 12 Dec 2023 17:48:41 +0800 Subject: [PATCH 047/110] add `motion_data` in `sequence.spawn_actor` in ue --- samples/blender/07_amass.py | 4 +- samples/unreal/07_amass.py | 85 +++++++++++++++++ .../Blueprints/SequenceAssetData.uasset | Bin 9740 -> 0 bytes .../Content/Python/sequence.py | 87 +++++++++++++++++- xrfeitoria/renderer/renderer_unreal.py | 2 +- xrfeitoria/sequence/sequence_unreal.py | 22 +++++ xrfeitoria/sequence/sequence_unreal.pyi | 2 + 7 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 samples/unreal/07_amass.py delete mode 100644 src/XRFeitoriaUnreal/Content/Blueprints/SequenceAssetData.uasset diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index 600f4c50..4d07c216 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -19,7 +19,7 @@ # prepare the assets #################### -root = Path('.cache/sample-amass') # modify this to your own path +root = Path('.cache/sample-amass').resolve() # modify this to your own path # 1. Download Amass from https://amass.is.tue.mpg.de/download.php # For example, download ACCAD (SMPL-X N) from https://download.is.tue.mpg.de/download.php?domain=amass&sfile=amass_per_dataset/smplx/neutral/mosh_results/ACCAD.tar.bz2 @@ -83,7 +83,7 @@ def main(background: bool = False): # Close the blender process xf_runner.close() - logger.info(f'You can use Blender to check the result in "{saved_blend_file.as_posix}"') + logger.info(f'You can use Blender to check the result in "{saved_blend_file.as_posix()}"') if __name__ == '__main__': diff --git a/samples/unreal/07_amass.py b/samples/unreal/07_amass.py new file mode 100644 index 00000000..01fd49fe --- /dev/null +++ b/samples/unreal/07_amass.py @@ -0,0 +1,85 @@ +""" +>>> python -m samples.unreal.07_amass + +This is a script to demonstrate importing Amass motion and applying it to SMPL-XL model. +Before running this script, please download `SMPL-XL model` and `Amass dataset` first, +you can find the download links in the comments in main function. + +SMPL-XL: a parametric human model based on SMPL-X in a layered representation, introduced in https://synbody.github.io/ +Amass: a large database of human motion, introduced in https://amass.is.tue.mpg.de/ +""" +from pathlib import Path + +import xrfeitoria as xf +from xrfeitoria.utils import setup_logger +from xrfeitoria.utils.anim.utils import dump_humandata, load_amass_motion + +from ..config import unreal_exec, unreal_project + +# prepare the assets +#################### +root = Path('.cache/sample-amass').resolve() # modify this to your own path + +# 1. Download Amass from https://amass.is.tue.mpg.de/download.php +# For example, download ACCAD (SMPL-X N) from https://download.is.tue.mpg.de/download.php?domain=amass&sfile=amass_per_dataset/smplx/neutral/mosh_results/ACCAD.tar.bz2 +# and use `ACCAD/s001/EricCamper04_stageii.npz` from the uncompressed folder +amass_file = root / 'EricCamper04_stageii.npz' + +# 2.1 Download SMPL-XL model from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.fbx +# or from https://openxlab.org.cn/datasets/OpenXDLab/SynBody/tree/main/Assets +# With downloading this, you are agreeing to CC BY-NC-SA 4.0 License (https://creativecommons.org/licenses/by-nc-sa/4.0/). +smpl_xl_file = root / 'SMPL-XL-001.fbx' +# 2.2 Download the meta information from https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/assets/SMPL-XL-001.npz +smpl_xl_meta_file = root / 'SMPL-XL-001.npz' + +# 3. Define the output file path +saved_humandata_file = root / 'output.npz' + + +def main(background: bool = False): + logger = setup_logger() + + motion = load_amass_motion(amass_file) + motion.convert_fps(30) # convert the motion from 120fps (amass) to 30fps + motion_data = motion.get_motion_data() + + xf_runner = xf.init_unreal(exec_path=unreal_exec, project_path=unreal_project, background=background) + + # Import SMPL-XL model + actor_path = xf_runner.utils.import_asset(smpl_xl_file) + + with xf_runner.Sequence.new( + seq_name='seq_amass', level='/Game/Levels/Playground', seq_length=200, replace=True + ) as seq: + seq.show() + + # Spawn the actor, and add motion data as FK animation + actor = seq.spawn_actor( + actor_asset_path=actor_path, + location=(0, 0, 0), + rotation=(0, 0, 0), + stencil_value=1, + motion_data=motion_data, + ) + + # Save the motion data as annotation in humandata format defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md + dump_humandata(motion, save_filepath=saved_humandata_file, meta_filepath=smpl_xl_meta_file) + + logger.info('🎉 [bold green]Success!') + if not background: + input('You can check the result in the unreal window. Press Any Key to Exit...') + + # Close the unreal process + xf_runner.close() + + logger.info(f'You can use Unreal to check the result in "{unreal_project.as_posix()}"') + + +if __name__ == '__main__': + from argparse import ArgumentParser + + parser = ArgumentParser() + parser.add_argument('--background', '-b', action='store_true') + args = parser.parse_args() + + main(background=args.background) diff --git a/src/XRFeitoriaUnreal/Content/Blueprints/SequenceAssetData.uasset b/src/XRFeitoriaUnreal/Content/Blueprints/SequenceAssetData.uasset deleted file mode 100644 index 91e2e03c7bc4191e190632bc132a78952e7746e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9740 zcmdrS3y@UBu~#_$$4^kyfPe@bpW{9p+<^h_c5jdSAKWMS0ep8md#vp3+uQe+a}n@B zLhzF^3NcAxw4_9dVilz%q(nt53}~vv#Ar;;NL55C7_Cx^_yfFj&zpYxc5m;aP*y4f z@6AkiPfvGGPfyR=JACJihrjIX?7XEPz>rG-KE;~KAbhSnymJ28B_{`bvITy-v317O z{s`MTZu~7y|p3mRAKe0kCv7_IsJDBUR;f^$#2y?^kVhY zJ>G$ncD%89zcveDCv)D}mtK9>)dw~n-@B{q{JbFuTWp>?-tO7wJ~8J0?Z(9MmtTvp zqLn+}@m9VtWzXKxTVj{{PGlhLN*MQoa}kt z`R)RDer~SYUF^+u=jXV+^NVx6-u%3|-u(GAM3yvsrws)dhV`|B0Y1Qn3)`d80j|I& z51%D>y?gHV=0TqO9{cAb_q<$C_5SR&AAivPOvT;1_da|gZ~sxECq5)d8j9h9>tDXt zH5z*}XVw?z&0HpeLNmndyv!Vl2YCAP&E4Q}kSxDB`fd|7eueYDLxyw|T;fi$*AEf) zt^@ZS?AI|ht6(^?DU3w4SWQF^i-;M6(tYm-Mj*7YFc{R^JSe^y&_mGq>F|H!w9r@~ znzg1_l@Pu2UqLoAgJ~%$`%KFQl(At<`ik1cuF^KOb zbMVZBL3OdPfMKH^IDyJTk*)`A$wv0yclTuktZC5DMH+*`BaEgWhs#UK!~C)ans45Nd`oqb9sg zZtkwG@~o=TLjv}Hk&{82i)JVnF-SiYy@@EO-=Lx2Lh--WKO!lFUyBA!*wm>nA_+WJ z-y$0Mhcmh4f1ZF54r;L~twq4PpPwqCB`EflM6~egnm|Yzv>mgr28tnQ0zGDMvOsF}4 z`W$e1PSHYmgSuwg20Hu1d*?}JZzLc>zMv&vzgJ!xO7fM6kcgn=eD0uT7_fEF@;b7B z6k4wRuFZd3OPfP>+%;dXCQVC2arV34oL4pmVHKKT7NcBowG0?G;gUDVe3e>Qs?z-C z1~-kTK09mPj!m0LXkKdR%CO8o`mZENC0adDLCWoEUfe-yka_LFn?D;=CV`@j9m;^m z9{==N5?QYCnSQUNlcLI?JZoXcvmcH}aE;cqMr#%odXp9mtkcMFh-$664gq+IszuOUW7Ss0d>Ua?@p`;=?Uz zA7$(e&mMpyET1vkZoB_V?5U`+YE?}n(4s|R6ldgF2uDso;UjmnDR`OeqjHrS?x`MA zNt&ye0{!*sgNpnzM@rEAKk`g7hzGbo3nZlJc=zwvnw)hKrKb@k zOO~pWgyyr6Yoc1DNQ+cW36oc~d!qXS>6Y}7uCxzypCiBP`$$*VN4nxZ(pB_8SEg`O zQG!7wm0g^4Y}4N87F+BR_%V!SBD=U#(D59V3PgUPJTHL$ame z!-e7hNz#+?$u-d@#^%)L-*^m8Jw@#9!c+#1drJ8m&6S$_6^KbZG&19d1to73OJyuC ziee5XDDor3y0=teX0TRRN5w*fF(b#xlqs>(7-OAFnaM$T8c;%EPb`l|38|^Op{5vf z-bjlU1QN%li6=Vx4H@Bt1He(X}6LY5LcJS9_|*$hQTf_h7p}PPIzlR?tB}8NS`n0&Bq! ztr*M*8iguo#JUkyBhp5~jBCAVp=O#s=Eh zhqVj+){^*Q=}-erum)F-xO>pLjp&c$k*q^^c%GlFoLYw|O4=-_!x?CAwBkHjI1|~* z4-MetKL@G8(2NvLXxyWQTqNCD_A=D82<0$PYg)JN;6pFcylhjNw-&W;#)qt6?VY&I zyTfw~>DlJPm9)ijhDP)p+A}{w9jzD@pQmR&@z@YX2`O4I=bp#$Dp3S%O^65_HQ3JI zb^ozs4d}a8bnOZm*=5aG`U>B}uY@^j#oZq_G7fooS54gRQs%HXvjJW=-_!*V?)S^_*EIfAYM*{Fsu;O5GO?3i+9K~m@(O`anaf`E z{JMusH{l}F%^iwN)QLA4zFAKvL9tA=Dooi_p-&Bby`>L)RBR+A^88a4bE2G^3UiWE z&;^X|^i0^H*yKrad=WP$V&LP7h`BPYT`y3rYgBC$(tV#wfmNZ?TZJyNRDNJpUEewL zZ`d=iBCT>qX6=9x>tGRi4;5xmY<1;bQ#BK-xvI+_8O|hs31CFxu*4o+$f@L7m2@K6 zq}AnJG*S8(xdN4WRt1@=mNy$b1OUndMvE}jCw8)s`b;Y+xSgSf<*VAZB&E?*W+_A$ zQ?ouZ6#;$Dpu!yJ-YpBsLS>Rw+1|&d^mc9TC*z*0``_>vHnlVt{#^C{;wtTGnQX5h-(-JcV7Vgd)>UW| zQR(8!lPx%2Ypue*WOGVGIQ}*u>~F|@@;VB)RIH6;eKn&#&o2=inI;eh3lqO#b?pEo z5lqA??^Hx|1g9)x6_f|??#M|=t$?$W2C<0O^)v`0(MuM7%u5`L=yMPio)w$zi+NAd zPfJwz$gqj%MCd0`Adi04R27r4Ml4*nk-cBW@L^MOcb6P&eCFDX>jvG)^Ao=ris?jE za}?wi;z=Z$XiCA!+hz!gVk(+l37S1^)BLNkA8}u@%MXlb0`onn?gnrwk!v0Ndr*(+;90FqL z;u1K%lT1TJ{hDHR None: + """Add FK motion to the given actor binding. + + Args: + binding (unreal.SequencerBindingProxy): The binding of actor in sequence to add FK motion to. + motion_data (List[Dict[str, Dict[str, List[float]]]]): The FK motion data to add. + """ + rig_track: unreal.MovieSceneControlRigParameterTrack = ( + unreal.ControlRigSequencerLibrary.find_or_create_control_rig_track( + world=get_world(), + level_sequence=binding.sequence, + control_rig_class=unreal.FKControlRig, + binding=binding, + ) + ) + rig_section: unreal.MovieSceneControlRigParameterSection = rig_track.get_section_to_key() + param_names = list(rig_section.get_parameter_names()) + bone_names = list(motion_data[0].keys()) + assert all([f'{bone}_CONTROL' in param_names for bone in bone_names]), RuntimeError( + f'Not All bone names (from json): {bone_names} not in param names (from seq FK): {param_names}' + ) + + def get_transform_from_bone_data(bone_data): + quat: Tuple[float, float, float, float] = bone_data.get('rotation') + location: Tuple[float, float, float] = bone_data.get('location', (0, 0, 0)) # default location is (0, 0, 0) + + # HACK: convert space + location = [location[0] * 100, -location[1] * 100, location[2] * 100] # cm -> m, y -> -y + quat = (-quat[1], quat[2], -quat[3], quat[0]) # (w, x, y, z) -> (-x, y, -z, w) + + transform = unreal.Transform(location=location, rotation=unreal.Quat(*quat).rotator()) + return transform + + for frame, motion_frame in enumerate(motion_data): + for bone_name, bone_data in motion_frame.items(): + # TODO: set key type to STATIC + rig_section.add_transform_parameter_key( + parameter_name=f'{bone_name}_CONTROL', + time=get_time(binding.sequence, frame), + value=get_transform_from_bone_data(bone_data), + ) + + def get_spawnable_actor_from_binding( sequence: unreal.MovieSceneSequence, binding: unreal.SequencerBindingProxy, @@ -605,15 +651,21 @@ def add_actor_to_sequence( actor_transform_keys: Optional[Union[SequenceTransformKey, List[SequenceTransformKey]]] = None, actor_stencil_value: int = 1, animation_asset: Optional[unreal.AnimSequence] = None, + motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, seq_fps: Optional[float] = None, seq_length: Optional[int] = None, ) -> Dict[str, Any]: + assert not ( + animation_asset is not None and motion_data is not None + ), 'Cannot provide both animation_asset and motion_data' # get sequence settings if seq_fps is None: seq_fps = get_sequence_fps(sequence) if seq_length is None: if animation_asset: seq_length = get_animation_length(animation_asset, seq_fps) + if motion_data: + seq_length = len(motion_data) else: seq_length = sequence.get_playback_end() @@ -636,6 +688,10 @@ def add_actor_to_sequence( if animation_asset: add_animation_to_binding(actor_binding, animation_asset, seq_length, seq_fps) + # add motion data (FK / ControlRig) + if motion_data: + add_fk_motion_to_binding(actor_binding, motion_data) + # ------- add transform track ------- # transform_track, transform_section = add_or_find_transform_track_to_binding(actor_binding) if actor_transform_keys: @@ -659,17 +715,23 @@ def add_spawnable_actor_to_sequence( actor_name: str, actor_asset: Union[unreal.SkeletalMesh, unreal.StaticMesh], animation_asset: Optional[unreal.AnimSequence] = None, + motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, actor_transform_keys: Optional[Union[SequenceTransformKey, List[SequenceTransformKey]]] = None, actor_stencil_value: int = 1, seq_fps: Optional[float] = None, seq_length: Optional[int] = None, ) -> Dict[str, Any]: + assert not ( + animation_asset is not None and motion_data is not None + ), 'Cannot provide both animation_asset and motion_data' # get sequence settings if seq_fps is None: seq_fps = get_sequence_fps(sequence) if seq_length is None: if animation_asset: seq_length = get_animation_length(animation_asset, seq_fps) + if motion_data: + seq_length = len(motion_data) else: seq_length = sequence.get_playback_end() @@ -695,6 +757,10 @@ def add_spawnable_actor_to_sequence( if animation_asset: add_animation_to_binding(actor_binding, animation_asset, seq_length, seq_fps) + # add motion data (FK / ControlRig) + if motion_data: + add_fk_motion_to_binding(actor_binding, motion_data) + # ------- add transform track ------- # transform_track, transform_section = add_or_find_transform_track_to_binding(actor_binding) if actor_transform_keys: @@ -935,17 +1001,26 @@ def add_actor( transform_keys: 'Optional[TransformKeys]' = None, stencil_value: int = 1, animation_asset: 'Optional[Union[str, unreal.AnimSequence]]' = None, + motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, ) -> None: """Spawn an actor in sequence. Args: - actor (Union[str, unreal.Actor]): actor path (e.g. '/Game/Cube') / loaded asset (via `unreal.load_asset('/Game/Cube')`) - animation_asset (Union[str, unreal.AnimSequence]): animation path (e.g. '/Game/Anim') / loaded asset (via `unreal.load_asset('/Game/Anim')`). Can be None which means no animation. - actor_name (str, optional): Name of actor to set in sequence. Defaults to "Actor". - transform_keys (TransformKeys, optional): List of transform keys. Defaults to None. - actor_stencil_value (int, optional): Stencil value of actor, used for specifying the mask color for this actor (mask id). Defaults to 1. + actor_name (str): The name of the actor. + actor (Optional[Union[str, unreal.Actor]]): actor path (e.g. '/Game/Cube') / loaded asset (via `unreal.load_asset('/Game/Cube')`) + transform_keys (Optional[TransformKeys]): List of transform keys. Defaults to None. + stencil_value (int): Stencil value of actor, used for specifying the mask color for this actor (mask id). Defaults to 1. + animation_asset (Optional[Union[str, unreal.AnimSequence]]): animation path (e.g. '/Game/Anim') / loaded asset (via `unreal.load_asset('/Game/Anim')`). Can be None which means no animation. + motion_data (Optional[List[Dict[str, Dict[str, List[float]]]]]): The motion data used for FK animation. + + Raises: + AssertionError: If `cls.sequence` is not initialized. + AssertionError: If `animation_asset` and `motion_data` are both provided. Only one can be provided. """ assert cls.sequence is not None, 'Sequence not initialized' + assert not ( + animation_asset is not None and motion_data is not None + ), 'Cannot provide both animation_asset and motion_data' if animation_asset and isinstance(animation_asset, str): animation_asset = unreal.load_asset(animation_asset) if isinstance(actor, str): @@ -957,6 +1032,7 @@ def add_actor( actor_name=actor_name, actor_asset=actor, animation_asset=animation_asset, + motion_data=motion_data, actor_transform_keys=transform_keys, actor_stencil_value=stencil_value, ) @@ -970,6 +1046,7 @@ def add_actor( actor_transform_keys=transform_keys, actor_stencil_value=stencil_value, animation_asset=animation_asset, + motion_data=motion_data, ) cls.bindings[actor_name] = bindings diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 00f0e627..273dd4c1 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -145,7 +145,7 @@ def render_jobs(cls) -> None: break if 'Render completed. Success: True' in data: break - logger.info(f'\[unreal] {data}') + logger.info(f'(engine) {data}') except BlockingIOError: pass except ConnectionResetError: diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 81b0a183..49d0cc14 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -107,6 +107,7 @@ def spawn_actor( actor_name: Optional[str] = None, stencil_value: int = 1, anim_asset_path: 'Optional[str]' = None, + motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, ) -> ActorUnreal: """Spawns an actor in the Unreal Engine at the specified location, rotation, and scale. @@ -121,10 +122,18 @@ def spawn_actor( stencil_value (int in [0, 255], optional): The stencil value to use for the spawned actor. Defaults to 1. Ref to :ref:`FAQ-stencil-value` for details. anim_asset_path (Optional[str], optional): The engine path to the animation asset of the actor. Defaults to None. + motion_data (Optional[List[Dict[str, Dict[str, List[float]]]]]): The motion data used for FK animation. Returns: ActorUnreal: The spawned actor object. + + Raises: + AssertionError: If both `anim_asset_path` and `motion_data` are provided. Only one of them can be provided. """ + assert not ( + anim_asset_path is not None and motion_data is not None + ), 'Cannot provide both `anim_asset_path` and `motion_data`' + transform_keys = SeqTransKey( frame=0, location=location, rotation=rotation, scale=scale, interpolation='CONSTANT' ) @@ -134,6 +143,7 @@ def spawn_actor( actor_asset_path=actor_asset_path, transform_keys=transform_keys.model_dump(), anim_asset_path=anim_asset_path, + motion_data=motion_data, actor_name=actor_name, stencil_value=stencil_value, ) @@ -148,6 +158,7 @@ def spawn_actor_with_keys( actor_name: Optional[str] = None, stencil_value: int = 1, anim_asset_path: 'Optional[str]' = None, + motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, ) -> ActorUnreal: """Spawns an actor in the Unreal Engine with the given asset path, transform keys, actor name, stencil value, and animation asset path. @@ -159,10 +170,18 @@ def spawn_actor_with_keys( stencil_value (int in [0, 255], optional): The stencil value to use for the spawned actor. Defaults to 1. Ref to :ref:`FAQ-stencil-value` for details. anim_asset_path (Optional[str], optional): The engine path to the animation asset of the actor. Defaults to None. + motion_data (Optional[List[Dict[str, Dict[str, List[float]]]]]): The motion data used for FK animation. Returns: ActorUnreal: The spawned actor. + + Raises: + AssertionError: If both `anim_asset_path` and `motion_data` are provided. Only one of them can be provided. """ + assert not ( + anim_asset_path is not None and motion_data is not None + ), 'Cannot provide both `anim_asset_path` and `motion_data`' + if not isinstance(transform_keys, list): transform_keys = [transform_keys] transform_keys = [i.model_dump() for i in transform_keys] @@ -174,6 +193,7 @@ def spawn_actor_with_keys( actor_asset_path=actor_asset_path, transform_keys=transform_keys, anim_asset_path=anim_asset_path, + motion_data=motion_data, actor_name=actor_name, stencil_value=stencil_value, ) @@ -375,6 +395,7 @@ def _spawn_actor_in_engine( actor_asset_path: str, transform_keys: 'Union[List[Dict], Dict]', anim_asset_path: 'Optional[str]' = None, + motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, actor_name: str = 'Actor', stencil_value: int = 1, ) -> None: @@ -391,6 +412,7 @@ def _spawn_actor_in_engine( XRFeitoriaUnrealFactory.Sequence.add_actor( actor=actor_asset_path, animation_asset=anim_asset_path, + motion_data=motion_data, actor_name=actor_name, transform_keys=transform_keys, stencil_value=stencil_value, diff --git a/xrfeitoria/sequence/sequence_unreal.pyi b/xrfeitoria/sequence/sequence_unreal.pyi index 938ac92b..1f6ac3a4 100644 --- a/xrfeitoria/sequence/sequence_unreal.pyi +++ b/xrfeitoria/sequence/sequence_unreal.pyi @@ -50,6 +50,7 @@ class SequenceUnreal(SequenceBase): actor_name: Optional[str] = ..., stencil_value: int = ..., anim_asset_path: Optional[str] = ..., + motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, ) -> ActorUnreal: ... @classmethod def spawn_actor_with_keys( @@ -59,6 +60,7 @@ class SequenceUnreal(SequenceBase): actor_name: Optional[str] = ..., stencil_value: int = ..., anim_asset_path: Optional[str] = ..., + motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, ) -> ActorUnreal: ... @classmethod def use_camera( From f477bf2de56a43cdca49dcc039724c275b7d7abb Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 18 Dec 2023 16:15:34 +0800 Subject: [PATCH 048/110] add render --- samples/blender/07_amass.py | 53 ++++++++++++++++++++++++++----------- samples/unreal/07_amass.py | 30 +++++++++++++++++---- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index 4d07c216..4881b961 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -13,6 +13,7 @@ from pathlib import Path import xrfeitoria as xf +from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.rpc import remote_blender from xrfeitoria.utils import setup_logger from xrfeitoria.utils.anim.utils import dump_humandata, load_amass_motion @@ -34,8 +35,11 @@ smpl_xl_meta_file = root / 'SMPL-XL-001.npz' # 3. Define the output file path -saved_blend_file = root / 'output.blend' -saved_humandata_file = root / 'output.npz' +seq_name = 'seq_amass' +output_path = Path(__file__).parents[2].resolve() / 'output/samples/blender' / Path(__file__).stem +output_path.mkdir(parents=True, exist_ok=True) +saved_humandata_file = output_path / 'output.npz' +saved_blend_file = output_path / 'output.blend' @remote_blender() @@ -59,24 +63,43 @@ def main(background: bool = False): exec_path='C:/Program Files/Blender Foundation/Blender 3.6/blender.exe', background=background ) - # Import SMPL-XL model - actor = xf_runner.Actor.import_from_file(smpl_xl_file) - apply_scale(actor.name) # SMPL-XL model is imported with scale, we need to apply scale to it - - # Apply motion data to the actor - logger.info('Applying motion data') - xf_runner.utils.apply_motion_data_to_actor(motion_data=motion_data, actor_name=actor.name) - # Save the motion data as annotation in humandata format defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md - dump_humandata(motion, save_filepath=saved_humandata_file, meta_filepath=smpl_xl_meta_file) - - # Modify the frame range to the length of the motion - frame_start, frame_end = xf_runner.utils.get_keys_range() - xf_runner.utils.set_frame_range(frame_start, frame_end) + with xf_runner.Sequence.new(seq_name=seq_name, seq_length=motion.n_frames) as seq: + # Import SMPL-XL model + actor = xf_runner.Actor.import_from_file(smpl_xl_file) + apply_scale(actor.name) # SMPL-XL model is imported with scale, we need to apply scale to it + + # Apply motion data to the actor + logger.info('Applying motion data') + xf_runner.utils.apply_motion_data_to_actor(motion_data=motion_data, actor_name=actor.name) + # Save the motion data as annotation in humandata format defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md + dump_humandata(motion, save_filepath=saved_humandata_file, meta_filepath=smpl_xl_meta_file) + + # Modify the frame range to the length of the motion + frame_start, frame_end = xf_runner.utils.get_keys_range() + xf_runner.utils.set_frame_range(frame_start, frame_end) + # env + xf_runner.utils.set_env_color(color=(1, 1, 1, 1)) + + # + camera = xf_runner.Camera.spawn(location=(0, -2.5, 0.6), rotation=(90, 0, 0)) + + seq.add_to_renderer( + output_path=output_path, + resolution=(1920, 1080), + render_passes=[RenderPass('img', 'png')], + render_engine='eevee', + ) # Save the blend file xf_runner.utils.save_blend(saved_blend_file, pack=True) + # render + xf_runner.render() + logger.info('🎉 [bold green]Success!') + output_img = output_path / seq_name / 'img' / camera.name / '0000.png' + if output_img.exists(): + logger.info(f'Check the output in "{output_img.as_posix()}"') if not background: input('You can check the result in the blender window. Press Any Key to Exit...') diff --git a/samples/unreal/07_amass.py b/samples/unreal/07_amass.py index 01fd49fe..0f77e800 100644 --- a/samples/unreal/07_amass.py +++ b/samples/unreal/07_amass.py @@ -11,6 +11,7 @@ from pathlib import Path import xrfeitoria as xf +from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.utils import setup_logger from xrfeitoria.utils.anim.utils import dump_humandata, load_amass_motion @@ -33,7 +34,10 @@ smpl_xl_meta_file = root / 'SMPL-XL-001.npz' # 3. Define the output file path -saved_humandata_file = root / 'output.npz' +seq_name = 'seq_amass' +output_path = Path(__file__).parents[2].resolve() / 'output/samples/unreal' / Path(__file__).stem +output_path.mkdir(parents=True, exist_ok=True) +saved_humandata_file = output_path / 'output.npz' def main(background: bool = False): @@ -48,9 +52,7 @@ def main(background: bool = False): # Import SMPL-XL model actor_path = xf_runner.utils.import_asset(smpl_xl_file) - with xf_runner.Sequence.new( - seq_name='seq_amass', level='/Game/Levels/Playground', seq_length=200, replace=True - ) as seq: + with xf_runner.Sequence.new(seq_name=seq_name, level='/Game/Levels/Playground', seq_length=motion.n_frames) as seq: seq.show() # Spawn the actor, and add motion data as FK animation @@ -62,17 +64,35 @@ def main(background: bool = False): motion_data=motion_data, ) + camera = seq.spawn_camera( + location=(0, 2.5, 0.6), + rotation=(0, 0, -90), + ) + + # Add render job to renderer + seq.add_to_renderer( + output_path=output_path, + resolution=(1920, 1080), + render_passes=[RenderPass('img', 'png')], + ) + # Save the motion data as annotation in humandata format defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md dump_humandata(motion, save_filepath=saved_humandata_file, meta_filepath=smpl_xl_meta_file) + # render + xf_runner.render() + logger.info('🎉 [bold green]Success!') + output_img = output_path / seq_name / 'img' / camera.name / '0000.png' + if output_img.exists(): + logger.info(f'Check the output in "{output_img.as_posix()}"') if not background: input('You can check the result in the unreal window. Press Any Key to Exit...') # Close the unreal process xf_runner.close() - logger.info(f'You can use Unreal to check the result in "{unreal_project.as_posix()}"') + logger.info(f'You can use Unreal to check the result in "{Path(unreal_project).as_posix()}"') if __name__ == '__main__': From 9a22906569846038971572899304ad73103e0518 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 18 Dec 2023 16:18:34 +0800 Subject: [PATCH 049/110] fix --- .github/CODE_OF_CONDUCT.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 2056f7aa..923f25f7 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -74,6 +74,3 @@ For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq [homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq From c4ce96b7524fb64ad349dfac3dbb33c8dde031da Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 21 Dec 2023 16:58:58 +0800 Subject: [PATCH 050/110] add debug log for rendering progress --- xrfeitoria/renderer/renderer_blender.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xrfeitoria/renderer/renderer_blender.py b/xrfeitoria/renderer/renderer_blender.py index 2844c00b..0e371cbf 100644 --- a/xrfeitoria/renderer/renderer_blender.py +++ b/xrfeitoria/renderer/renderer_blender.py @@ -93,6 +93,7 @@ def receive_stdout( break text = f'[bold green]:rocket: Rendering Job{job_info}: frame {frame_count}/{frame_length}[/bold green]' spinner.update(text=text) + logger.debug(f'(XF-Rendering) Job{job_info}: frame {frame_count}/{frame_length}') # reset first_trigger = second_trigger = False else: @@ -114,6 +115,7 @@ def receive_stdout( break text = f'[bold green]:rocket: Rendering Job{job_info}: frame {frame_count}/{frame_length}[/bold green]' spinner.update(text=text) + logger.debug(f'(XF-Rendering) Job{job_info}: frame {frame_count}/{frame_length}') # reset first_trigger = second_trigger = False From 6b83fb7b00343e48bceb6929b6da650148f4fefc Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 22 Dec 2023 15:09:39 +0800 Subject: [PATCH 051/110] add `seq_dir` for `Sequence.open()` in unreal --- src/XRFeitoriaUnreal/Content/Python/utils.py | 7 ++- xrfeitoria/sequence/sequence_unreal.py | 13 +++++ xrfeitoria/sequence/sequence_unreal.pyi | 2 + xrfeitoria/sequence/sequence_wrapper.py | 55 +++++-------------- xrfeitoria/sequence/sequence_wrapper.pyi | 2 +- .../utils/functions/unreal_functions.py | 24 ++++++++ xrfeitoria/utils/runner.py | 2 +- 7 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/utils.py b/src/XRFeitoriaUnreal/Content/Python/utils.py index a6f811cc..e1f3b575 100644 --- a/src/XRFeitoriaUnreal/Content/Python/utils.py +++ b/src/XRFeitoriaUnreal/Content/Python/utils.py @@ -88,10 +88,10 @@ def import_asset(path: Union[str, List[str]], dst_dir_in_engine: Optional[str] = asset_paths = [] for path in paths: + assert Path(path).exists(), f'File does not exist: {path}' name = Path(path).stem - dst_dir = unreal.Paths.combine([dst_dir_in_engine, Path(path).stem]) - # check if asset exists - dst_path = unreal.Paths.combine([dst_dir, name]) + dst_dir = unreal.Paths.combine([dst_dir_in_engine, name]) + dst_path = unreal.Paths.combine([dst_dir, name]) # check if asset exists if unreal.EditorAssetLibrary.does_asset_exist(dst_path): asset_paths.append(dst_path) continue @@ -136,6 +136,7 @@ def import_anim(path: str, skeleton_path: str, dest_path: Optional[str] = None) Returns: str: a path to the imported animation, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL" """ + assert Path(path).exists(), f'File does not exist: {path}' # init task import_task = unreal.AssetImportTask() import_task.set_editor_property('filename', path) diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 49d0cc14..6b1b3c63 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -221,6 +221,19 @@ def get_seq_path(cls) -> str: """ return cls._get_seq_path_in_engine() + @classmethod + def _open(cls, seq_name: str, seq_dir: 'Optional[str]' = None) -> None: + """Open an exist sequence. + + Args: + seq_name (str): Name of the sequence. + seq_dir (Optional[str], optional): Path of the sequence. + Defaults to None and fallback to the default path '/Game/XRFeitoriaUnreal/Sequences'. + """ + cls._open_seq_in_engine(seq_name=seq_name, seq_dir=seq_dir) + cls.name = seq_name + logger.info(f'>>>> [cyan]Opened[/cyan] sequence "{cls.name}" >>>>') + ##################################### ###### RPC METHODS (Private) ######## ##################################### diff --git a/xrfeitoria/sequence/sequence_unreal.pyi b/xrfeitoria/sequence/sequence_unreal.pyi index 1f6ac3a4..51715693 100644 --- a/xrfeitoria/sequence/sequence_unreal.pyi +++ b/xrfeitoria/sequence/sequence_unreal.pyi @@ -92,3 +92,5 @@ class SequenceUnreal(SequenceBase): def get_map_path(cls) -> str: ... @classmethod def get_seq_path(cls) -> str: ... + @classmethod + def _open(cls, seq_name: str, seq_dir: 'Optional[str]' = ...) -> None: ... diff --git a/xrfeitoria/sequence/sequence_wrapper.py b/xrfeitoria/sequence/sequence_wrapper.py index 83a3141c..12826f1d 100644 --- a/xrfeitoria/sequence/sequence_wrapper.py +++ b/xrfeitoria/sequence/sequence_wrapper.py @@ -11,6 +11,7 @@ class SequenceWrapperBase: """Sequence utils class.""" _seq = SequenceBase + default_level = None @classmethod @contextmanager @@ -21,7 +22,7 @@ def new( seq_fps: int = 30, seq_length: int = 1, replace: bool = False, - ) -> ContextManager[SequenceUnreal]: + ) -> ContextManager[SequenceBase]: """Create a new sequence and close the sequence after exiting the it. Args: @@ -33,6 +34,8 @@ def new( Yields: SequenceBase: Sequence object. """ + if level is None: + level = cls.default_level cls._seq._new( seq_name=seq_name, level=level, @@ -65,63 +68,33 @@ class SequenceWrapperBlender(SequenceWrapperBase): """Sequence utils class for Blender.""" _seq = SequenceBlender + default_level = default_level_blender - @classmethod - @contextmanager - def new( - cls, - seq_name: str, - level: Optional[str] = None, - seq_fps: int = 30, - seq_length: int = 1, - replace: bool = False, - ) -> ContextManager[SequenceBlender]: - """Create a new sequence and close the sequence after exiting the it. - Args: - seq_name (str): Name of the sequence. - level (Optional[str], optional): Name of the level. Defaults to None. If None, use the default level named 'XRFeitoria'. - seq_fps (int, optional): Frame per second of the new sequence. Defaults to 30. - seq_length (int, optional): Frame length of the new sequence. Defaults to 1. - replace (bool, optional): Replace the exist same-name sequence. Defaults to False. - Yields: - SequenceBase: Sequence object. - """ - if level is None: - level = default_level_blender - cls._seq._new( - seq_name=seq_name, - level=level, - seq_fps=seq_fps, - seq_length=seq_length, - replace=replace, - ) - yield cls._seq - cls._seq.save() - cls._seq.close() +class SequenceWrapperUnreal(SequenceWrapperBase): + """Sequence utils class for Unreal.""" + + _seq = SequenceUnreal + default_level = None @classmethod @contextmanager - def open(cls, seq_name: str) -> ContextManager[SequenceBase]: + def open(cls, seq_name: str, seq_dir: 'Optional[str]' = None) -> ContextManager[SequenceUnreal]: """Open a sequence and close the sequence after existing it. Args: seq_name (str): Name of the sequence. + seq_dir (Optional[str], optional): Path of the sequence. + Defaults to None and fallback to the default path '/Game/XRFeitoriaUnreal/Sequences'. Yields: SequenceBase: Sequence object. """ - cls._seq._open(seq_name=seq_name) + cls._seq._open(seq_name=seq_name, seq_dir=seq_dir) yield cls._seq cls._seq.save() cls._seq.close() - -class SequenceWrapperUnreal(SequenceWrapperBase): - """Sequence utils class for Unreal.""" - - _seq = SequenceUnreal - @classmethod @contextmanager def new( diff --git a/xrfeitoria/sequence/sequence_wrapper.pyi b/xrfeitoria/sequence/sequence_wrapper.pyi index c3b1c1a9..21207023 100644 --- a/xrfeitoria/sequence/sequence_wrapper.pyi +++ b/xrfeitoria/sequence/sequence_wrapper.pyi @@ -52,4 +52,4 @@ class SequenceWrapperUnreal(SequenceWrapperBase): seq_dir: Optional[str] = ..., ) -> ContextManager[SequenceUnreal]: ... @classmethod - def open(cls, seq_name: str) -> ContextManager[SequenceUnreal]: ... + def open(cls, seq_name: str, seq_dir: Optional[str] = ...) -> ContextManager[SequenceUnreal]: ... diff --git a/xrfeitoria/utils/functions/unreal_functions.py b/xrfeitoria/utils/functions/unreal_functions.py index 5b187c65..6433ec36 100644 --- a/xrfeitoria/utils/functions/unreal_functions.py +++ b/xrfeitoria/utils/functions/unreal_functions.py @@ -120,6 +120,30 @@ def duplicate_asset(src_path: str, dst_path: str, replace: bool = False) -> None unreal.EditorAssetLibrary.save_asset(dst_path) +@remote_unreal() +def new_seq_data(asset_path: str, sequence_path: str, map_path: str) -> None: + """Create a new data asset of sequence data. + + Args: + asset_path (str): path of the data asset. + sequence_path (str): path of the sequence asset. + map_path (str): path of the map asset. + + Returns: + unreal.DataAsset: the created data asset. + + Notes: + SequenceData Properties: + - "SequencePath": str + - "MapPath": str + """ + XRFeitoriaUnrealFactory.Sequence.new_data_asset( + asset_path=asset_path, + sequence_path=sequence_path, + map_path=map_path, + ) + + @remote_unreal() def delete_asset(asset_path: str) -> None: """Delete asset. diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 0ce488e6..e058308a 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -598,7 +598,7 @@ def get_src_plugin_path(self) -> Path: if not src_plugin_path.exists(): raise FileNotFoundError( f'Plugin source code not found in {src_plugin_path}, ' - 'please set `dev_plugin=False` to download the pre-built plugin. ' + 'please set `dev_plugin=False` to download the pre-built plugin. \n' 'Or clone the source code and build the plugin from source. ' 'https://github.com/openxrlab/xrfeitoria.git' ) From 58324b474aec2c8db7d11f8e89ce7ce8fdb87f51 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 25 Dec 2023 21:11:02 +0800 Subject: [PATCH 052/110] Add set_playback method to Sequence class --- .../Content/Python/sequence.py | 18 ++++++++++++++++++ xrfeitoria/sequence/sequence_unreal.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 00cdb11b..496bc2ad 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -957,6 +957,24 @@ def get_data_asset_info(data_asset_path: str) -> Tuple[str, str]: map_path = seq_data_asset.get_editor_property('MapPath').export_text() return seq_path.split('.')[0], map_path.split('.')[0] + @classmethod + def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: + """ + Set the playback range for the sequence. + + Args: + start_frame (Optional[int]): The start frame of the playback range. Defaults to None. + end_frame (Optional[int]): The end frame of the playback range. Defaults to None. + + Raises: + AssertionError: If the sequence is not initialized. + """ + assert cls.sequence is not None, 'Sequence not initialized' + if start_frame: + cls.sequence.set_playback_start(start_frame=start_frame) + if end_frame: + cls.sequence.set_playback_end(end_frame=end_frame) + # ------ add actor and camera -------- # @classmethod diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 6b1b3c63..955c5fd7 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -221,6 +221,20 @@ def get_seq_path(cls) -> str: """ return cls._get_seq_path_in_engine() + @classmethod + def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: + """ + Sets the playback range for the sequence. + + Args: + start_frame (Optional[int]): The start frame of the playback range. If not provided, the default start frame will be used. + end_frame (Optional[int]): The end frame of the playback range. If not provided, the default end frame will be used. + + Returns: + None + """ + cls._set_playback_in_engine(start_frame=start_frame, end_frame=end_frame) + @classmethod def _open(cls, seq_name: str, seq_dir: 'Optional[str]' = None) -> None: """Open an exist sequence. @@ -321,6 +335,10 @@ def _close_seq_in_engine() -> None: def _show_seq_in_engine() -> None: XRFeitoriaUnrealFactory.Sequence.show() + @staticmethod + def _set_playback_in_engine(start_frame: 'Optional[int]' = None, end_frame: 'Optional[int]' = None) -> None: + XRFeitoriaUnrealFactory.Sequence.set_playback(start_frame=start_frame, end_frame=end_frame) + # ------ add actor and camera -------- # @staticmethod From 107c3be8871aef59590e394ba680e136893a9fc6 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 26 Dec 2023 09:57:55 +0800 Subject: [PATCH 053/110] pre-commit --- src/XRFeitoriaUnreal/Content/Python/sequence.py | 3 +-- xrfeitoria/sequence/sequence_unreal.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 496bc2ad..1ed623d1 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -959,8 +959,7 @@ def get_data_asset_info(data_asset_path: str) -> Tuple[str, str]: @classmethod def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: - """ - Set the playback range for the sequence. + """Set the playback range for the sequence. Args: start_frame (Optional[int]): The start frame of the playback range. Defaults to None. diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 955c5fd7..95449f31 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -223,8 +223,7 @@ def get_seq_path(cls) -> str: @classmethod def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: - """ - Sets the playback range for the sequence. + """Set the playback range for the sequence. Args: start_frame (Optional[int]): The start frame of the playback range. If not provided, the default start frame will be used. From 8e51f06f5e69f3c4576cc4e4c2125ecba297f239 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 26 Dec 2023 14:35:43 +0800 Subject: [PATCH 054/110] Update `set_camera_cut_playback` for Sequence in UE --- .../Content/Python/sequence.py | 47 +++++++++++++++---- xrfeitoria/data_structure/models.py | 6 +-- xrfeitoria/renderer/renderer_unreal.py | 13 +++-- xrfeitoria/sequence/sequence_base.pyi | 27 +++++++++-- xrfeitoria/sequence/sequence_blender.pyi | 5 -- xrfeitoria/sequence/sequence_unreal.py | 29 ++++++++++-- xrfeitoria/sequence/sequence_unreal.pyi | 27 ++++++----- xrfeitoria/sequence/sequence_wrapper.py | 2 +- 8 files changed, 112 insertions(+), 44 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 1ed623d1..fdc414ce 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -17,7 +17,6 @@ EditorLevelSequenceSub = SubSystem.EditorLevelSequenceSub EditorAssetSub = SubSystem.EditorAssetSub EditorLevelSub = SubSystem.EditorLevelSub -START_FRAME = -1 ################################################################################ # misc @@ -457,13 +456,13 @@ def add_level_visibility_to_sequence( # add level visibility section level_visible_section: unreal.MovieSceneLevelVisibilitySection = level_visibility_track.add_section() level_visible_section.set_visibility(unreal.LevelVisibility.VISIBLE) - level_visible_section.set_start_frame(START_FRAME) + level_visible_section.set_start_frame(Sequence.START_FRAME) level_visible_section.set_end_frame(seq_length) level_hidden_section: unreal.MovieSceneLevelVisibilitySection = level_visibility_track.add_section() level_hidden_section.set_row_index(1) level_hidden_section.set_visibility(unreal.LevelVisibility.HIDDEN) - level_hidden_section.set_start_frame(START_FRAME) + level_hidden_section.set_start_frame(Sequence.START_FRAME) level_hidden_section.set_end_frame(seq_length) return level_visible_section, level_hidden_section @@ -527,7 +526,7 @@ def add_camera_to_sequence( camera_binding = sequence.add_possessable(camera) camera_track: unreal.MovieScene3DTransformTrack = camera_binding.add_track(unreal.MovieScene3DTransformTrack) # type: ignore camera_section: unreal.MovieScene3DTransformSection = camera_track.add_section() # type: ignore - camera_section.set_start_frame(START_FRAME) + camera_section.set_start_frame(Sequence.START_FRAME) camera_section.set_end_frame(seq_length) camera_component_binding = sequence.add_possessable(camera.camera_component) camera_component_binding.set_parent(camera_binding) @@ -539,9 +538,9 @@ def add_camera_to_sequence( camera_cut_track: unreal.MovieSceneCameraCutTrack = sequence.add_master_track(unreal.MovieSceneCameraCutTrack) # type: ignore # add a camera cut track for this camera - # make sure the camera cut is stretched to the START_FRAME mark + # make sure the camera cut is stretched to the Sequence.START_FRAME mark camera_cut_section: unreal.MovieSceneCameraCutSection = camera_cut_track.add_section() # type: ignore - camera_cut_section.set_start_frame(START_FRAME) + camera_cut_section.set_start_frame(Sequence.START_FRAME) camera_cut_section.set_end_frame(seq_length) # set the camera cut to use this camera @@ -609,9 +608,9 @@ def add_spawnable_camera_to_sequence( # camera_cut_track = sequence.add_track(unreal.MovieSceneCameraCutTrack) camera_cut_track: unreal.MovieSceneCameraCutTrack = sequence.add_master_track(unreal.MovieSceneCameraCutTrack) - # add a camera cut track for this camera, make sure the camera cut is stretched to the START_FRAME mark + # add a camera cut track for this camera, make sure the camera cut is stretched to the Sequence.START_FRAME mark camera_cut_section: unreal.MovieSceneCameraCutSection = camera_cut_track.add_section() - camera_cut_section.set_start_frame(START_FRAME) + camera_cut_section.set_start_frame(Sequence.START_FRAME) camera_cut_section.set_end_frame(seq_length) # set the camera cut to use this camera @@ -808,6 +807,8 @@ class Sequence: sequence: unreal.LevelSequence = None bindings: Dict[str, Dict[str, Any]] = {} + START_FRAME = -1 + def __init__(self) -> NoReturn: raise Exception('Sequence (XRFeitoriaUnreal/Python) should not be instantiated') @@ -957,6 +958,26 @@ def get_data_asset_info(data_asset_path: str) -> Tuple[str, str]: map_path = seq_data_asset.get_editor_property('MapPath').export_text() return seq_path.split('.')[0], map_path.split('.')[0] + @classmethod + def set_camera_cut_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: + """Set the camera cut playback. + + Args: + start_frame (Optional[int], optional): start frame of the camera cut playback. Defaults to None. + end_frame (Optional[int], optional): end frame of the camera cut playback. Defaults to None. + + Raises: + AssertionError: If the sequence is not initialized. + """ + assert cls.sequence is not None, 'Sequence not initialized' + camera_tracks = cls.sequence.find_master_tracks_by_type(unreal.MovieSceneCameraCutTrack) + for camera_track in camera_tracks: + for section in camera_track.get_sections(): + if start_frame: + section.set_start_frame(start_frame) + if end_frame: + section.set_end_frame(end_frame) + @classmethod def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: """Set the playback range for the sequence. @@ -969,10 +990,20 @@ def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int AssertionError: If the sequence is not initialized. """ assert cls.sequence is not None, 'Sequence not initialized' + master_tracks = cls.sequence.get_tracks() + if start_frame: + cls.START_FRAME = start_frame cls.sequence.set_playback_start(start_frame=start_frame) + for master_track in master_tracks: + for section in master_track.get_sections(): + section.set_start_frame(start_frame) + if end_frame: cls.sequence.set_playback_end(end_frame=end_frame) + for master_track in master_tracks: + for section in master_track.get_sections(): + section.set_end_frame(end_frame) # ------ add actor and camera -------- # diff --git a/xrfeitoria/data_structure/models.py b/xrfeitoria/data_structure/models.py index 0f93c4ec..292ca7d0 100644 --- a/xrfeitoria/data_structure/models.py +++ b/xrfeitoria/data_structure/models.py @@ -300,9 +300,9 @@ class SequenceTransformKey(BaseModel): def __init__( self, frame: int, - location: Vector = None, - rotation: Vector = None, - scale: Vector = None, + location: Optional[Vector] = None, + rotation: Optional[Vector] = None, + scale: Optional[Vector] = None, interpolation: Literal['CONSTANT', 'AUTO', 'LINEAR'] = 'AUTO', ) -> None: """Transform key for Unreal or Blender, which contains frame, location, diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 273dd4c1..17d95d0d 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -18,8 +18,7 @@ pass try: - from ..data_structure.models import RenderJobUnreal as RenderJob - from ..data_structure.models import RenderPass + from ..data_structure.models import RenderJobUnreal, RenderPass except ModuleNotFoundError: pass @@ -28,7 +27,7 @@ class RendererUnreal(RendererBase): """Renderer class for Unreal.""" - render_queue: 'List[RenderJob]' = [] + render_queue: 'List[RenderJobUnreal]' = [] @classmethod def add_job( @@ -40,7 +39,7 @@ def add_job( render_passes: 'List[RenderPass]', file_name_format: str = '{sequence_name}/{render_pass}/{camera_name}/{frame_number}', console_variables: Dict[str, float] = {'r.MotionBlurQuality': 0}, - anti_aliasing: 'Optional[RenderJob.AntiAliasSetting]' = None, + anti_aliasing: 'Optional[RenderJobUnreal.AntiAliasSetting]' = None, export_vertices: bool = False, export_skeleton: bool = False, ) -> None: @@ -55,7 +54,7 @@ def add_job( file_name_format (str, optional): File name format of the output image. Defaults to ``{sequence_name}/{render_pass}/{camera_name}/{frame_number}``. console_variables (Dict[str, float], optional): Console variables to set. Defaults to ``{'r.MotionBlurQuality': 0}``. Ref to :ref:`FAQ-console-variables` for details. - anti_aliasing (Optional[RenderJob.AntiAliasSetting], optional): Anti aliasing setting. Defaults to None. + anti_aliasing (Optional[RenderJobUnreal.AntiAliasSetting], optional): Anti aliasing setting. Defaults to None. export_vertices (bool, optional): Whether to export vertices. Defaults to False. export_skeleton (bool, optional): Whether to export skeleton. Defaults to False. @@ -63,7 +62,7 @@ def add_job( The motion blur is turned off by default. If you want to turn it on, please set ``r.MotionBlurQuality`` to a non-zero value in ``console_variables``. """ if anti_aliasing is None: - anti_aliasing = RenderJob.AntiAliasSetting() + anti_aliasing = RenderJobUnreal.AntiAliasSetting() # turn off motion blur by default if 'r.MotionBlurQuality' not in console_variables.keys(): @@ -73,7 +72,7 @@ def add_job( "If you want to turn off the motion blur the same as default, set ``console_variables={..., 'r.MotionBlurQuality': 0}``." ) - job = RenderJob( + job = RenderJobUnreal( map_path=map_path, sequence_path=sequence_path, output_path=Path(output_path).resolve(), diff --git a/xrfeitoria/sequence/sequence_base.pyi b/xrfeitoria/sequence/sequence_base.pyi index ac8dfec3..4e6198e0 100644 --- a/xrfeitoria/sequence/sequence_base.pyi +++ b/xrfeitoria/sequence/sequence_base.pyi @@ -36,7 +36,13 @@ class SequenceBase(ABC): stencil_value: int = ..., ) -> ...: ... @classmethod - def spawn_camera(cls, location: Vector, rotation: Vector, fov: float = ..., camera_name: str = ...) -> ...: ... + def spawn_camera( + cls, + location: Vector, + rotation: Vector, + fov: float = ..., + camera_name: str = ..., + ) -> ...: ... @classmethod def spawn_camera_with_keys( cls, @@ -45,10 +51,19 @@ class SequenceBase(ABC): camera_name: str = ..., ) -> ...: ... def use_camera( - cls, camera, location: Optional[Vector] = ..., rotation: Optional[Vector] = ..., fov: float = ... + cls, + camera, + location: Optional[Vector] = ..., + rotation: Optional[Vector] = ..., + fov: float = ..., ) -> None: ... @classmethod - def use_camera_with_keys(cls, camera, transform_keys: TransformKeys, fov: float = ...) -> None: ... + def use_camera_with_keys( + cls, + camera, + transform_keys: TransformKeys, + fov: float = ..., + ) -> None: ... @classmethod def use_actor( cls, @@ -89,5 +104,9 @@ class SequenceBase(ABC): ) -> ...: ... @classmethod def add_to_renderer( - cls, output_path: PathLike, resolution: Tuple[int, int], render_passes: List[RenderPass], **kwargs + cls, + output_path: PathLike, + resolution: Tuple[int, int], + render_passes: List[RenderPass], + **kwargs, ): ... diff --git a/xrfeitoria/sequence/sequence_blender.pyi b/xrfeitoria/sequence/sequence_blender.pyi index 7fc9843b..b4377a8a 100644 --- a/xrfeitoria/sequence/sequence_blender.pyi +++ b/xrfeitoria/sequence/sequence_blender.pyi @@ -1,4 +1,3 @@ -from pathlib import Path as Path from typing import List, Literal, Optional, Tuple, Union from ..actor.actor_blender import ActorBlender @@ -71,10 +70,6 @@ class SequenceBlender(SequenceBase): arrange_file_structure: bool = True, ): ... @classmethod - def use_camera( - cls, camera: CameraBlender, location: Optional[Vector] = ..., rotation: Optional[Vector] = ..., fov: float = ... - ) -> None: ... - @classmethod def use_camera_with_keys( cls, camera: CameraBlender, transform_keys: TransformKeys, fov: float = ... ) -> CameraBlender: ... diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 95449f31..52fc8db8 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -49,7 +49,7 @@ def add_to_renderer( """Add the sequence to the renderer's job queue. Can only be called after the sequence is instantiated using :meth:`~xrfeitoria.sequence.sequence_wrapper.SequenceWrapperUnreal.new` or - :meth:`~xrfeitoria.sequence.sequence_wrapper.SequenceWrapperUnreal.o pen`. + :meth:`~xrfeitoria.sequence.sequence_wrapper.SequenceWrapperUnreal.open`. Args: output_path (PathLike): The path where the rendered output will be saved. @@ -101,8 +101,8 @@ def add_to_renderer( def spawn_actor( cls, actor_asset_path: str, - location: 'Vector' = None, - rotation: 'Vector' = None, + location: 'Optional[Vector]' = None, + rotation: 'Optional[Vector]' = None, scale: 'Optional[Vector]' = None, actor_name: Optional[str] = None, stencil_value: int = 1, @@ -115,8 +115,8 @@ def spawn_actor( Args: cls: The class object. actor_asset_path (str): The actor asset path in engine to spawn. - location (Vector): The location to spawn the actor at. unit: meter. - rotation (Vector): The rotation to spawn the actor with. unit: degree. + location (Optional[Vector, optional]): The location to spawn the actor at. unit: meter. + rotation (Optional[Vector, optional]): The rotation to spawn the actor with. unit: degree. scale (Optional[Vector], optional): The scale to spawn the actor with. Defaults to None. actor_name (Optional[str], optional): The name to give the spawned actor. Defaults to None. stencil_value (int in [0, 255], optional): The stencil value to use for the spawned actor. Defaults to 1. @@ -234,6 +234,19 @@ def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int """ cls._set_playback_in_engine(start_frame=start_frame, end_frame=end_frame) + @classmethod + def set_camera_cut_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: + """Set the playback range for the sequence. + + Args: + start_frame (Optional[int]): The start frame of the playback range. If not provided, the default start frame will be used. + end_frame (Optional[int]): The end frame of the playback range. If not provided, the default end frame will be used. + + Returns: + None + """ + cls._set_camera_cut_player_in_engine(start_frame=start_frame, end_frame=end_frame) + @classmethod def _open(cls, seq_name: str, seq_dir: 'Optional[str]' = None) -> None: """Open an exist sequence. @@ -338,6 +351,12 @@ def _show_seq_in_engine() -> None: def _set_playback_in_engine(start_frame: 'Optional[int]' = None, end_frame: 'Optional[int]' = None) -> None: XRFeitoriaUnrealFactory.Sequence.set_playback(start_frame=start_frame, end_frame=end_frame) + @staticmethod + def _set_camera_cut_player_in_engine( + start_frame: 'Optional[int]' = None, end_frame: 'Optional[int]' = None + ) -> None: + XRFeitoriaUnrealFactory.Sequence.set_camera_cut_playback(start_frame=start_frame, end_frame=end_frame) + # ------ add actor and camera -------- # @staticmethod diff --git a/xrfeitoria/sequence/sequence_unreal.pyi b/xrfeitoria/sequence/sequence_unreal.pyi index 51715693..a5010b54 100644 --- a/xrfeitoria/sequence/sequence_unreal.pyi +++ b/xrfeitoria/sequence/sequence_unreal.pyi @@ -41,26 +41,27 @@ class SequenceUnreal(SequenceBase): cls, location: Vector, rotation: Vector, fov: float = ..., camera_name: str = ... ) -> CameraUnreal: ... @classmethod + @classmethod def spawn_actor( cls, actor_asset_path: str, - location: Vector, - rotation: Vector, - scale: Optional[Vector] = ..., - actor_name: Optional[str] = ..., - stencil_value: int = ..., - anim_asset_path: Optional[str] = ..., - motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, + location: Optional[Vector] = None, + rotation: Optional[Vector] = None, + scale: Optional[Vector] = None, + actor_name: Optional[str] = None, + stencil_value: int = 1, + anim_asset_path: Optional[str] = None, + motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, ) -> ActorUnreal: ... @classmethod def spawn_actor_with_keys( cls, actor_asset_path: str, transform_keys: TransformKeys, - actor_name: Optional[str] = ..., - stencil_value: int = ..., - anim_asset_path: Optional[str] = ..., - motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, + actor_name: Optional[str] = None, + stencil_value: int = 1, + anim_asset_path: Optional[str] = None, + motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, ) -> ActorUnreal: ... @classmethod def use_camera( @@ -93,4 +94,8 @@ class SequenceUnreal(SequenceBase): @classmethod def get_seq_path(cls) -> str: ... @classmethod + def set_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: ... + @classmethod + def set_camera_cut_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: ... + @classmethod def _open(cls, seq_name: str, seq_dir: 'Optional[str]' = ...) -> None: ... diff --git a/xrfeitoria/sequence/sequence_wrapper.py b/xrfeitoria/sequence/sequence_wrapper.py index 12826f1d..dc65173d 100644 --- a/xrfeitoria/sequence/sequence_wrapper.py +++ b/xrfeitoria/sequence/sequence_wrapper.py @@ -88,7 +88,7 @@ def open(cls, seq_name: str, seq_dir: 'Optional[str]' = None) -> ContextManager[ Defaults to None and fallback to the default path '/Game/XRFeitoriaUnreal/Sequences'. Yields: - SequenceBase: Sequence object. + SequenceUnreal: Sequence object. """ cls._seq._open(seq_name=seq_name, seq_dir=seq_dir) yield cls._seq From 10f2ccb92c61352d3bb9114f7c90b5576ba79be4 Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Wed, 27 Dec 2023 18:13:58 +0800 Subject: [PATCH 055/110] [fix bug]save seq properties when closing seq --- src/XRFeitoriaBpy/core/factory.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 6be879ce..caac11c7 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -229,6 +229,15 @@ def close_sequence() -> None: # clear all sequences in this level for collection in level_scene.collection.children: if XRFeitoriaBlenderFactory.is_sequence_collecion(collection): + # save sequence properties + XRFeitoriaBlenderFactory.set_sequence_properties( + collection=collection, + level=level_scene, + fps=level_scene.render.fps, + frame_start=level_scene.frame_start, + frame_end=level_scene.frame_end, + frame_current=level_scene.frame_current + ) # unlink the sequence from the level XRFeitoriaBlenderFactory.unlink_collection_from_scene(collection=collection, scene=level_scene) # restore level actors' properties From 0b0bef3a441f64afb4e21de8009e288a09d6d3e3 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Fri, 29 Dec 2023 00:45:07 +0800 Subject: [PATCH 056/110] Fix pass_index assignment for actor children --- src/XRFeitoriaBpy/core/factory.py | 6 +++--- xrfeitoria/actor/actor_blender.py | 2 +- xrfeitoria/sequence/sequence_blender.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index caac11c7..f80f4d42 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -205,7 +205,7 @@ def open_sequence(seq_name: str) -> 'bpy.types.Scene': for actor_data in seq_collection.sequence_properties.level_actors: actor = actor_data.actor actor.pass_index = actor_data.sequence_stencil_value - for child in actor.children: + for child in actor.children_recursive: child.pass_index = actor_data.sequence_stencil_value if actor_data.sequence_animation: XRFeitoriaBlenderFactory.apply_action_to_actor(action=actor_data.sequence_animation, actor=actor) @@ -236,7 +236,7 @@ def close_sequence() -> None: fps=level_scene.render.fps, frame_start=level_scene.frame_start, frame_end=level_scene.frame_end, - frame_current=level_scene.frame_current + frame_current=level_scene.frame_current, ) # unlink the sequence from the level XRFeitoriaBlenderFactory.unlink_collection_from_scene(collection=collection, scene=level_scene) @@ -244,7 +244,7 @@ def close_sequence() -> None: for actor_data in collection.sequence_properties.level_actors: actor = actor_data.actor actor.pass_index = actor_data.level_stencil_value - for child in actor.children: + for child in actor.children_recursive: child.pass_index = actor_data.level_stencil_value if actor_data.level_animation: XRFeitoriaBlenderFactory.apply_action_to_actor(action=actor_data.level_animation, actor=actor) diff --git a/xrfeitoria/actor/actor_blender.py b/xrfeitoria/actor/actor_blender.py index f5056d41..9e25dcd4 100644 --- a/xrfeitoria/actor/actor_blender.py +++ b/xrfeitoria/actor/actor_blender.py @@ -86,7 +86,7 @@ def _set_stencil_value_in_engine(actor_name: str, value: int) -> int: """ object = bpy.data.objects[actor_name] object.pass_index = value - for child in object.children: + for child in object.children_recursive: child.pass_index = value @staticmethod diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index b5e3539f..e3e83fc3 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -159,7 +159,7 @@ def _import_actor_in_engine( # XXX: set stencil value. may use actor property actor = bpy.data.objects[actor_name] actor.pass_index = stencil_value - for child in actor.children: + for child in actor.children_recursive: child.pass_index = stencil_value @staticmethod @@ -234,7 +234,7 @@ def _spawn_shape_in_engine( # XXX: set stencil value. may use actor property actor = bpy.data.objects[shape_name] actor.pass_index = stencil_value - for child in actor.children: + for child in actor.children_recursive: child.pass_index = stencil_value # -------- use methods -------- # @@ -313,7 +313,7 @@ def _use_actor_in_engine( # set level actor's properties actor.pass_index = stencil_value - for child in actor.children: + for child in actor.children_recursive: child.pass_index = stencil_value if action: XRFeitoriaBlenderFactory.apply_action_to_actor(action=action, actor=actor) From 53f608e692b1d58d21a994631c3edfd8cb2e0189 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 29 Dec 2023 10:25:03 +0800 Subject: [PATCH 057/110] add `from_amass_data` for `SMPLMotion` --- README.md | 2 +- xrfeitoria/utils/anim/motion.py | 43 +++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 875cec8d..422602bf 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@
[![Documentation](https://readthedocs.org/projects/xrfeitoria/badge/?version=latest)](https://xrfeitoria.readthedocs.io/en/latest/?badge=latest) -[![actions](https://github.com/openxrlab/xrfeitoria/workflows/lint/badge.svg)](https://github.com/openxrlab/xrfeitoria/actions) +[![actions](https://github.com/openxrlab/xrfeitoria/actions/workflows/lint.yml/badge.svg)](https://github.com/openxrlab/xrfeitoria/actions) [![PyPI](https://img.shields.io/pypi/v/xrfeitoria)](https://pypi.org/project/xrfeitoria/) [![license](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index 25073ab1..50405904 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -387,6 +387,40 @@ def from_smpl_data( instance.smpl_data = smpl_data return instance + @classmethod + def from_amass_data(cls, amass_data, insert_rest_pose: bool) -> Self: + """Create a Motion instance from AMASS data (SMPL) + + Args: + amass_data (dict): The AMASS data containing betas, transl, global_orient, and body_pose. + insert_rest_pose (bool): Whether to insert the rest pose into the SMPL data. + + Returns: + SMPLMotion: A SMPLMotion instance containing the AMASS data. + """ + if 'mocap_framerate' in amass_data: + fps = amass_data.get('mocap_framerate') + elif 'mocap_frame_rate' in amass_data: + fps = amass_data['mocap_frame_rate'] + else: + fps = 120.0 + + betas = amass_data['betas'][:10] + transl = amass_data['trans'] + global_orient = amass_data['poses'][:, :3] # controls the global root orientation + body_pose = amass_data['poses'][:, 3:66] # controls the body + # pose_hand = amass_data['poses'][:, 66:] # controls the finger articulation + # left_hand_pose = pose_hand[:, :45] + # right_hand_pose = pose_hand[:, 45:] + # dmpls = amass_data['dmpls'][:, :8] # controls soft tissue dynamics + + transl, global_orient = cls._transform_transl_global_orient(transl, global_orient) + smpl_data = {'betas': betas, 'transl': transl, 'global_orient': global_orient, 'body_pose': body_pose} + if insert_rest_pose: + smpl_data = cls._insert_rest_pose(smpl_x_data=smpl_data) + + return cls.from_smpl_data(smpl_data, insert_rest_pose=False, fps=fps) + def get_bone_rotvec(self, bone_name, frame=0) -> np.ndarray: idx = self._bone2idx(bone_name) if idx == 0: @@ -589,7 +623,7 @@ def from_smplx_data( @classmethod def from_amass_data(cls, amass_data, insert_rest_pose: bool, flat_hand_mean: bool = True) -> Self: - """Create a Motion instance from AMASS data. + """Create a Motion instance from AMASS data (SMPLX) Args: amass_data (dict): A dictionary containing the AMASS data. @@ -652,12 +686,7 @@ def from_amass_data(cls, amass_data, insert_rest_pose: bool, flat_hand_mean: boo # arr[0, 1] = pelvis_height smplx_data[key] = arr - return cls.from_smplx_data( - smplx_data, - insert_rest_pose=False, - fps=fps, - flat_hand_mean=flat_hand_mean, - ) + return cls.from_smplx_data(smplx_data, insert_rest_pose=False, fps=fps, flat_hand_mean=flat_hand_mean) def get_parent_bone_name(self, bone_name) -> Optional[str]: idx = self._bone2idx(bone_name) From 91ddfbfcb6af80f29e259b079e502aa42db9a5e3 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 29 Dec 2023 16:42:24 +0800 Subject: [PATCH 058/110] Add: `apply_shape_keys` for blender --- src/XRFeitoriaBpy/core/factory.py | 14 ++++++++ xrfeitoria/utils/anim/motion.py | 34 ------------------- .../utils/functions/blender_functions.py | 11 ++++++ 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index f80f4d42..c138f5ba 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -1147,6 +1147,20 @@ def apply_motion_data_to_actor(motion_data: 'List[Dict[str, Dict[str, List[float XRFeitoriaBlenderFactory.apply_motion_data_to_action(motion_data=motion_data, action=action) XRFeitoriaBlenderFactory.apply_action_to_actor(action, actor=bpy.data.objects[actor_name]) + def apply_shape_keys(shape_keys: 'List[Dict[str, float]]', actor_name: str) -> None: + """Apply shape keys to the specified actor. + + Args: + shape_keys (List[Dict[str, float]]): A list of dictionaries representing the shape keys and their values. + actor_name (str): The name of the actor object in Blender. + """ + actor = bpy.data.objects[actor_name] + for f in range(len(shape_keys)): + for key, value in shape_keys[f].items(): + actor.data.shape_keys.key_blocks[key].value = value + # set keyframe + actor.data.shape_keys.key_blocks[key].keyframe_insert(data_path='value', frame=f) + ##################################### ############# validate ############## ##################################### diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index 50405904..d76eddab 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -387,40 +387,6 @@ def from_smpl_data( instance.smpl_data = smpl_data return instance - @classmethod - def from_amass_data(cls, amass_data, insert_rest_pose: bool) -> Self: - """Create a Motion instance from AMASS data (SMPL) - - Args: - amass_data (dict): The AMASS data containing betas, transl, global_orient, and body_pose. - insert_rest_pose (bool): Whether to insert the rest pose into the SMPL data. - - Returns: - SMPLMotion: A SMPLMotion instance containing the AMASS data. - """ - if 'mocap_framerate' in amass_data: - fps = amass_data.get('mocap_framerate') - elif 'mocap_frame_rate' in amass_data: - fps = amass_data['mocap_frame_rate'] - else: - fps = 120.0 - - betas = amass_data['betas'][:10] - transl = amass_data['trans'] - global_orient = amass_data['poses'][:, :3] # controls the global root orientation - body_pose = amass_data['poses'][:, 3:66] # controls the body - # pose_hand = amass_data['poses'][:, 66:] # controls the finger articulation - # left_hand_pose = pose_hand[:, :45] - # right_hand_pose = pose_hand[:, 45:] - # dmpls = amass_data['dmpls'][:, :8] # controls soft tissue dynamics - - transl, global_orient = cls._transform_transl_global_orient(transl, global_orient) - smpl_data = {'betas': betas, 'transl': transl, 'global_orient': global_orient, 'body_pose': body_pose} - if insert_rest_pose: - smpl_data = cls._insert_rest_pose(smpl_x_data=smpl_data) - - return cls.from_smpl_data(smpl_data, insert_rest_pose=False, fps=fps) - def get_bone_rotvec(self, bone_name, frame=0) -> np.ndarray: idx = self._bone2idx(bone_name) if idx == 0: diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index 2b08780e..0456c11e 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -77,6 +77,17 @@ def apply_motion_data_to_actor(motion_data: 'List[Dict[str, Dict[str, List[float XRFeitoriaBlenderFactory.apply_motion_data_to_actor(motion_data=motion_data, actor_name=actor_name) +@remote_blender() +def apply_shape_keys(shape_keys: 'List[Dict[str, float]]', actor_name: str) -> None: + """Apply shape keys to the specified actor. + + Args: + shape_keys (List[Dict[str, float]]): A list of dictionaries representing the shape keys and their values. + actor_name (str): The name of the actor object in Blender. + """ + XRFeitoriaBlenderFactory.apply_shape_keys(shape_keys=shape_keys, actor_name=actor_name) + + @remote_blender() def is_background_mode(warning: bool = False) -> bool: """Check whether Blender is running in background mode. From d611091487d509b76ab32a6b8db1626707b5b7bc Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 29 Dec 2023 16:45:32 +0800 Subject: [PATCH 059/110] add `set_active_level` in `blender_functions` --- xrfeitoria/utils/functions/blender_functions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index 0456c11e..bae6c48a 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -167,6 +167,22 @@ def set_hdr_map(hdr_map_path: 'PathLike') -> None: XRFeitoriaBlenderFactory.set_hdr_map(scene=scene, hdr_map_path=hdr_map_path) +@remote_blender() +def set_active_level(level_name: str): + """Sets the active level in XRFeitoria Blender Factory. + + Args: + level_name (str): The name of the level to set as active. (e.g. 'Scene') + + Example: + >>> import xrfeitoria as xf + >>> xf_runner = xf.init_blender() + >>> xf_runner.utils.set_active_level('Scene') # Return to default level defined by blender + """ + level = XRFeitoriaBlenderFactory.get_scene(level_name) + XRFeitoriaBlenderFactory.set_scene_active(level) + + @remote_blender() def get_frame_range() -> 'Tuple[int, int]': """Get the frame range of the active scene. From c769fe053922a3f9b960f4b83e2892dd1266d1cb Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Fri, 29 Dec 2023 21:30:17 +0800 Subject: [PATCH 060/110] [fix bug] add resolution to sequence properties --- samples/blender/04_staticmesh_render.py | 3 ++- samples/unreal/04_staticmesh_render.py | 3 ++- src/XRFeitoriaBpy/core/factory.py | 24 +++++++++++++++++++----- src/XRFeitoriaBpy/properties.py | 4 +++- xrfeitoria/sequence/sequence_blender.py | 2 ++ 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/samples/blender/04_staticmesh_render.py b/samples/blender/04_staticmesh_render.py index 5c2883db..864ae0e4 100644 --- a/samples/blender/04_staticmesh_render.py +++ b/samples/blender/04_staticmesh_render.py @@ -9,8 +9,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey -from xrfeitoria.utils import setup_logger, visualize_vertices +from xrfeitoria.utils import setup_logger +from ..utils import visualize_vertices from ..config import assets_path, blender_exec root = Path(__file__).parents[2].resolve() diff --git a/samples/unreal/04_staticmesh_render.py b/samples/unreal/04_staticmesh_render.py index 11f8e953..313ef3b0 100644 --- a/samples/unreal/04_staticmesh_render.py +++ b/samples/unreal/04_staticmesh_render.py @@ -9,8 +9,9 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey -from xrfeitoria.utils import setup_logger, visualize_vertices +from xrfeitoria.utils import setup_logger +from ..utils import visualize_vertices from ..config import assets_path, unreal_exec, unreal_project root = Path(__file__).parents[2].resolve() diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index f80f4d42..5b42db8f 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -123,6 +123,8 @@ def set_sequence_properties( frame_start: int, frame_end: int, frame_current: int, + resolution_x: int, + resolution_y: int ) -> None: """Set the sequence properties. @@ -133,29 +135,37 @@ def set_sequence_properties( frame_start (int): Start frame of the sequence. frame_end (int): End frame of the sequence. frame_current (int): Current frame of the sequence. + resoltion_x (int): Resolution_x of the sequence. + resoltion_y (int): Resolution_y of the sequence. """ collection.sequence_properties.level = level collection.sequence_properties.fps = fps collection.sequence_properties.frame_start = frame_start collection.sequence_properties.frame_end = frame_end collection.sequence_properties.frame_current = frame_current + collection.sequence_properties.resolution_x = resolution_x + collection.sequence_properties.resolution_y = resolution_y - def get_sequence_properties(collection: 'bpy.types.Collection') -> 'Tuple[bpy.types.Scene, int, int, int, int]': + def get_sequence_properties(collection: 'bpy.types.Collection') -> 'Tuple[bpy.types.Scene, int, int, int, int, int, int]': """Get the sequence properties. Args: collection (bpy.types.Collection): Collection of the sequence. Returns: - Tuple[bpy.types.Scene, int, int, int, int]: - The level(scene), FPS of the sequence, Start frame of the sequence, End frame of the sequence, Current frame of the sequence. + Tuple[bpy.types.Scene, int, int, int, int, int, int]: + The level(scene), FPS of the sequence, Start frame of the sequence, End frame of the sequence, Current frame of the sequence, + Resolution_x of the sequence, Resolution_y of the sequence. """ level = collection.sequence_properties.level fps = collection.sequence_properties.fps frame_start = collection.sequence_properties.frame_start frame_end = collection.sequence_properties.frame_end frame_current = collection.sequence_properties.frame_current - return level, fps, frame_start, frame_end, frame_current + resolution_x = collection.sequence_properties.resolution_x + resolution_y = collection.sequence_properties.resolution_y + + return level, fps, frame_start, frame_end, frame_current, resolution_x, resolution_y def open_sequence(seq_name: str) -> 'bpy.types.Scene': """Open the given sequence. @@ -166,7 +176,7 @@ def open_sequence(seq_name: str) -> 'bpy.types.Scene': # get sequence collection seq_collection = XRFeitoriaBlenderFactory.get_collection(seq_name) # get sequence properties - level_scene, fps, frame_start, frame_end, frame_current = XRFeitoriaBlenderFactory.get_sequence_properties( + level_scene, fps, frame_start, frame_end, frame_current, resolution_x, resolution_y = XRFeitoriaBlenderFactory.get_sequence_properties( collection=seq_collection ) # deactivate all cameras in this level @@ -187,6 +197,8 @@ def open_sequence(seq_name: str) -> 'bpy.types.Scene': level_scene.frame_end = frame_end level_scene.frame_current = frame_current level_scene.render.fps = fps + level_scene.render.resolution_x = resolution_x + level_scene.render.resolution_y = resolution_y # set cameras in this sequence to active for obj in seq_collection.objects: @@ -237,6 +249,8 @@ def close_sequence() -> None: frame_start=level_scene.frame_start, frame_end=level_scene.frame_end, frame_current=level_scene.frame_current, + resolution_x=level_scene.render.resolution_x, + resolution_y=level_scene.render.resolution_y ) # unlink the sequence from the level XRFeitoriaBlenderFactory.unlink_collection_from_scene(collection=collection, scene=level_scene) diff --git a/src/XRFeitoriaBpy/properties.py b/src/XRFeitoriaBpy/properties.py index 50ee2c10..cddf8acf 100644 --- a/src/XRFeitoriaBpy/properties.py +++ b/src/XRFeitoriaBpy/properties.py @@ -35,6 +35,8 @@ class SequenceProperties(bpy.types.PropertyGroup): frame_start: bpy.props.IntProperty() frame_end: bpy.props.IntProperty() frame_current: bpy.props.IntProperty() + resolution_x: bpy.props.IntProperty() + resolution_y: bpy.props.IntProperty() ## level properties for scene @@ -44,7 +46,7 @@ def active_sequence_update(self, context): if self.active_sequence is None: XRFeitoriaBlenderFactory.close_sequence() elif XRFeitoriaBlenderFactory.is_sequence_collecion(self.active_sequence): - level_scene, _, _, _, _ = XRFeitoriaBlenderFactory.get_sequence_properties(collection=self.active_sequence) + level_scene, _, _, _, _, _, _ = XRFeitoriaBlenderFactory.get_sequence_properties(collection=self.active_sequence) if self.active_sequence.name not in level_scene.collection.children: XRFeitoriaBlenderFactory.open_sequence(self.active_sequence.name) else: diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index e3e83fc3..23c3727c 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -96,6 +96,8 @@ def _new_seq_in_engine( frame_start=0, frame_end=seq_length - 1, frame_current=0, + resolution_x=level_scene.render.resolution_x, + resolution_y=level_scene.render.resolution_y ) level_scene.frame_start = 0 level_scene.frame_end = seq_length - 1 From 1f7312b30f62faea1f1d865286747e1ae600df0d Mon Sep 17 00:00:00 2001 From: wangfanzhou Date: Fri, 29 Dec 2023 21:43:54 +0800 Subject: [PATCH 061/110] pre-commit --- samples/blender/04_staticmesh_render.py | 2 +- samples/unreal/04_staticmesh_render.py | 2 +- src/XRFeitoriaBpy/core/factory.py | 20 ++++++++++++++------ src/XRFeitoriaBpy/properties.py | 4 +++- xrfeitoria/sequence/sequence_blender.py | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/samples/blender/04_staticmesh_render.py b/samples/blender/04_staticmesh_render.py index 864ae0e4..f491b947 100644 --- a/samples/blender/04_staticmesh_render.py +++ b/samples/blender/04_staticmesh_render.py @@ -11,8 +11,8 @@ from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey from xrfeitoria.utils import setup_logger -from ..utils import visualize_vertices from ..config import assets_path, blender_exec +from ..utils import visualize_vertices root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' diff --git a/samples/unreal/04_staticmesh_render.py b/samples/unreal/04_staticmesh_render.py index 313ef3b0..9d3d7130 100644 --- a/samples/unreal/04_staticmesh_render.py +++ b/samples/unreal/04_staticmesh_render.py @@ -11,8 +11,8 @@ from xrfeitoria.data_structure.models import SequenceTransformKey as SeqTransKey from xrfeitoria.utils import setup_logger -from ..utils import visualize_vertices from ..config import assets_path, unreal_exec, unreal_project +from ..utils import visualize_vertices root = Path(__file__).parents[2].resolve() # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 88558832..058d3bca 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -124,7 +124,7 @@ def set_sequence_properties( frame_end: int, frame_current: int, resolution_x: int, - resolution_y: int + resolution_y: int, ) -> None: """Set the sequence properties. @@ -146,7 +146,9 @@ def set_sequence_properties( collection.sequence_properties.resolution_x = resolution_x collection.sequence_properties.resolution_y = resolution_y - def get_sequence_properties(collection: 'bpy.types.Collection') -> 'Tuple[bpy.types.Scene, int, int, int, int, int, int]': + def get_sequence_properties( + collection: 'bpy.types.Collection', + ) -> 'Tuple[bpy.types.Scene, int, int, int, int, int, int]': """Get the sequence properties. Args: @@ -176,9 +178,15 @@ def open_sequence(seq_name: str) -> 'bpy.types.Scene': # get sequence collection seq_collection = XRFeitoriaBlenderFactory.get_collection(seq_name) # get sequence properties - level_scene, fps, frame_start, frame_end, frame_current, resolution_x, resolution_y = XRFeitoriaBlenderFactory.get_sequence_properties( - collection=seq_collection - ) + ( + level_scene, + fps, + frame_start, + frame_end, + frame_current, + resolution_x, + resolution_y, + ) = XRFeitoriaBlenderFactory.get_sequence_properties(collection=seq_collection) # deactivate all cameras in this level for obj in level_scene.objects: if obj.type == 'CAMERA': @@ -250,7 +258,7 @@ def close_sequence() -> None: frame_end=level_scene.frame_end, frame_current=level_scene.frame_current, resolution_x=level_scene.render.resolution_x, - resolution_y=level_scene.render.resolution_y + resolution_y=level_scene.render.resolution_y, ) # unlink the sequence from the level XRFeitoriaBlenderFactory.unlink_collection_from_scene(collection=collection, scene=level_scene) diff --git a/src/XRFeitoriaBpy/properties.py b/src/XRFeitoriaBpy/properties.py index cddf8acf..719b0493 100644 --- a/src/XRFeitoriaBpy/properties.py +++ b/src/XRFeitoriaBpy/properties.py @@ -46,7 +46,9 @@ def active_sequence_update(self, context): if self.active_sequence is None: XRFeitoriaBlenderFactory.close_sequence() elif XRFeitoriaBlenderFactory.is_sequence_collecion(self.active_sequence): - level_scene, _, _, _, _, _, _ = XRFeitoriaBlenderFactory.get_sequence_properties(collection=self.active_sequence) + level_scene, _, _, _, _, _, _ = XRFeitoriaBlenderFactory.get_sequence_properties( + collection=self.active_sequence + ) if self.active_sequence.name not in level_scene.collection.children: XRFeitoriaBlenderFactory.open_sequence(self.active_sequence.name) else: diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index 23c3727c..20c22b10 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -97,7 +97,7 @@ def _new_seq_in_engine( frame_end=seq_length - 1, frame_current=0, resolution_x=level_scene.render.resolution_x, - resolution_y=level_scene.render.resolution_y + resolution_y=level_scene.render.resolution_y, ) level_scene.frame_start = 0 level_scene.frame_end = seq_length - 1 From ac3fef48874d82b42cc2ca64b156e72089666468 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Tue, 2 Jan 2024 14:32:17 +0800 Subject: [PATCH 062/110] rename --- src/XRFeitoriaBpy/core/factory.py | 8 ++++---- xrfeitoria/utils/functions/blender_functions.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 058d3bca..10516fb5 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -1169,14 +1169,14 @@ def apply_motion_data_to_actor(motion_data: 'List[Dict[str, Dict[str, List[float XRFeitoriaBlenderFactory.apply_motion_data_to_action(motion_data=motion_data, action=action) XRFeitoriaBlenderFactory.apply_action_to_actor(action, actor=bpy.data.objects[actor_name]) - def apply_shape_keys(shape_keys: 'List[Dict[str, float]]', actor_name: str) -> None: - """Apply shape keys to the specified actor. + def apply_shape_keys_to_mesh(shape_keys: 'List[Dict[str, float]]', mesh_name: str) -> None: + """Apply shape keys to the given mesh. Args: shape_keys (List[Dict[str, float]]): A list of dictionaries representing the shape keys and their values. - actor_name (str): The name of the actor object in Blender. + mesh_name (str): Name of the mesh. """ - actor = bpy.data.objects[actor_name] + actor = bpy.data.objects[mesh_name] for f in range(len(shape_keys)): for key, value in shape_keys[f].items(): actor.data.shape_keys.key_blocks[key].value = value diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index bae6c48a..97ca45b1 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -78,14 +78,14 @@ def apply_motion_data_to_actor(motion_data: 'List[Dict[str, Dict[str, List[float @remote_blender() -def apply_shape_keys(shape_keys: 'List[Dict[str, float]]', actor_name: str) -> None: - """Apply shape keys to the specified actor. +def apply_shape_keys_to_mesh(shape_keys: 'List[Dict[str, float]]', mesh_name: str) -> None: + """Apply shape keys to the given mesh. Args: shape_keys (List[Dict[str, float]]): A list of dictionaries representing the shape keys and their values. - actor_name (str): The name of the actor object in Blender. + mesh_name (str): Name of the mesh. """ - XRFeitoriaBlenderFactory.apply_shape_keys(shape_keys=shape_keys, actor_name=actor_name) + XRFeitoriaBlenderFactory.apply_shape_keys_to_mesh(shape_keys=shape_keys, mesh_name=mesh_name) @remote_blender() From aeb71f4f0f55ad196c2a5f2e502897a3bd39d284 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Wed, 3 Jan 2024 12:16:47 +0800 Subject: [PATCH 063/110] Copy motion instances in derived classes --- xrfeitoria/utils/anim/motion.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index d76eddab..c68ff9c2 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -289,6 +289,15 @@ def get_motion_data(self) -> List[Dict[str, Dict[str, List[float]]]]: motion_data.append(frame_motion_data) return motion_data + def copy(self) -> Self: + """Return a copy of the motion instance.""" + return self.__class__( + transl=self.transl.copy(), + body_poses=self.body_poses.copy(), + n_frames=self.n_frames, + fps=self.fps, + ) + def __repr__(self) -> str: return f'Motion(n_frames={self.n_frames}, fps={self.fps})' @@ -458,6 +467,17 @@ def dump_humandata( filepath.parent.mkdir(parents=True, exist_ok=True) np.savez(filepath, **humandata) + def copy(self) -> Self: + """Return a copy of the motion instance.""" + instance = self.__class__( + transl=self.transl.copy(), + body_poses=self.body_poses.copy(), + n_frames=self.n_frames, + fps=self.fps, + ) + instance.smpl_data = {k: v.copy() for k, v in self.smpl_data.items()} + return instance + def __repr__(self) -> str: return f'SMPLMotion(n_frames={self.n_frames}, fps={self.fps})' @@ -722,6 +742,17 @@ def dump_humandata( filepath.parent.mkdir(parents=True, exist_ok=True) np.savez(filepath, **humandata) + def copy(self) -> Self: + """Return a copy of the motion instance.""" + instance = self.__class__( + transl=self.transl.copy(), + body_poses=self.body_poses.copy(), + n_frames=self.n_frames, + fps=self.fps, + ) + instance.smplx_data = {k: v.copy() for k, v in self.smplx_data.items()} + return instance + def __repr__(self) -> str: return f'SMPLXMotion(n_frames={self.n_frames}, fps={self.fps})' From efcd18e4ec3e5f151d350943693ba75bd2353916 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Wed, 3 Jan 2024 12:17:09 +0800 Subject: [PATCH 064/110] Add `__repr__` method to ActorBase, CameraBase, ObjectBase, and SequenceBase classes --- xrfeitoria/actor/actor_base.py | 3 +++ xrfeitoria/camera/camera_base.py | 3 +++ xrfeitoria/object/object_base.py | 3 +++ xrfeitoria/sequence/sequence_base.py | 3 +++ 4 files changed, 12 insertions(+) diff --git a/xrfeitoria/actor/actor_base.py b/xrfeitoria/actor/actor_base.py index 09999a48..2123ed9b 100644 --- a/xrfeitoria/actor/actor_base.py +++ b/xrfeitoria/actor/actor_base.py @@ -130,6 +130,9 @@ def setup_animation(self, animation_path: 'PathLike', action_name: 'Optional[str f'from "{animation_path}" and setup for actor "{self.name}"' ) + def __repr__(self) -> str: + return f'' + ##################################### ###### RPC METHODS (Private) ######## ##################################### diff --git a/xrfeitoria/camera/camera_base.py b/xrfeitoria/camera/camera_base.py index f6263186..89cc5a6c 100644 --- a/xrfeitoria/camera/camera_base.py +++ b/xrfeitoria/camera/camera_base.py @@ -100,6 +100,9 @@ def look_at(self, target: Vector) -> None: # self.rotation = self._object_utils.direction_to_euler(direction) self._look_at_in_engine(self._name, target) + def __repr__(self) -> str: + return f'' + ################################# #### RPC METHODS (Private) #### ################################# diff --git a/xrfeitoria/object/object_base.py b/xrfeitoria/object/object_base.py index 6e923ac3..cf14828d 100644 --- a/xrfeitoria/object/object_base.py +++ b/xrfeitoria/object/object_base.py @@ -98,3 +98,6 @@ def delete(self): self._object_utils.delete_obj(self._name) logger.info(f'[red]Deleted[/red] object "{self.name}"') del self + + def __repr__(self): + return f'' diff --git a/xrfeitoria/sequence/sequence_base.py b/xrfeitoria/sequence/sequence_base.py index 554a3e0e..063e12e1 100644 --- a/xrfeitoria/sequence/sequence_base.py +++ b/xrfeitoria/sequence/sequence_base.py @@ -436,6 +436,9 @@ def add_to_renderer( ) # return render_job + def __repr__(self) -> str: + return f'' + ##################################### ###### RPC METHODS (Private) ######## ##################################### From d817064bc03b5519148161616ea706b4117c7181 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Wed, 3 Jan 2024 16:41:04 +0800 Subject: [PATCH 065/110] Add load_humandata_motion function to anim utils --- xrfeitoria/utils/anim/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrfeitoria/utils/anim/__init__.py b/xrfeitoria/utils/anim/__init__.py index 835692ca..9e9e61a3 100644 --- a/xrfeitoria/utils/anim/__init__.py +++ b/xrfeitoria/utils/anim/__init__.py @@ -1 +1 @@ -from .utils import load_amass_motion +from .utils import dump_humandata, load_amass_motion, load_humandata_motion From 4d492061cb8ab5a0ccee0cf0133132b46b346dab Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Wed, 3 Jan 2024 16:42:01 +0800 Subject: [PATCH 066/110] Refactor import statements in 07_amass.py --- samples/blender/07_amass.py | 2 +- samples/unreal/07_amass.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index 4881b961..91dc28dd 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -16,7 +16,7 @@ from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.rpc import remote_blender from xrfeitoria.utils import setup_logger -from xrfeitoria.utils.anim.utils import dump_humandata, load_amass_motion +from xrfeitoria.utils.anim import dump_humandata, load_amass_motion # prepare the assets #################### diff --git a/samples/unreal/07_amass.py b/samples/unreal/07_amass.py index 0f77e800..8fa83e29 100644 --- a/samples/unreal/07_amass.py +++ b/samples/unreal/07_amass.py @@ -13,7 +13,7 @@ import xrfeitoria as xf from xrfeitoria.data_structure.models import RenderPass from xrfeitoria.utils import setup_logger -from xrfeitoria.utils.anim.utils import dump_humandata, load_amass_motion +from xrfeitoria.utils.anim import dump_humandata, load_amass_motion from ..config import unreal_exec, unreal_project From 5defe13ee76a06ec598a7c0fd0c239cd61e3bf74 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Wed, 3 Jan 2024 18:28:40 +0800 Subject: [PATCH 067/110] Fix Unreal engine version extraction in runner.py --- xrfeitoria/utils/runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index e058308a..b49a6a78 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -624,7 +624,8 @@ def get_src_plugin_path(self) -> Path: @staticmethod def _get_engine_info(engine_exec: Path) -> Tuple[str, str]: try: - _version = re.findall(r'UE_(\d+\.\d+)', engine_exec.as_posix())[0] + build_info = (engine_exec.parents[2] / 'Build' / 'InstalledBuild.txt').read_text() + _version = re.findall(r'UE_(\d+\.\d+)', build_info)[0] except IndexError: raise FileNotFoundError(f'Cannot find unreal executable in {engine_exec}') return 'Unreal', _version From d762cec7cc0b09f7083c40d4a596f678b9aa1f74 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 5 Jan 2024 19:13:23 +0800 Subject: [PATCH 068/110] [Fix] Get ue version --- xrfeitoria/utils/runner.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index b49a6a78..23293a1b 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -623,11 +623,8 @@ def get_src_plugin_path(self) -> Path: @staticmethod def _get_engine_info(engine_exec: Path) -> Tuple[str, str]: - try: - build_info = (engine_exec.parents[2] / 'Build' / 'InstalledBuild.txt').read_text() - _version = re.findall(r'UE_(\d+\.\d+)', build_info)[0] - except IndexError: - raise FileNotFoundError(f'Cannot find unreal executable in {engine_exec}') + build_info = json.loads((engine_exec.parents[2] / 'Build' / 'Build.version').read_text()) + _version = f'{build_info["MajorVersion"]}.{build_info["MinorVersion"]}' return 'Unreal', _version def _get_cmd( From 7cec275e237730b827e6f208feaa02c60aa3b46a Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 8 Jan 2024 19:19:38 +0800 Subject: [PATCH 069/110] [Fix] RPC server stopping unexpectedly --- .../Content/Python/utils_actor.py | 2 +- xrfeitoria/renderer/renderer_unreal.py | 1 + xrfeitoria/rpc/client.py | 6 +- xrfeitoria/utils/runner.py | 82 +++++++++++++++---- 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/utils_actor.py b/src/XRFeitoriaUnreal/Content/Python/utils_actor.py index 1b7289d7..3c3bf56b 100644 --- a/src/XRFeitoriaUnreal/Content/Python/utils_actor.py +++ b/src/XRFeitoriaUnreal/Content/Python/utils_actor.py @@ -15,7 +15,7 @@ 'rgb': Tuple[int, int, int], }, ) -mask_colors: List[color_type] = json.load((root / 'data' / 'mask_colors.json').open()) +mask_colors: List[color_type] = json.loads((root / 'data' / 'mask_colors.json').read_text()) def get_stencil_value(actor: unreal.Actor) -> int: diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 17d95d0d..8d00fe74 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -159,6 +159,7 @@ def render_jobs(cls) -> None: error_txt += f' Check unreal log: "{log_path.as_posix()}"' logger.error(error_txt) + break # cls.clear() server.close() diff --git a/xrfeitoria/rpc/client.py b/xrfeitoria/rpc/client.py index 398a0a73..d7cc61bd 100644 --- a/xrfeitoria/rpc/client.py +++ b/xrfeitoria/rpc/client.py @@ -4,7 +4,7 @@ import inspect import os import re -from xmlrpc.client import ExpatParser, ResponseError, ServerProxy, Transport, Unmarshaller +from xmlrpc.client import ExpatParser, Fault, ResponseError, ServerProxy, Transport, Unmarshaller class RPCUnmarshaller(Unmarshaller): @@ -52,9 +52,9 @@ def close(self): logger.error( f"RPC Fault :\n{marshallables.get('faultString')}" ) - # raise Fault(**marshallables) + raise Fault(**marshallables) # raise RuntimeError('RPC Fault') - exit(1) + # exit(1) return tuple(self._stack) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 23293a1b..9ddf9adf 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -17,6 +17,7 @@ from urllib.error import HTTPError, URLError from xmlrpc.client import ProtocolError +import psutil from loguru import logger from packaging.version import parse from rich import get_console @@ -91,6 +92,7 @@ def __init__( """ self.console = get_console() self.engine_type: EngineEnum = _tls.cache.get('platform', None) + self.engine_pid: Optional[int] = None self.engine_process: Optional[subprocess.Popen] = None self.engine_running: bool = False self.engine_outputs: List[str] = [] @@ -175,25 +177,28 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: def stop(self) -> None: """Stop rpc server.""" - import psutil # isort:skip - from ..rpc.factory import RPCFactory # isort:skip + # clear rpc server + factory.RPCFactory.clear() + # stop engine process process = self.engine_process if process is not None: logger.info(':bell: [bold red]Exiting RPC Server[/bold red], killing engine process') + if psutil.pid_exists(self.engine_pid): + for child in psutil.Process(self.engine_pid).children(recursive=True): + if child.is_running(): + logger.debug(f'Killing child process {child.pid}') + child.kill() + process.kill() self.engine_running = False - for child in psutil.Process(process.pid).children(recursive=True): - if child.is_running(): - logger.debug(f'Killing child process {child.pid}') - child.kill() - process.kill() self.engine_process = None + self.engine_pid = None + if hasattr(_tls, 'cache'): # prevent to be called from another thread + _tls.cache['engine_process'] = None + _tls.cache['engine_pid'] = None else: logger.info(':bell: [bold red]Exiting runner[/bold red], reused engine process remains') - # clear rpc server - RPCFactory.clear() - def reuse(self) -> bool: """Try to reuse existing engine process. @@ -206,6 +211,7 @@ def reuse(self) -> bool: try: with self.console.status('[bold green]Try to reuse existing engine process...[/bold green]'): self.test_connection(debug=self.debug) + self.engine_pid = self.get_pid() logger.info(':direct_hit: [bold cyan]Reuse[/bold cyan] existing engine process') # raise an error if new_process is True if self.new_process: @@ -238,7 +244,7 @@ def _popen(cmd: str) -> subprocess.Popen: return process def _receive_stdout(self) -> None: - """Receive output from the subprocess, and log it if `self.debug=True`. + """Receive output from the subprocess, and log it in `trace` level. This function should be called in a separate thread. """ @@ -254,13 +260,34 @@ def _receive_stdout(self) -> None: logger.trace(f'(engine) {data.strip()}') self.engine_outputs.append(data) - def check_engine_alive(self) -> bool: - """Check if the engine process is alive.""" + def check_engine_alive(self) -> None: + """Check if the engine process is alive. This function should be called in a + separate thread. + + Raises: + RuntimeError: if engine process is not alive. + """ while self.engine_running: if self.engine_process.poll() is not None: logger.error(self.get_process_output(self.engine_process)) logger.error('[red]RPC server stopped unexpectedly, check the engine output above[/red]') - os._exit(1) # exit main thread + factory.RPCFactory.clear() + raise RuntimeError('RPC server stopped unexpectedly') + time.sleep(1) + + def check_engine_alive_psutil(self) -> None: + """Check if the engine process is alive using psutil. This function should be + called in a separate thread. + + Raises: + RuntimeError: if engine process is not alive. + """ + p = psutil.Process(self.engine_pid) + while self.engine_running: + if not p.is_running(): + logger.error('[red]RPC server stopped unexpectedly, check the engine output above[/red]') + factory.RPCFactory.clear() + raise RuntimeError('RPC server stopped unexpectedly') time.sleep(1) def get_process_output(self, process: subprocess.Popen) -> str: @@ -278,6 +305,8 @@ def get_process_output(self, process: subprocess.Popen) -> str: def start(self) -> None: """Start rpc server.""" if not self.new_process: + self.engine_running = True + threading.Thread(target=self.check_engine_alive_psutil, daemon=True).start() return with self.console.status('Initializing RPC server...') as status: @@ -286,7 +315,9 @@ def start(self) -> None: status.update(status=f'[green bold]Starting {" ".join(self.engine_info)} as RPC server...') self.engine_process = self._start_rpc(background=self.background, project_path=self.project_path) self.engine_running = True + self.engine_pid = self.engine_process.pid _tls.cache['engine_process'] = self.engine_process + _tls.cache['engine_pid'] = self.engine_pid logger.info(f'RPC server started at port {self.port}') # check if engine process is alive in a separate thread @@ -409,7 +440,7 @@ def _get_cmd(self) -> str: pass @abstractmethod - def _start_rpc(self, background: bool = True, project_path: Optional[Path] = '') -> None: + def _start_rpc(self, background: bool = True, project_path: Optional[Path] = '') -> subprocess.Popen: pass def _get_plugin_url(self) -> Optional[str]: @@ -486,6 +517,11 @@ def _install_plugin(self) -> None: def test_connection(debug: bool = False) -> None: pass + @staticmethod + @abstractmethod + def get_pid() -> int: + pass + class BlenderRPCRunner(RPCRunner): def get_src_plugin_path(self) -> Path: @@ -587,6 +623,14 @@ def test_connection(debug: bool = False) -> bool: except Exception: pass + @staticmethod + @remote_blender(default_imports=[]) + def get_pid() -> int: + """Get blender process id.""" + import os + + return os.getpid() + class UnrealRPCRunner(RPCRunner): """UnrealRPCRunner.""" @@ -692,3 +736,11 @@ def test_connection(debug: bool = False) -> None: import unreal unreal.log('Connection test passed') + + @staticmethod + @remote_unreal(default_imports=[]) + def get_pid() -> int: + """Get unreal process id.""" + import os + + return os.getpid() From abadc7e32e0d7514fbd158dd2e2d329f571abc4e Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 12 Jan 2024 14:55:54 +0800 Subject: [PATCH 070/110] [Update] refactor `Sequence` - add `sequence` and deprecated `Sequence`: - merge Sequence.open() and Sequence.new() into one function - for blender, set renderer when job `add_to_renderer` - move `sequence.save()` from Base class to Unreal class - update the corresponding docs --- docs/en/apis/sequence.rst | 14 +-- xrfeitoria/factory.py | 10 +- xrfeitoria/sequence/sequence_base.py | 30 ++---- xrfeitoria/sequence/sequence_blender.py | 31 +++++- xrfeitoria/sequence/sequence_unreal.py | 15 +++ xrfeitoria/sequence/sequence_wrapper.py | 100 +++++++++++++++--- xrfeitoria/sequence/sequence_wrapper.pyi | 54 ++++------ .../utils/functions/blender_functions.py | 13 +++ 8 files changed, 181 insertions(+), 86 deletions(-) diff --git a/docs/en/apis/sequence.rst b/docs/en/apis/sequence.rst index 5b575dea..7834dcc9 100644 --- a/docs/en/apis/sequence.rst +++ b/docs/en/apis/sequence.rst @@ -5,9 +5,6 @@ xrfeitoria.sequence xrfeitoria.sequence.sequence_base.SequenceBase xrfeitoria.sequence.sequence_blender.SequenceBlender xrfeitoria.sequence.sequence_unreal.SequenceUnreal - xrfeitoria.sequence.sequence_wrapper.SequenceWrapperBase - xrfeitoria.sequence.sequence_wrapper.SequenceWrapperBlender - xrfeitoria.sequence.sequence_wrapper.SequenceWrapperUnreal :parts: 1 :align: center :skip-classes: abc.ABC @@ -22,6 +19,11 @@ xrfeitoria.sequence xrfeitoria.sequence.sequence_base.SequenceBase xrfeitoria.sequence.sequence_blender.SequenceBlender xrfeitoria.sequence.sequence_unreal.SequenceUnreal - xrfeitoria.sequence.sequence_wrapper.SequenceWrapperBase - xrfeitoria.sequence.sequence_wrapper.SequenceWrapperBlender - xrfeitoria.sequence.sequence_wrapper.SequenceWrapperUnreal + +---- + +.. autosummary:: + :toctree: generated/ + :template: custom-module.rst + + xrfeitoria.sequence.sequence_wrapper diff --git a/xrfeitoria/factory.py b/xrfeitoria/factory.py index 2ec9de16..e86f1677 100644 --- a/xrfeitoria/factory.py +++ b/xrfeitoria/factory.py @@ -16,7 +16,7 @@ class XRFeitoriaBlender: :class:`Actor `: Actor class.\n :class:`Shape `: Shape wrapper class.\n :class:`Renderer `: Renderer class.\n - :class:`Sequence `: Sequence wrapper class.\n + :class:`sequence `: Sequence wrapper function.\n :class:`utils `: Utilities functions executed in Blender.\n :meth:`render `: Render jobs.\n """ @@ -51,7 +51,7 @@ def __init__( from .actor.actor_blender import ActorBlender, ShapeBlenderWrapper # isort:skip from .material.material_blender import MaterialBlender # isort:skip from .renderer.renderer_blender import RendererBlender # isort:skip - from .sequence.sequence_wrapper import SequenceWrapperBlender # isort:skip + from .sequence.sequence_wrapper import SequenceWrapperBlender, sequence_wrapper_blender # isort:skip from .utils.runner import BlenderRPCRunner # isort:skip from .utils.functions import blender_functions # isort:skip @@ -63,6 +63,7 @@ def __init__( self.Shape = ShapeBlenderWrapper self.Renderer = RendererBlender self.render = self.Renderer.render_jobs + self.sequence = sequence_wrapper_blender self.Sequence = SequenceWrapperBlender self.utils = blender_functions self._rpc_runner = BlenderRPCRunner( @@ -85,7 +86,7 @@ class XRFeitoriaUnreal: :class:`Actor `: Actor class.\n :class:`Shape `: Shape wrapper class.\n :class:`Renderer `: Renderer class.\n - :class:`Sequence `: Sequence wrapper class.\n + :class:`sequence `: Sequence wrapper function.\n :class:`utils `: Utilities functions executed in Unreal.\n :meth:`render `: Render jobs.\n """ @@ -118,7 +119,7 @@ def __init__( from .camera.camera_unreal import CameraUnreal # isort:skip from .actor.actor_unreal import ActorUnreal, ShapeUnrealWrapper # isort:skip from .renderer.renderer_unreal import RendererUnreal # isort:skip - from .sequence.sequence_wrapper import SequenceWrapperUnreal # isort:skip + from .sequence.sequence_wrapper import SequenceWrapperUnreal, sequence_wrapper_unreal # isort:skip from .utils.runner import UnrealRPCRunner # isort:skip from .utils.functions import unreal_functions # isort:skip @@ -129,6 +130,7 @@ def __init__( self.Shape = ShapeUnrealWrapper self.Renderer = RendererUnreal self.render = self.Renderer.render_jobs + self.sequence = sequence_wrapper_unreal self.Sequence = SequenceWrapperUnreal self.utils = unreal_functions self._rpc_runner = UnrealRPCRunner( diff --git a/xrfeitoria/sequence/sequence_base.py b/xrfeitoria/sequence/sequence_base.py index 063e12e1..f7a77912 100644 --- a/xrfeitoria/sequence/sequence_base.py +++ b/xrfeitoria/sequence/sequence_base.py @@ -3,6 +3,7 @@ from typing import Dict, List, Literal, Optional, Tuple, Union from loguru import logger +from typing_extensions import Self from .. import _tls from ..actor.actor_base import ActorBase @@ -29,6 +30,12 @@ class SequenceBase(ABC): _renderer = RendererBase __platform__: EngineEnum = _tls.cache.get('platform', None) + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + @classmethod def _new( cls, @@ -43,7 +50,7 @@ def _new( Args: seq_name (str): Name of the sequence. - level (str): Level to link to the sequence. + level (Optional[str], optional): Name of the level. Defaults to None. seq_fps (int, optional): Frame per second of the new sequence. Defaults to 60. seq_length (int, optional): Frame length of the new sequence. Defaults to 60. replace (bool, optional): Replace the exist same-name sequence. Defaults to False. @@ -75,17 +82,6 @@ def close(cls) -> None: logger.info(f'<<<< [red]Closed[/red] sequence "{cls.name}" <<<<') cls.name = None - @classmethod - def save(cls) -> None: - """Save the sequence.""" - cls._save_seq_in_engine() - logger.info(f'++++ [cyan]Saved[/cyan] sequence "{cls.name}" ++++') - - @classmethod - def show(cls) -> None: - """Show the sequence in the engine.""" - cls._show_seq_in_engine() - # ------ import actor ------ # @classmethod def import_actor( @@ -497,13 +493,3 @@ def _open_seq_in_engine(*args, **kwargs) -> None: @abstractmethod def _close_seq_in_engine() -> None: pass - - @staticmethod - @abstractmethod - def _save_seq_in_engine() -> None: - pass - - @staticmethod - @abstractmethod - def _show_seq_in_engine() -> None: - pass diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index 20c22b10..49ab5ce8 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -1,8 +1,11 @@ from typing import Dict, List, Optional, Tuple, Union +from loguru import logger + from ..actor.actor_blender import ActorBlender, ShapeBlenderWrapper from ..camera.camera_blender import CameraBlender from ..data_structure.constants import PathLike, Vector +from ..data_structure.models import RenderPass from ..object.object_utils import ObjectUtilsBlender from ..renderer.renderer_blender import RendererBlender from ..rpc import remote_blender @@ -56,6 +59,30 @@ def import_actor_with_keys( cls._object_utils.set_transform_keys(name=actor.name, transform_keys=transform_keys) return actor + @classmethod + def add_to_renderer( + cls, + output_path: PathLike, + resolution: Tuple[int, int], + render_passes: List[RenderPass], + **kwargs, + ): + cls._renderer.add_job( + sequence_name=cls.name, + output_path=output_path, + resolution=resolution, + render_passes=render_passes, + **kwargs, + ) + # set renderer in engine (for storing the render settings like resolution, render_passes, etc.) + cls._renderer._set_renderer_in_engine( + job=cls._renderer.render_queue[-1].model_dump(mode='json'), tmp_render_path='/tmp' + ) + logger.info( + f'[cyan]Added[/cyan] sequence "{cls.name}" to [bold]`Renderer`[/bold] ' + f'(jobs to render: {len(cls._renderer.render_queue)})' + ) + ##################################### ###### RPC METHODS (Private) ######## ##################################### @@ -133,10 +160,6 @@ def _close_seq_in_engine() -> None: level_scene = XRFeitoriaBlenderFactory.get_active_scene() XRFeitoriaBlenderFactory.set_level_properties(scene=level_scene, active_seq=None) - @staticmethod - def _show_seq_in_engine() -> None: - raise NotImplementedError - # -------- spawn methods -------- # @staticmethod def _import_actor_in_engine( diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 52fc8db8..6bd65dd7 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -34,6 +34,21 @@ class SequenceUnreal(SequenceBase): _object_utils = ObjectUtilsUnreal _renderer = RendererUnreal + def __exit__(self, exc_type, exc_val, exc_tb): + self.save() + self.close() + + @classmethod + def save(cls) -> None: + """Save the sequence.""" + cls._save_seq_in_engine() + logger.info(f'++++ [cyan]Saved[/cyan] sequence "{cls.name}" ++++') + + @classmethod + def show(cls) -> None: + """Show the sequence in the engine.""" + cls._show_seq_in_engine() + @classmethod def add_to_renderer( cls, diff --git a/xrfeitoria/sequence/sequence_wrapper.py b/xrfeitoria/sequence/sequence_wrapper.py index dc65173d..e9d4bb46 100644 --- a/xrfeitoria/sequence/sequence_wrapper.py +++ b/xrfeitoria/sequence/sequence_wrapper.py @@ -1,24 +1,34 @@ +"""Sequence wrapper functions.""" + +import warnings from contextlib import contextmanager from typing import ContextManager, List, Optional, Tuple, Union from ..data_structure.constants import default_level_blender +from ..utils.functions import blender_functions, unreal_functions from .sequence_base import SequenceBase from .sequence_blender import SequenceBlender from .sequence_unreal import SequenceUnreal +__all__ = ['sequence_wrapper_blender', 'sequence_wrapper_unreal'] + -class SequenceWrapperBase: +class SequenceWrapperBlender: """Sequence utils class.""" - _seq = SequenceBase - default_level = None + _seq = SequenceBlender + _warn_msg = ( + '\n`Sequence` class will be deprecated in the future. Please use `sequence` function instead:\n' + '>>> xf_runner = xf.init_blender()\n' + '>>> with xf_runner.sequence(...) as seq: ...' + ) @classmethod @contextmanager def new( cls, seq_name: str, - level: Optional[str] = None, + level: str = default_level_blender, seq_fps: int = 30, seq_length: int = 1, replace: bool = False, @@ -34,8 +44,7 @@ def new( Yields: SequenceBase: Sequence object. """ - if level is None: - level = cls.default_level + warnings.showwarning(cls._warn_msg, DeprecationWarning, __file__, 0) cls._seq._new( seq_name=seq_name, level=level, @@ -44,7 +53,6 @@ def new( replace=replace, ) yield cls._seq - cls._seq.save() cls._seq.close() @classmethod @@ -58,24 +66,21 @@ def open(cls, seq_name: str) -> ContextManager[SequenceBase]: Yields: SequenceBase: Sequence object. """ + warnings.showwarning(cls._warn_msg, DeprecationWarning, __file__, 0) cls._seq._open(seq_name=seq_name) yield cls._seq - cls._seq.save() cls._seq.close() -class SequenceWrapperBlender(SequenceWrapperBase): - """Sequence utils class for Blender.""" - - _seq = SequenceBlender - default_level = default_level_blender - - -class SequenceWrapperUnreal(SequenceWrapperBase): +class SequenceWrapperUnreal: """Sequence utils class for Unreal.""" _seq = SequenceUnreal - default_level = None + _warn_msg = ( + '\n`Sequence` class will be deprecated in the future. Please use `sequence` function instead:\n' + '>>> xf_runner = xf.init_unreal()\n' + '>>> with xf_runner.sequence(...) as seq: ...' + ) @classmethod @contextmanager @@ -131,3 +136,64 @@ def new( yield cls._seq cls._seq.save() cls._seq.close() + + +def sequence_wrapper_blender( + seq_name: str, + level: str = default_level_blender, + seq_fps: int = 30, + seq_length: int = 1, + replace: bool = False, +) -> Union[SequenceBlender, ContextManager[SequenceBlender]]: + """Create a new sequence and close the sequence after exiting it. + + Args: + seq_name (str): The name of the sequence. + level (str, optional): The level to associate the sequence with. Defaults to 'XRFeitoria'. + seq_fps (int, optional): The frames per second of the sequence. Defaults to 30. + seq_length (int, optional): The length of the sequence. Defaults to 1. + replace (bool, optional): Whether to replace an existing sequence with the same name. Defaults to False. + + Returns: + SequenceBlender: The created SequenceBlender object. + """ + if blender_functions.check_sequence(seq_name=seq_name) and not replace: + SequenceBlender._open(seq_name=seq_name) + else: + SequenceBlender._new(seq_name=seq_name, level=level, seq_fps=seq_fps, seq_length=seq_length, replace=replace) + return SequenceBlender() + + +def sequence_wrapper_unreal( + seq_name: str, + seq_dir: Optional[str] = None, + level: Optional[str] = None, + seq_fps: int = 30, + seq_length: int = 1, + replace: bool = False, +) -> Union[SequenceUnreal, ContextManager[SequenceUnreal]]: + """Create a new sequence and close the sequence after exiting it. + + Args: + seq_name (str): The name of the sequence. + seq_dir (Optional[str], optional): The directory where the sequence is located. Defaults to None. + level (Optional[str], optional): The level to associate the sequence with. Defaults to None. + seq_fps (int, optional): The frames per second of the sequence. Defaults to 30. + seq_length (int, optional): The length of the sequence in seconds. Defaults to 1. + replace (bool, optional): Whether to replace an existing sequence with the same name. Defaults to False. + + Returns: + SequenceUnreal: The created SequenceUnreal object. + """ + if unreal_functions.check_asset_in_engine(f'{seq_dir}/{seq_name}') and not replace: + SequenceUnreal._open(seq_name=seq_name, seq_dir=seq_dir) + else: + SequenceUnreal._new( + seq_name=seq_name, + seq_dir=seq_dir, + level=level, + seq_fps=seq_fps, + seq_length=seq_length, + replace=replace, + ) + return SequenceUnreal() diff --git a/xrfeitoria/sequence/sequence_wrapper.pyi b/xrfeitoria/sequence/sequence_wrapper.pyi index 21207023..d4a6075d 100644 --- a/xrfeitoria/sequence/sequence_wrapper.pyi +++ b/xrfeitoria/sequence/sequence_wrapper.pyi @@ -1,46 +1,18 @@ from typing import ContextManager, List, Optional, Union +from ..data_structure.constants import default_level_blender from .sequence_blender import SequenceBlender as SequenceBlender from .sequence_unreal import SequenceUnreal as SequenceUnreal -class SequenceWrapperBase: +class SequenceWrapperBlender: @classmethod def new( - cls, - seq_name: str, - level: Union[str, List[str]] = ..., - seq_fps: int = ..., - seq_length: int = ..., - replace: bool = ..., - ) -> ...: ... - @classmethod - def open(cls, seq_name: str) -> ...: ... - -class SequenceWrapperBlender(SequenceWrapperBase): - @classmethod - def new( - cls, - seq_name: str, - level: str = ..., - seq_fps: int = ..., - seq_length: int = ..., - replace: bool = ..., - ) -> ContextManager[SequenceBlender]: - """Create a new sequence and close the sequence after exiting the it. - - Args: - seq_name (str): Name of the sequence. - level (Optional[str], optional): Name of the level. Defaults to None. When None, use the default level named 'XRFeitoria'. - seq_fps (int, optional): Frame per second of the new sequence. Defaults to 60. - seq_length (int, optional): Frame length of the new sequence. Defaults to 1. - replace (bool, optional): Replace the exist same-name sequence. Defaults to False. - Yields: - SequenceBlender: Sequence object. - """ + cls, seq_name: str, level: str = ..., seq_fps: int = ..., seq_length: int = ..., replace: bool = ... + ) -> ContextManager[SequenceBlender]: ... @classmethod def open(cls, seq_name: str) -> ContextManager[SequenceBlender]: ... -class SequenceWrapperUnreal(SequenceWrapperBase): +class SequenceWrapperUnreal: @classmethod def new( cls, @@ -53,3 +25,19 @@ class SequenceWrapperUnreal(SequenceWrapperBase): ) -> ContextManager[SequenceUnreal]: ... @classmethod def open(cls, seq_name: str, seq_dir: Optional[str] = ...) -> ContextManager[SequenceUnreal]: ... + +def sequence_wrapper_blender( + seq_name: str, + level: str = default_level_blender, + seq_fps: int = 30, + seq_length: int = 1, + replace: bool = False, +) -> Union[SequenceBlender, ContextManager[SequenceBlender]]: ... +def sequence_wrapper_unreal( + seq_name: str, + seq_dir: Optional[str] = None, + level: Optional[str] = None, + seq_fps: int = 30, + seq_length: int = 1, + replace: bool = False, +) -> Union[SequenceUnreal, ContextManager[SequenceUnreal]]: ... diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index 97ca45b1..e377ea1e 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -300,6 +300,19 @@ def get_rotation_to_look_at(location: 'Vector', target: 'Vector') -> 'Vector': return tuple(math.degrees(r) for r in rotation) +@remote_blender() +def check_sequence(seq_name: str) -> bool: + """Check whether the sequence exists. + + Args: + seq_name (str): Name of the sequence. + + Returns: + bool: True if the sequence exists. + """ + return seq_name in bpy.data.collections.keys() + + @remote_blender() def init_scene_and_collection(name: str, cleanup: bool = False) -> None: """Init the default scene and default collection. From a188c98d50c51446f03e6c1e2d3d001419d9db75 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 12 Jan 2024 23:19:25 +0800 Subject: [PATCH 071/110] add `replace` for `import_asset` and `import_anim` --- src/XRFeitoriaUnreal/Content/Python/utils.py | 33 ++++++++++++------- .../utils/functions/unreal_functions.py | 12 ++++--- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/utils.py b/src/XRFeitoriaUnreal/Content/Python/utils.py index e1f3b575..027835ba 100644 --- a/src/XRFeitoriaUnreal/Content/Python/utils.py +++ b/src/XRFeitoriaUnreal/Content/Python/utils.py @@ -68,13 +68,16 @@ def wrap_func(*args, **kwargs): # assets -def import_asset(path: Union[str, List[str]], dst_dir_in_engine: Optional[str] = None) -> List[str]: +def import_asset( + path: Union[str, List[str]], dst_dir_in_engine: Optional[str] = None, replace: bool = True +) -> List[str]: """Import assets to the default asset path. Args: path (Union[str, List[str]]): a file path or a list of file paths to import, e.g. "D:/assets/SMPL_XL.fbx" dst_dir_in_engine (str, optional): destination directory in the engine. Defaults to None falls back to DEFAULT_ASSET_PATH. + replace (bool, optional): whether to replace the existing asset. Defaults to True. Returns: List[str]: a list of paths to the imported assets, e.g. ["/Game/XRFeitoriaUnreal/Assets/SMPL_XL"] @@ -92,7 +95,7 @@ def import_asset(path: Union[str, List[str]], dst_dir_in_engine: Optional[str] = name = Path(path).stem dst_dir = unreal.Paths.combine([dst_dir_in_engine, name]) dst_path = unreal.Paths.combine([dst_dir, name]) # check if asset exists - if unreal.EditorAssetLibrary.does_asset_exist(dst_path): + if unreal.EditorAssetLibrary.does_asset_exist(dst_path) and not replace: asset_paths.append(dst_path) continue @@ -113,7 +116,7 @@ def import_asset(path: Union[str, List[str]], dst_dir_in_engine: Optional[str] = import_task.set_editor_property('destination_name', '') import_task.set_editor_property('destination_path', dst_dir) import_task.set_editor_property('filename', path) - import_task.set_editor_property('replace_existing', True) + import_task.set_editor_property('replace_existing', replace) import_task.set_editor_property('options', import_options) import_tasks = [import_task] @@ -125,13 +128,14 @@ def import_asset(path: Union[str, List[str]], dst_dir_in_engine: Optional[str] = return asset_paths -def import_anim(path: str, skeleton_path: str, dest_path: Optional[str] = None) -> List[str]: +def import_anim(path: str, skeleton_path: str, dst_dir: Optional[str] = None, replace: bool = True) -> List[str]: """Import animation to the default asset path. Args: path (str): a file path to import, e.g. "D:/assets/SMPL_XL.fbx" skeleton_path (str): a path to the skeleton, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL" - dest_path (str, optional): destination directory in the engine. Defaults to None falls back to {skeleton_path.parent}/Animation. + dst_dir (str, optional): destination directory in the engine. Defaults to None falls back to {skeleton_path.parent}/Animation. + replace (bool, optional): whether to replace the existing asset. Defaults to True. Returns: str: a path to the imported animation, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL" @@ -140,17 +144,24 @@ def import_anim(path: str, skeleton_path: str, dest_path: Optional[str] = None) # init task import_task = unreal.AssetImportTask() import_task.set_editor_property('filename', path) + # set destination path to {skeleton_path}/Animation - if dest_path is None: - dest_path = unreal.Paths.combine([unreal.Paths.get_path(skeleton_path), 'Animation']) - import_task.set_editor_property('destination_path', dest_path) - import_task.set_editor_property('replace_existing', True) - import_task.set_editor_property('replace_existing_settings', True) + if dst_dir is None: + dst_dir = unreal.Paths.combine([unreal.Paths.get_path(skeleton_path), 'Animation']) + dst_path = unreal.Paths.combine([dst_dir, Path(path).stem]) + # check if asset exists + if unreal.EditorAssetLibrary.does_asset_exist(dst_path) and not replace: + return [dst_path] + + import_task.set_editor_property('destination_path', dst_dir) + import_task.set_editor_property('replace_existing', replace) + import_task.set_editor_property('replace_existing_settings', replace) import_task.set_editor_property('automated', True) # options for importing animation options = unreal.FbxImportUI() options.mesh_type_to_import = unreal.FBXImportType.FBXIT_ANIMATION options.skeleton = unreal.load_asset(skeleton_path) + options.import_animations = True import_data = unreal.FbxAnimSequenceImportData() import_data.set_editor_properties( { @@ -164,7 +175,7 @@ def import_anim(path: str, skeleton_path: str, dest_path: Optional[str] = None) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([import_task]) # save assets - unreal.EditorAssetLibrary.save_directory(dest_path, False, True) + unreal.EditorAssetLibrary.save_directory(dst_dir, False, True) # return paths return [path.split('.')[0] for path in import_task.get_editor_property('imported_object_paths')] diff --git a/xrfeitoria/utils/functions/unreal_functions.py b/xrfeitoria/utils/functions/unreal_functions.py index 6433ec36..1e226fb3 100644 --- a/xrfeitoria/utils/functions/unreal_functions.py +++ b/xrfeitoria/utils/functions/unreal_functions.py @@ -69,36 +69,40 @@ def save_current_level(asset_path: 'Optional[str]' = None) -> None: @remote_unreal() -def import_asset(path: 'Union[str, List[str]]', dst_dir_in_engine: 'Optional[str]' = None) -> 'Union[str, List[str]]': +def import_asset( + path: 'Union[str, List[str]]', dst_dir_in_engine: 'Optional[str]' = None, replace: bool = True +) -> 'Union[str, List[str]]': """Import assets to the default asset path. Args: path (Union[str, List[str]]): a file path or a list of file paths to import, e.g. "D:/assets/SMPL_XL.fbx" dst_dir_in_engine (Optional[str], optional): destination directory in the engine. Defaults to None falls back to '/Game/XRFeitoriaUnreal/Assets' + replace (bool, optional): whether to replace the existing asset. Defaults to True. Returns: Union[str, List[str]]: a path or a list of paths to the imported assets, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL" """ - paths = XRFeitoriaUnrealFactory.utils.import_asset(path, dst_dir_in_engine) + paths = XRFeitoriaUnrealFactory.utils.import_asset(path, dst_dir_in_engine, replace=replace) if len(paths) == 1: return paths[0] return paths @remote_unreal() -def import_anim(path: str, skeleton_path: str, dest_path: 'Optional[str]' = None) -> 'List[str]': +def import_anim(path: str, skeleton_path: str, dest_path: 'Optional[str]' = None, replace: bool = True) -> 'List[str]': """Import animation to the default asset path. Args: path (str): The file path to import, e.g. "D:/assets/SMPL_XL-Animation.fbx". skeleton_path (str): The path to the skeleton, e.g. "/Game/XRFeitoriaUnreal/Assets/SMPL_XL_Skeleton". dest_path (str, optional): The destination directory in the engine. Defaults to None, falls back to {skeleton_path.parent}/Animation. + replace (bool, optional): whether to replace the existing asset. Defaults to True. Returns: List[str]: A list of paths to the imported animations, e.g. ["/Game/XRFeitoriaUnreal/Assets/SMPL_XL-Animation"]. """ - return XRFeitoriaUnrealFactory.utils.import_anim(path, skeleton_path, dest_path) + return XRFeitoriaUnrealFactory.utils.import_anim(path, skeleton_path, dest_path, replace=replace) @remote_unreal() From e2d1db107843b6251beb9d265589956a737d1991 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 12 Jan 2024 23:19:41 +0800 Subject: [PATCH 072/110] [Fix] misc bugs --- .../Content/Python/sequence.py | 26 +++++++++++++++---- xrfeitoria/renderer/renderer_unreal.py | 11 ++++---- xrfeitoria/sequence/sequence_blender.py | 3 +-- xrfeitoria/sequence/sequence_unreal.pyi | 20 ++++++-------- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index fdc414ce..75ef8c7d 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -30,6 +30,20 @@ def duplicate_binding(binding: unreal.SequencerBindingProxy) -> None: # TODO: the event track would be lost after pasting, need to fix it +def get_binding_id(binding: unreal.SequencerBindingProxy) -> unreal.MovieSceneObjectBindingID: + """Get the MovieSceneObjectBindingID from a SequencerBindingProxy. + + Args: + binding (unreal.SequencerBindingProxy): The SequencerBindingProxy object. + + Returns: + unreal.MovieSceneObjectBindingID: The MovieSceneObjectBindingID extracted from the binding. + """ + binding_id = unreal.MovieSceneObjectBindingID() + binding_id.set_editor_property('Guid', binding.binding_id) + return binding_id + + def convert_frame_rate_to_fps(frame_rate: unreal.FrameRate) -> float: return frame_rate.numerator / frame_rate.denominator @@ -246,7 +260,7 @@ def add_property_bool_track_to_binding( bool_section.set_end_frame_bounded(0) # set key - for channel in bool_section.find_channels_by_type(unreal.MovieSceneScriptingBoolChannel): + for channel in bool_section.get_channels_by_type(unreal.MovieSceneScriptingBoolChannel): channel.set_default(property_value) return bool_track, bool_section @@ -267,7 +281,7 @@ def add_property_int_track_to_binding( int_section.set_end_frame_bounded(0) # set key - for channel in int_section.find_channels_by_type(unreal.MovieSceneScriptingIntegerChannel): + for channel in int_section.get_channels_by_type(unreal.MovieSceneScriptingIntegerChannel): channel.set_default(property_value) return int_track, int_section @@ -288,7 +302,7 @@ def add_property_string_track_to_binding( string_section.set_end_frame_bounded(0) # set key - for channel in string_section.find_channels_by_type(unreal.MovieSceneScriptingStringChannel): + for channel in string_section.get_channels_by_type(unreal.MovieSceneScriptingStringChannel): channel: unreal.MovieSceneScriptingStringChannel if isinstance(property_value, str): channel.set_default(property_value) @@ -544,7 +558,8 @@ def add_camera_to_sequence( camera_cut_section.set_end_frame(seq_length) # set the camera cut to use this camera - camera_cut_section.set_camera_binding_id(camera_binding.get_binding_id()) + # camera_cut_section.set_camera_binding_id(camera_binding.get_binding_id()) + camera_cut_section.set_camera_binding_id(get_binding_id(camera_binding)) # ------- add transform track ------- # transform_track, transform_section = add_or_find_transform_track_to_binding(camera_binding) @@ -622,7 +637,8 @@ def add_spawnable_camera_to_sequence( # camera_binding_id = sequence.make_binding_id(camera_binding, unreal.MovieSceneObjectBindingSpace.LOCAL) # camera_cut_section.set_camera_binding_id(camera_binding_id) - camera_cut_section.set_camera_binding_id(camera_binding.get_binding_id()) + # camera_cut_section.set_camera_binding_id(camera_binding.get_binding_id()) + camera_cut_section.set_camera_binding_id(get_binding_id(camera_binding)) # ------- add transform track ------- # transform_track, transform_section = add_or_find_transform_track_to_binding(camera_binding) diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 8d00fe74..f4737e3a 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -18,7 +18,8 @@ pass try: - from ..data_structure.models import RenderJobUnreal, RenderPass + from ..data_structure.models import RenderJobUnreal as RenderJob + from ..data_structure.models import RenderPass except ModuleNotFoundError: pass @@ -27,7 +28,7 @@ class RendererUnreal(RendererBase): """Renderer class for Unreal.""" - render_queue: 'List[RenderJobUnreal]' = [] + render_queue: 'List[RenderJob]' = [] @classmethod def add_job( @@ -39,7 +40,7 @@ def add_job( render_passes: 'List[RenderPass]', file_name_format: str = '{sequence_name}/{render_pass}/{camera_name}/{frame_number}', console_variables: Dict[str, float] = {'r.MotionBlurQuality': 0}, - anti_aliasing: 'Optional[RenderJobUnreal.AntiAliasSetting]' = None, + anti_aliasing: 'Optional[RenderJob.AntiAliasSetting]' = None, export_vertices: bool = False, export_skeleton: bool = False, ) -> None: @@ -62,7 +63,7 @@ def add_job( The motion blur is turned off by default. If you want to turn it on, please set ``r.MotionBlurQuality`` to a non-zero value in ``console_variables``. """ if anti_aliasing is None: - anti_aliasing = RenderJobUnreal.AntiAliasSetting() + anti_aliasing = RenderJob.AntiAliasSetting() # turn off motion blur by default if 'r.MotionBlurQuality' not in console_variables.keys(): @@ -72,7 +73,7 @@ def add_job( "If you want to turn off the motion blur the same as default, set ``console_variables={..., 'r.MotionBlurQuality': 0}``." ) - job = RenderJobUnreal( + job = RenderJob( map_path=map_path, sequence_path=sequence_path, output_path=Path(output_path).resolve(), diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index 49ab5ce8..a88cdce3 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -5,7 +5,6 @@ from ..actor.actor_blender import ActorBlender, ShapeBlenderWrapper from ..camera.camera_blender import CameraBlender from ..data_structure.constants import PathLike, Vector -from ..data_structure.models import RenderPass from ..object.object_utils import ObjectUtilsBlender from ..renderer.renderer_blender import RendererBlender from ..rpc import remote_blender @@ -13,7 +12,7 @@ try: import bpy # isort:skip - from ..data_structure.models import TransformKeys # isort:skip + from ..data_structure.models import RenderPass, TransformKeys # isort:skip from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py except ModuleNotFoundError: pass diff --git a/xrfeitoria/sequence/sequence_unreal.pyi b/xrfeitoria/sequence/sequence_unreal.pyi index a5010b54..1468b86e 100644 --- a/xrfeitoria/sequence/sequence_unreal.pyi +++ b/xrfeitoria/sequence/sequence_unreal.pyi @@ -1,17 +1,13 @@ from typing import Dict, List, Optional, Tuple -from ..actor.actor_unreal import ActorUnreal as ActorUnreal -from ..camera.camera_unreal import CameraUnreal as CameraUnreal -from ..data_structure.constants import PathLike as PathLike -from ..data_structure.constants import Vector as Vector -from ..data_structure.models import RenderJobUnreal as RenderJobUnreal -from ..data_structure.models import RenderPass as RenderPass -from ..data_structure.models import TransformKeys as TransformKeys -from ..object.object_utils import ObjectUtilsUnreal as ObjectUtilsUnreal -from ..renderer.renderer_unreal import RendererUnreal as RendererUnreal -from ..rpc import remote_unreal as remote_unreal -from ..utils.functions import unreal_functions as unreal_functions -from .sequence_base import SequenceBase as SequenceBase +from ..actor.actor_unreal import ActorUnreal +from ..camera.camera_unreal import CameraUnreal +from ..data_structure.constants import PathLike, Vector +from ..data_structure.models import RenderJobUnreal, RenderPass, TransformKeys +from ..object.object_utils import ObjectUtilsUnreal +from ..renderer.renderer_unreal import RendererUnreal +from ..utils.functions import unreal_functions +from .sequence_base import SequenceBase class SequenceUnreal(SequenceBase): @classmethod From 7af4669dde322f099c4a809848b7560c8275dd00 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Sat, 13 Jan 2024 14:51:35 +0800 Subject: [PATCH 073/110] [Fix] use `_blank` in href --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 422602bf..6818a79c 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,12 @@ Please follow the instructions [here](/samples/README.md). | Project | Teaser | Engine | | :---: | :---: | :---: | -| [SynBody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | -| [Zolly: Zoom Focal Length Correctly for Perspective-Distorted Human Mesh Reconstruction](https://wenjiawang0312.github.io/projects/zolly/) | | Blender | -| [SHERF: Generalizable Human NeRF from a Single Image](https://skhu101.github.io/SHERF/) | | Blender | -| [MatrixCity: A Large-scale City Dataset for City-scale Neural Rendering and Beyond](https://city-super.github.io/matrixcity/) | | Unreal Engine | -| [HumanLiff: Layer-wise 3D Human Generation with Diffusion Model](https://skhu101.github.io/HumanLiff/) | | Blender | -| [PrimDiffusion: Volumetric Primitives Diffusion for 3D Human Generation](https://frozenburning.github.io/projects/primdiffusion/) | | Blender | +| [SynBody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | +| [Zolly: Zoom Focal Length Correctly for Perspective-Distorted Human Mesh Reconstruction](https://wenjiawang0312.github.io/projects/zolly/) | | Blender | +| [SHERF: Generalizable Human NeRF from a Single Image](https://skhu101.github.io/SHERF/) | | Blender | +| [MatrixCity: A Large-scale City Dataset for City-scale Neural Rendering and Beyond](https://city-super.github.io/matrixcity/) | | Unreal Engine | +| [HumanLiff: Layer-wise 3D Human Generation with Diffusion Model](https://skhu101.github.io/HumanLiff/) | | Blender | +| [PrimDiffusion: Volumetric Primitives Diffusion for 3D Human Generation](https://frozenburning.github.io/projects/primdiffusion/) | | Blender | ## License From dfdb6ed74ab80e47371bc6ac7b7596eaa077e123 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Sat, 13 Jan 2024 14:56:38 +0800 Subject: [PATCH 074/110] [Fix] Get rid of `_blank` --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6818a79c..422602bf 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,12 @@ Please follow the instructions [here](/samples/README.md). | Project | Teaser | Engine | | :---: | :---: | :---: | -| [SynBody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | -| [Zolly: Zoom Focal Length Correctly for Perspective-Distorted Human Mesh Reconstruction](https://wenjiawang0312.github.io/projects/zolly/) | | Blender | -| [SHERF: Generalizable Human NeRF from a Single Image](https://skhu101.github.io/SHERF/) | | Blender | -| [MatrixCity: A Large-scale City Dataset for City-scale Neural Rendering and Beyond](https://city-super.github.io/matrixcity/) | | Unreal Engine | -| [HumanLiff: Layer-wise 3D Human Generation with Diffusion Model](https://skhu101.github.io/HumanLiff/) | | Blender | -| [PrimDiffusion: Volumetric Primitives Diffusion for 3D Human Generation](https://frozenburning.github.io/projects/primdiffusion/) | | Blender | +| [SynBody: Synthetic Dataset with Layered Human Models for 3D Human Perception and Modeling](https://synbody.github.io/) | | Unreal Engine / Blender | +| [Zolly: Zoom Focal Length Correctly for Perspective-Distorted Human Mesh Reconstruction](https://wenjiawang0312.github.io/projects/zolly/) | | Blender | +| [SHERF: Generalizable Human NeRF from a Single Image](https://skhu101.github.io/SHERF/) | | Blender | +| [MatrixCity: A Large-scale City Dataset for City-scale Neural Rendering and Beyond](https://city-super.github.io/matrixcity/) | | Unreal Engine | +| [HumanLiff: Layer-wise 3D Human Generation with Diffusion Model](https://skhu101.github.io/HumanLiff/) | | Blender | +| [PrimDiffusion: Volumetric Primitives Diffusion for 3D Human Generation](https://frozenburning.github.io/projects/primdiffusion/) | | Blender | ## License From 66ffd3f6ce293d6218862f982670cb7b5925bb70 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Sat, 13 Jan 2024 18:23:45 +0800 Subject: [PATCH 075/110] Add GitHub corner and update HTML context --- docs/en/_templates/layout.html | 113 +++++++++++++++++++++++++++++++++ docs/en/conf.py | 4 ++ 2 files changed, 117 insertions(+) create mode 100644 docs/en/_templates/layout.html diff --git a/docs/en/_templates/layout.html b/docs/en/_templates/layout.html new file mode 100644 index 00000000..0697e716 --- /dev/null +++ b/docs/en/_templates/layout.html @@ -0,0 +1,113 @@ + +{% extends '!layout.html' %} +{% block document %} +{{super()}} + +
+ + + + + + + + + CLICK + + + + + + + + + + + + + + +
+ + +{% endblock %} diff --git a/docs/en/conf.py b/docs/en/conf.py index 53ae319b..eacbd19f 100644 --- a/docs/en/conf.py +++ b/docs/en/conf.py @@ -131,6 +131,10 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # +html_context = { + 'github_user': 'openxrlab', + 'github_repo': 'xrfeitoria', +} html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] html_css_files = ['override.css'] # override py property From 15eae4a0bd9a3da51a6a08367ebef510034f3528 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 15 Jan 2024 15:56:09 +0800 Subject: [PATCH 076/110] [Fix] logger msg --- xrfeitoria/utils/runner.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 9ddf9adf..2258765e 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -267,13 +267,14 @@ def check_engine_alive(self) -> None: Raises: RuntimeError: if engine process is not alive. """ + logger.debug('(Thread) Start checking engine process') while self.engine_running: if self.engine_process.poll() is not None: logger.error(self.get_process_output(self.engine_process)) logger.error('[red]RPC server stopped unexpectedly, check the engine output above[/red]') factory.RPCFactory.clear() raise RuntimeError('RPC server stopped unexpectedly') - time.sleep(1) + time.sleep(5) def check_engine_alive_psutil(self) -> None: """Check if the engine process is alive using psutil. This function should be @@ -282,13 +283,14 @@ def check_engine_alive_psutil(self) -> None: Raises: RuntimeError: if engine process is not alive. """ + logger.debug('(Thread) Start checking engine process, using psutil') p = psutil.Process(self.engine_pid) while self.engine_running: if not p.is_running(): - logger.error('[red]RPC server stopped unexpectedly, check the engine output above[/red]') + logger.error('[red]RPC server stopped unexpectedly[/red]') factory.RPCFactory.clear() raise RuntimeError('RPC server stopped unexpectedly') - time.sleep(1) + time.sleep(5) def get_process_output(self, process: subprocess.Popen) -> str: """Get process output when process is exited with non-zero code.""" @@ -352,7 +354,7 @@ def wait_for_start(self, process: subprocess.Popen) -> None: self.test_connection(debug=self.debug) break except (RemoteDisconnected, ConnectionRefusedError, ProtocolError): - logger.debug(f'Waiting for RPC server to start [tryout {_num}/{tryout_num}]') + logger.debug(f'Waiting for RPC server to start (tryout {_num}/{tryout_num})') _num += 1 time.sleep(tryout_sec) # wait for 5 seconds if _num >= tryout_num: # 3 minutes From c7371756bce7b2b9b8338ea16ed17b28aca67750 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 16 Jan 2024 22:51:28 +0800 Subject: [PATCH 077/110] Update: - optimize `publish_plugins.py`, it's can be used by a CI/CD machine - accomplish plugin_version check - add more instructions --- docs/en/faq.rst | 33 ++++ src/XRFeitoriaBpy/__init__.py | 2 +- src/XRFeitoriaUnreal/Config/FilterPlugin.ini | 8 + src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin | 4 +- xrfeitoria/data_structure/constants.py | 1 + xrfeitoria/utils/plugin_infos.json | 5 + xrfeitoria/utils/publish_plugins.py | 183 ++++++++++++------ xrfeitoria/utils/runner.py | 103 ++++++++-- 8 files changed, 262 insertions(+), 77 deletions(-) create mode 100644 src/XRFeitoriaUnreal/Config/FilterPlugin.ini diff --git a/docs/en/faq.rst b/docs/en/faq.rst index 927043e6..2940795a 100644 --- a/docs/en/faq.rst +++ b/docs/en/faq.rst @@ -12,6 +12,39 @@ API .. _FAQ-stencil-value: +How to use the plugin of Blender/Unreal under development +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +First you should clone the repo of XRFeitoria, and maybe modify the code of the plugin under ``src/XRFeitoriaBlender`` or ``src/XRFeitoriaUnreal``. +Then you can use the plugin under development by setting ``dev_plugin=True`` in :class:`init_blender ` or :class:`init_unreal `. + +You can install the plugin by: + +.. tabs:: + .. tab:: Blender + .. code-block:: bash + :linenos: + + git clone https://github.com/openxrlab/xrfeitoria.git + cd xrfeitoria + pip install -e . + python -c "import xrfeitoria as xf; xf.init_blender(replace_plugin=True, dev_plugin=True)" + + # or through the code in tests + python -m tests.blender.init --dev [-b] + + .. tab:: Unreal + .. code-block:: bash + :linenos: + + git clone https://github.com/openxrlab/xrfeitoria.git + cd xrfeitoria + pip install -e . + python -c "import xrfeitoria as xf; xf.init_unreal(replace_plugin=True, dev_plugin=True)" + + # or through the code in tests + python -m tests.unreal.init --dev [-b] + What is ``stencil_value`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/XRFeitoriaBpy/__init__.py b/src/XRFeitoriaBpy/__init__.py index 5f7745a9..ff22e14c 100644 --- a/src/XRFeitoriaBpy/__init__.py +++ b/src/XRFeitoriaBpy/__init__.py @@ -4,7 +4,7 @@ bl_info = { 'name': 'XRFeitoriaBpy', 'author': 'OpenXRLab', - 'version': (0, 5, 1), + 'version': (0, 6, 0), 'blender': (3, 3, 0), 'category': 'Tools', } diff --git a/src/XRFeitoriaUnreal/Config/FilterPlugin.ini b/src/XRFeitoriaUnreal/Config/FilterPlugin.ini new file mode 100644 index 00000000..ccebca2f --- /dev/null +++ b/src/XRFeitoriaUnreal/Config/FilterPlugin.ini @@ -0,0 +1,8 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll diff --git a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin index bbbdc06b..f19539ac 100644 --- a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin +++ b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, "Version": 1, - "VersionName": "0.5.1", + "VersionName": "0.6.0", "FriendlyName": "XRFeitoriaUnreal", "Description": "OpenXRLab Synthetic Data Rendering Toolbox", "Category": "Scripting", @@ -17,7 +17,7 @@ "Modules": [ { "Name": "XRFeitoriaUnreal", - "Type": "Runtime", + "Type": "Editor", "LoadingPhase": "Default" } ], diff --git a/xrfeitoria/data_structure/constants.py b/xrfeitoria/data_structure/constants.py index f2c129a7..1ff26e86 100644 --- a/xrfeitoria/data_structure/constants.py +++ b/xrfeitoria/data_structure/constants.py @@ -15,6 +15,7 @@ package_name = 'XRFeitoria' plugin_name_blender = 'XRFeitoriaBpy' plugin_name_unreal = 'XRFeitoriaUnreal' +plugin_name_pattern = '{plugin_name}-{plugin_version}-{engine_version}-{platform}' xf_obj_name = '[XF]{obj_type}-{obj_idx:03d}' ##### Path Constants ##### diff --git a/xrfeitoria/utils/plugin_infos.json b/xrfeitoria/utils/plugin_infos.json index 563bf95d..329bc044 100644 --- a/xrfeitoria/utils/plugin_infos.json +++ b/xrfeitoria/utils/plugin_infos.json @@ -1,4 +1,9 @@ { + "0.6.0": { + "XRFeitoria": "0.6.0", + "XRFeitoriaBpy": "0.6.0", + "XRFeitoriaUnreal": "0.6.0" + }, "0.5.1": { "XRFeitoria": "0.5.1", "XRFeitoriaBpy": "0.5.1", diff --git a/xrfeitoria/utils/publish_plugins.py b/xrfeitoria/utils/publish_plugins.py index d3e8e5b1..8875d33d 100644 --- a/xrfeitoria/utils/publish_plugins.py +++ b/xrfeitoria/utils/publish_plugins.py @@ -1,98 +1,173 @@ """Publish plugins to zip files. - ->>> python -m xrfeitoria.utils.publish_plugins --unreal -s 0.5.0-Unreal5.2-Windows ->>> python -m xrfeitoria.utils.publish_plugins --blender -s 0.5.0-None-None +>>> python -m xrfeitoria.utils.publish_plugins --help """ +import os +import platform +import re +import subprocess +from contextlib import contextmanager from pathlib import Path -from typing import Literal, Optional +from typing import List, Optional from loguru import logger -from .. import __version__ -from ..data_structure.constants import PathLike, plugin_name_blender, plugin_name_unreal +from ..data_structure.constants import plugin_name_blender, plugin_name_pattern, plugin_name_unreal +from ..utils import setup_logger +from ..version import __version__, __version_tuple__ +from .runner import UnrealRPCRunner root = Path(__file__).parent.resolve() project_root = root.parents[1] +src_root = project_root / 'src' +dist_root = src_root / 'dist' + + +@contextmanager +def working_directory(path): + """Changes working directory and returns to previous on exit.""" + prev_cwd = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev_cwd) def _make_archive( - plugin_folder: PathLike, - zip_name: Optional[str] = None, - folder_name: Optional[str] = None, + src_folder: Path, + dst_path: Optional[Path] = None, + folder_name_inside_zip: Optional[str] = None, ) -> Path: """Make archive of plugin folder. + Zip Plugin folder to ``{plugin_folder.parent}/{zip_name}.zip``. + Args: - plugin_folder (PathLike): path to plugin folder. - zip_name (Optional[str], optional): name of the archive file. - E.g. dst_name='plugin', the archive file would be ``plugin.zip``. + plugin_folder (Path): path to plugin folder. + zip_name (Optional[str], optional): name of the archive file. E.g. dst_name='plugin', the archive file would be ``plugin.zip``. Defaults to None, fallback to {plugin_folder.name}. + folder_name (Optional[str], optional): name of the root folder in the archive. """ import zipfile - if zip_name is None: - zip_name = plugin_folder.name - if folder_name is None: - folder_name = zip_name + if dst_path is None: + dst_path = src_folder.parent / f'{src_folder.name}.zip' + if folder_name_inside_zip is None: + folder_name_inside_zip = dst_path.stem - plugin_folder = Path(plugin_folder).resolve() - plugin_zip = plugin_folder.parent / f'{zip_name}.zip' - if plugin_zip.exists(): - plugin_zip.unlink() + if dst_path.exists(): + dst_path.unlink() filter_names = ['.git', '.idea', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate'] - with zipfile.ZipFile(plugin_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: - for file in plugin_folder.rglob('*'): + with zipfile.ZipFile(dst_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: + for file in src_folder.rglob('*'): # filter if any([folder in file.parts for folder in filter_names]): continue # in zip, the folder name is the root folder - # {folder_name}/a/b/c - arcname = folder_name + '/' + file.relative_to(plugin_folder).as_posix() + # {folder_name_inside_zip}/a/b/c + arcname = folder_name_inside_zip + '/' + file.relative_to(src_folder).as_posix() zipf.write(file, arcname=arcname) - # plugin_folder = shutil.make_archive(plugin_zip.with_suffix(''), 'zip', plugin_folder.parent, plugin_folder.name) - logger.debug(f'Compressed {plugin_folder} => {plugin_zip}') - return plugin_zip + logger.info(f'Compressed {src_folder} => {dst_path}') + return dst_path + +def update_bpy_version(bpy_init_file: Path): + """Update version in ``src/XRFeitoriaBpy/__init__.py``. + + Args: + bpy_init_file (Path): path to ``__init__.py`` file. + """ + content = bpy_init_file.read_text() + # update version + content = re.sub(pattern=r"'version': \(.*\)", repl=f"'version': {__version_tuple__}", string=content) + bpy_init_file.write_text(content) + logger.info(f'Updated "{bpy_init_file}" with version {__version__}') -def main(engine: Literal['unreal', 'blender'], suffix: Optional[str] = None): - if engine == 'blender': - plugin_name = plugin_name_blender - folder_name = plugin_name # in zip, {XRFeitoriaBpy} is the root folder - elif engine == 'unreal': - # TODO: auto detect binaries - plugin_name = plugin_name_unreal - folder_name = None # in zip, {XRFeitoriaUnreal-x.x.x-UE5.x-Windows} is the root folder - dir_plugin = project_root / 'src' / plugin_name +def update_uplugin_version(uplugin_path: Path): + """Update version in ``src/XRFeitoriaUnreal/XRFeitoria.uplugin``. - name = plugin_name - if suffix is not None: - name += f'-{suffix}' - else: - name += f'-{__version__}' - plugin_zip = _make_archive(dir_plugin, zip_name=name, folder_name=folder_name) - logger.info(f'Plugin for {engine}: {plugin_zip}') + Args: + uplugin_file (Path): path to ``XRFeitoria.uplugin`` file. + """ + content = uplugin_path.read_text() + # update version + content = re.sub(pattern=r'"VersionName": ".*"', repl=f'"VersionName": "{__version__}"', string=content) + uplugin_path.write_text(content) + logger.info(f'Updated "{uplugin_path}" with version {__version__}') + + +def build_blender(): + plugin_name = plugin_name_pattern.format( + plugin_name=plugin_name_blender, + plugin_version=__version__, + engine_version='None', + platform='None', + ) # e.g. XRFeitoriaBlender-0.5.0-None-None + dir_plugin = src_root / plugin_name_blender + update_bpy_version(dir_plugin / '__init__.py') + + plugin_zip = _make_archive( + src_folder=dir_plugin, + dst_path=dist_root / f'{plugin_name}.zip', + folder_name_inside_zip=plugin_name_blender, + ) + dst_plugin_zip = dist_root / plugin_zip.name + logger.info(f'Plugin for blender: "{dst_plugin_zip}"') + + +def build_unreal(unreal_exec_list: List[Path]): + dir_plugin = src_root / plugin_name_unreal + uplugin_path = dir_plugin / f'{plugin_name_unreal}.uplugin' + update_uplugin_version(uplugin_path) + logger.info(f'Compiling plugin for Unreal Engine...') + for unreal_exec in unreal_exec_list: + uat_path = unreal_exec.parents[2] / 'Build/BatchFiles/RunUAT.bat' + unreal_infos = UnrealRPCRunner._get_engine_info(unreal_exec) + engine_version = ''.join(unreal_infos) # e.g. Unreal5.1 + plugin_name = plugin_name_pattern.format( + plugin_name=plugin_name_unreal, + plugin_version=__version__, + engine_version=engine_version, + platform=platform.system(), + ) # e.g. XRFeitoriaUnreal-0.5.0-None-Windows + dist_path = dist_root / plugin_name + subprocess.call([uat_path, 'BuildPlugin', f'-Plugin={uplugin_path}', f'-Package={dist_path}']) + _make_archive(src_folder=dist_path) + logger.info(f'Plugin for {engine_version}: "{dist_path}.zip"') if __name__ == '__main__': from typer import Option, run def wrapper( - unreal: bool = Option(False, '--unreal', '-u', help='Build plugin for Unreal'), - blender: bool = Option(False, '--blender', '-b', help='Build plugin for Blender'), - suffix: str = Option( + unreal_exec: List[Path] = Option( None, - '--suffix', - '-s', - help='Suffix of the compressed file, e.g. "XRFeitoriaUnreal-{suffix}.zip". if None, use version number', - ), + '-u', + resolve_path=True, + file_okay=True, + dir_okay=False, + exists=True, + help='Path to Unreal Engine executable. e.g. "C:/Program Files/Epic Games/UE_5.1/Engine/Binaries/Win64/UnrealEditor-Cmd.exe"', + ) ): - if unreal: - main(engine='unreal', suffix=suffix) - if blender: - main(engine='blender', suffix=suffix) + """Publish plugins to zip files. + + Examples: + + >>> python -m xrfeitoria.utils.publish_plugins + -u "C:/Program Files/Epic Games/UE_5.1/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" + -u "C:/Program Files/Epic Games/UE_5.2/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" + -u "C:/Program Files/Epic Games/UE_5.3/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" + """ + print(unreal_exec) + setup_logger(level='INFO') + build_blender() + if len(unreal_exec) > 0: + build_unreal(unreal_exec_list=unreal_exec) + logger.info(f'Check "{dist_root}" for the plugin zip files.') run(wrapper) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 2258765e..54f9a8ee 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -1,6 +1,5 @@ """Runner for starting blender or unreal as a rpc server.""" import json -import os import platform import re import shutil @@ -13,7 +12,7 @@ from http.client import RemoteDisconnected from pathlib import Path from textwrap import dedent -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Literal, Optional, Tuple, TypedDict from urllib.error import HTTPError, URLError from xmlrpc.client import ProtocolError @@ -24,7 +23,15 @@ from rich.prompt import Confirm from .. import __version__, _tls -from ..data_structure.constants import EngineEnum, PathLike, plugin_name_blender, plugin_name_unreal, tmp_dir +from ..data_structure.constants import ( + EngineEnum, + PathLike, + package_name, + plugin_name_blender, + plugin_name_pattern, + plugin_name_unreal, + tmp_dir, +) from ..rpc import BLENDER_PORT, UNREAL_PORT, factory, remote_blender, remote_unreal from .downloader import download from .setup import get_exec_path @@ -385,7 +392,7 @@ def get_src_plugin_path(self) -> Path: @property @lru_cache def dst_plugin_dir(self) -> Path: - """Get plugin directory.""" + """Get plugin directory to install.""" if self.engine_type == EngineEnum.blender: dst_plugin_dir = _get_user_addon_path(version=self.engine_info[1]) / plugin_name_blender elif self.engine_type == EngineEnum.unreal: @@ -395,6 +402,30 @@ def dst_plugin_dir(self) -> Path: dst_plugin_dir.parent.mkdir(exist_ok=True, parents=True) return dst_plugin_dir + @property + @lru_cache + def dst_plugin_version(self) -> str: + """Get plugin version installed.""" + assert ( + self.dst_plugin_dir.exists() + ), f'Plugin not installed in "{self.dst_plugin_dir.as_posix()}", should not call this function' + + if self.engine_type == EngineEnum.blender: + init_file = self.dst_plugin_dir / '__init__.py' + _content = init_file.read_text() + _match = re.search(r"'version': (.*?),\n", _content) + if _match: + dst_plugin_version_tuple = eval(_match.group(1)) + dst_plugin_version = '.'.join(map(str, dst_plugin_version_tuple)) + else: + raise ValueError("Failed to extract plugin version from '__init__.py'") + elif self.engine_type == EngineEnum.unreal: + uplugin_file = self.dst_plugin_dir / f'{plugin_name_unreal}.uplugin' + dst_plugin_version = json.loads(uplugin_file.read_text())['VersionName'] + else: + raise NotImplementedError + return dst_plugin_version + def _download(self, url: str, dst_dir: Path) -> Path: """Check if the url is valid and download the plugin to the given directory.""" try: @@ -405,9 +436,9 @@ def _download(self, url: str, dst_dir: Path) -> Path: code=e.code, msg=( 'Failed to download plugin.\n' - f'Sorry, pre-built plugin for {"".join(self.engine_info)} in {platform.system()} is not supported.\n' - 'Set `dev_plugin=True` in init_blender/init_unreal to build the plugin from source.\n' - 'Clone the source code from https://github.com/openxrlab/xrfeitoria.git' + f'Sorry, pre-built plugin for {plugin_name_pattern.format(**self.plugin_info)} is not provided. ' + 'You can try to build the plugin from source.\n' + 'Follow the instructions here: https://xrfeitoria.readthedocs.io/en/latest/faq.html#how-to-use-the-plugin-of-blender-unreal-under-development' ), hdrs=e.hdrs, fp=e.fp, @@ -445,7 +476,19 @@ def _get_cmd(self) -> str: def _start_rpc(self, background: bool = True, project_path: Optional[Path] = '') -> subprocess.Popen: pass - def _get_plugin_url(self) -> Optional[str]: + @property + @lru_cache + def plugin_info( + self, + ) -> TypedDict( + 'PluginInfo', + { + 'plugin_name': str, + 'plugin_version': str, + 'engine_version': str, + 'platform': Literal['Windows', 'Linux', 'Darwin'], + }, + ): # plugin_infos = { "0.5.0": { "XRFeitoria": "0.5.0", "XRFeitoriaBpy": "0.5.0", "XRFeitoriaUnreal": "0.5.0" }, ... } plugin_infos: Dict[str, Dict[str, str]] = json.loads(plugin_infos_json.read_text()) plugin_versions = sorted((map(parse, plugin_infos.keys()))) @@ -461,19 +504,41 @@ def _get_plugin_url(self) -> Optional[str]: # get link if self.engine_type == EngineEnum.unreal: - _plugin_name = plugin_name_unreal - _platform = f'{"".join(self.engine_info)}-{platform.system()}' # e.g. Unreal5.1-Windows + plugin_name = plugin_name_unreal + engine_version = ''.join(self.engine_info) # e.g. Unreal5.1 + platform = platform.system() # Literal["Windows", "Linux", "Darwin"] elif self.engine_type == EngineEnum.blender: - _plugin_name = plugin_name_blender - _platform = 'None-None' - _plugin_version = plugin_infos[str(compatible_version)][_plugin_name] + plugin_name = plugin_name_blender + engine_version = 'None' # support all blender versions + platform = 'None' # support all platforms + plugin_version = plugin_infos[str(compatible_version)][plugin_name] + # e.g. XRFeitoriaBpy-0.5.0-None-None + # e.g. XRFeitoriaUnreal-0.5.0-Unreal5.1-Windows + return dict( + plugin_name=plugin_name, + plugin_version=plugin_version, + engine_version=engine_version, + platform=platform, + ) + + @property + @lru_cache + def plugin_url(self) -> Optional[str]: # e.g. https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/plugins/XRFeitoriaBpy-0.5.0-None-None.zip # e.g. https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/plugins/XRFeitoriaUnreal-0.5.0-Unreal5.1-Windows.zip - return f'{oss_root}/plugins/{_plugin_name}-{_plugin_version}-{_platform}.zip' + return f'{oss_root}/plugins/{plugin_name_pattern.format(**self.plugin_info)}.zip' def _install_plugin(self) -> None: """Install plugin.""" if self.dst_plugin_dir.exists(): + if parse(self.dst_plugin_version) < parse(self.plugin_info['plugin_version']): + self.replace_plugin = True + if parse(self.dst_plugin_version) > parse(self.plugin_info['plugin_version']) and not self.replace_plugin: + logger.warning( + f'Plugin installed in "{self.dst_plugin_dir.as_posix()}" is in version {self.dst_plugin_version}, ' + f'newer than version {self.plugin_info["plugin_version"]} which is required by {package_name}-{__version__}. ' + 'May cause unexpected errors.' + ) if not self.replace_plugin: logger.debug(f'Plugin "{self.dst_plugin_dir.as_posix()}" already exists') return @@ -534,14 +599,13 @@ def get_src_plugin_path(self) -> Path: src_plugin_dir = Path(__file__).resolve().parents[2] / 'src' / plugin_name_blender src_plugin_path = _make_archive(src_plugin_dir) else: - url = self._get_plugin_url() src_plugin_root = tmp_dir / 'plugins' - src_plugin_path = src_plugin_root / Path(url).name # with suffix (.zip) + src_plugin_path = src_plugin_root / Path(self.plugin_url).name # with suffix (.zip) if src_plugin_path.exists(): logger.debug(f'Downloaded Plugin "{src_plugin_path.as_posix()}" exists') return src_plugin_path - plugin_path = self._download(url=url, dst_dir=src_plugin_root) + plugin_path = self._download(url=self.plugin_url, dst_dir=src_plugin_root) if plugin_path != src_plugin_path: shutil.move(plugin_path, src_plugin_path) return src_plugin_path @@ -649,9 +713,8 @@ def get_src_plugin_path(self) -> Path: 'https://github.com/openxrlab/xrfeitoria.git' ) else: - url = self._get_plugin_url() src_plugin_root = tmp_dir / 'plugins' - src_plugin_compress = src_plugin_root / Path(url).name # with suffix (.zip) + src_plugin_compress = src_plugin_root / Path(self.plugin_url).name # with suffix (.zip) src_plugin_path = src_plugin_compress.with_suffix('') # without suffix (.zip) if src_plugin_path.exists(): logger.debug(f'Downloaded Plugin "{src_plugin_path.as_posix()}" exists') @@ -662,7 +725,7 @@ def get_src_plugin_path(self) -> Path: assert src_plugin_path.exists(), f'Failed to unzip {src_plugin_compress} to {src_plugin_path}' return src_plugin_path - plugin_compress = self._download(url=url, dst_dir=src_plugin_root) + plugin_compress = self._download(url=self.plugin_url, dst_dir=src_plugin_root) shutil.unpack_archive(plugin_compress, src_plugin_root) assert src_plugin_path.exists(), f'Failed to download plugin to {src_plugin_path}' return src_plugin_path From b6de86ca1f453eb86b37c6093dba4a3e41e6d72d Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Tue, 16 Jan 2024 22:51:54 +0800 Subject: [PATCH 078/110] pre-commit --- docs/en/faq.rst | 2 +- xrfeitoria/utils/publish_plugins.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en/faq.rst b/docs/en/faq.rst index 2940795a..020da560 100644 --- a/docs/en/faq.rst +++ b/docs/en/faq.rst @@ -32,7 +32,7 @@ You can install the plugin by: # or through the code in tests python -m tests.blender.init --dev [-b] - + .. tab:: Unreal .. code-block:: bash :linenos: diff --git a/xrfeitoria/utils/publish_plugins.py b/xrfeitoria/utils/publish_plugins.py index 8875d33d..3590a87f 100644 --- a/xrfeitoria/utils/publish_plugins.py +++ b/xrfeitoria/utils/publish_plugins.py @@ -1,4 +1,5 @@ """Publish plugins to zip files. + >>> python -m xrfeitoria.utils.publish_plugins --help """ import os From 44115b85cd2a1a4cb077186c2f7154b16af2f39d Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Thu, 18 Jan 2024 14:10:34 +0800 Subject: [PATCH 079/110] [Fix] --- xrfeitoria/utils/runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 54f9a8ee..77270c91 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -506,11 +506,11 @@ def plugin_info( if self.engine_type == EngineEnum.unreal: plugin_name = plugin_name_unreal engine_version = ''.join(self.engine_info) # e.g. Unreal5.1 - platform = platform.system() # Literal["Windows", "Linux", "Darwin"] + _platform = platform.system() # Literal["Windows", "Linux", "Darwin"] elif self.engine_type == EngineEnum.blender: plugin_name = plugin_name_blender engine_version = 'None' # support all blender versions - platform = 'None' # support all platforms + _platform = 'None' # support all platforms plugin_version = plugin_infos[str(compatible_version)][plugin_name] # e.g. XRFeitoriaBpy-0.5.0-None-None # e.g. XRFeitoriaUnreal-0.5.0-Unreal5.1-Windows @@ -518,7 +518,7 @@ def plugin_info( plugin_name=plugin_name, plugin_version=plugin_version, engine_version=engine_version, - platform=platform, + platform=_platform, ) @property From 919aa41f6891c2db79f26f89344c8dd17dd0f2d8 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 18 Jan 2024 17:29:38 +0800 Subject: [PATCH 080/110] - Add export_audio option to RendererUnreal - Optimize Docs for motion - Add support for using face FK (curve) --- docs/en/apis/utils.rst | 10 ++ .../Content/Python/constants.py | 1 + .../Content/Python/custom_movie_pipeline.py | 10 ++ .../Content/Python/sequence.py | 101 +++++++++--- src/XRFeitoriaUnreal/Content/Python/utils.py | 49 +++--- src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin | 4 + xrfeitoria/actor/actor_base.py | 5 +- xrfeitoria/data_structure/models.py | 1 + xrfeitoria/material/material_base.py | 7 +- xrfeitoria/renderer/renderer_unreal.py | 3 + xrfeitoria/sequence/sequence_base.py | 3 +- xrfeitoria/sequence/sequence_unreal.py | 52 ++++-- xrfeitoria/sequence/sequence_unreal.pyi | 11 +- xrfeitoria/sequence/sequence_wrapper.py | 3 + xrfeitoria/utils/anim/motion.py | 156 ++++++++++-------- xrfeitoria/utils/anim/transform3d.py | 52 +++--- xrfeitoria/utils/anim/utils.py | 53 +++--- xrfeitoria/utils/runner.py | 21 ++- 18 files changed, 340 insertions(+), 202 deletions(-) diff --git a/docs/en/apis/utils.rst b/docs/en/apis/utils.rst index 952b5d70..8b8c4300 100644 --- a/docs/en/apis/utils.rst +++ b/docs/en/apis/utils.rst @@ -11,6 +11,16 @@ Remote Functions xrfeitoria.utils.functions.blender_functions xrfeitoria.utils.functions.unreal_functions +Animation utils +--------------- + +.. autosummary:: + :toctree: generated/ + :template: custom-module.rst + + xrfeitoria.utils.anim.motion + xrfeitoria.utils.anim.utils + RPC runner ---------- diff --git a/src/XRFeitoriaUnreal/Content/Python/constants.py b/src/XRFeitoriaUnreal/Content/Python/constants.py index c38b86b3..c70ce505 100644 --- a/src/XRFeitoriaUnreal/Content/Python/constants.py +++ b/src/XRFeitoriaUnreal/Content/Python/constants.py @@ -184,6 +184,7 @@ class AntiAliasSetting: anti_aliasing: AntiAliasSetting = AntiAliasSetting() export_vertices: bool = False export_skeleton: bool = False + export_audio: bool = False def __post_init__(self): self.render_passes = [RenderPass(**rp) for rp in self.render_passes] diff --git a/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py b/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py index 30f8f8b7..4acfd0c9 100644 --- a/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py +++ b/src/XRFeitoriaUnreal/Content/Python/custom_movie_pipeline.py @@ -114,6 +114,12 @@ def set_export_skeleton(movie_preset: unreal.MoviePipelineMasterConfig, enable: ) export_setting.skeletal_mesh_operator_option.save_skeleton_position = enable + @staticmethod + def set_export_audio(movie_preset: unreal.MoviePipelineMasterConfig) -> None: + export_setting: unreal.MoviePipelineWaveOutput = movie_preset.find_or_add_setting_by_class( + unreal.MoviePipelineWaveOutput + ) + @staticmethod def add_render_passes(movie_preset: unreal.MoviePipelineMasterConfig, render_passes: List[RenderPass]) -> None: """Add render passes to a movie preset. @@ -273,6 +279,7 @@ def create_movie_preset( console_variables: Dict[str, float] = {'r.MotionBlurQuality': 0.0}, export_vertices: bool = False, export_skeleton: bool = False, + export_audio: bool = False, ) -> unreal.MoviePipelineMasterConfig: """ Create a movie preset from args. @@ -303,6 +310,8 @@ def create_movie_preset( cls.set_render_all_cameras(movie_preset, enable=True) cls.set_export_vertices(movie_preset, enable=export_vertices) cls.set_export_skeleton(movie_preset, enable=export_skeleton) + if export_audio: + cls.set_export_audio(movie_preset) return movie_preset @@ -389,6 +398,7 @@ def add_job_to_queue(cls, job: RenderJobUnreal) -> bool: console_variables=job.console_variables, export_vertices=job.export_vertices, export_skeleton=job.export_skeleton, + export_audio=job.export_audio, ) new_job.set_configuration(movie_preset) unreal.log(f'Added new job ({new_job.job_name}) to queue') diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 75ef8c7d..f56a6d4e 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -393,13 +393,13 @@ def add_animation_to_binding( def add_fk_motion_to_binding( binding: unreal.SequencerBindingProxy, - motion_data: List[Dict[str, Dict[str, List[float]]]], + motion_data: List[Dict[str, Dict[str, Union[float, List[float]]]]], ) -> None: """Add FK motion to the given actor binding. Args: binding (unreal.SequencerBindingProxy): The binding of actor in sequence to add FK motion to. - motion_data (List[Dict[str, Dict[str, List[float]]]]): The FK motion data to add. + motion_data (List[Dict[str, Dict[str, Union[float, List[float]]]]]): The FK motion data. """ rig_track: unreal.MovieSceneControlRigParameterTrack = ( unreal.ControlRigSequencerLibrary.find_or_create_control_rig_track( @@ -411,12 +411,21 @@ def add_fk_motion_to_binding( ) rig_section: unreal.MovieSceneControlRigParameterSection = rig_track.get_section_to_key() param_names = list(rig_section.get_parameter_names()) - bone_names = list(motion_data[0].keys()) - assert all([f'{bone}_CONTROL' in param_names for bone in bone_names]), RuntimeError( - f'Not All bone names (from json): {bone_names} not in param names (from seq FK): {param_names}' - ) + for bone_name, bone_data in motion_data[0].items(): + if 'curve' in bone_data.keys(): + bone_name = f'{bone_name}_CURVE_CONTROL' + else: + bone_name = f'{bone_name}_CONTROL' + assert bone_name in param_names, RuntimeError(f'bone name: {bone_name} not in param names: {param_names}') + + rig_proxies = unreal.ControlRigSequencerLibrary.get_control_rigs(binding.sequence) + for rig_proxy in rig_proxies: + ### TODO: judge if the track belongs to this actor + unreal.ControlRigSequencerLibrary.set_control_rig_apply_mode( + rig_proxy.control_rig, unreal.ControlRigFKRigExecuteMode.ADDITIVE + ) - def get_transform_from_bone_data(bone_data): + def get_transform_from_bone_data(bone_data: Dict[str, List[float]]): quat: Tuple[float, float, float, float] = bone_data.get('rotation') location: Tuple[float, float, float] = bone_data.get('location', (0, 0, 0)) # default location is (0, 0, 0) @@ -430,11 +439,18 @@ def get_transform_from_bone_data(bone_data): for frame, motion_frame in enumerate(motion_data): for bone_name, bone_data in motion_frame.items(): # TODO: set key type to STATIC - rig_section.add_transform_parameter_key( - parameter_name=f'{bone_name}_CONTROL', - time=get_time(binding.sequence, frame), - value=get_transform_from_bone_data(bone_data), - ) + if 'curve' in bone_data.keys(): + rig_section.add_scalar_parameter_key( + parameter_name=f"{bone_name}_CURVE_CONTROL", + time=get_time(binding.sequence, frame), + value=bone_data['curve'], + ) + else: + rig_section.add_transform_parameter_key( + parameter_name=f'{bone_name}_CONTROL', + time=get_time(binding.sequence, frame), + value=get_transform_from_bone_data(bone_data), + ) def get_spawnable_actor_from_binding( @@ -670,9 +686,6 @@ def add_actor_to_sequence( seq_fps: Optional[float] = None, seq_length: Optional[int] = None, ) -> Dict[str, Any]: - assert not ( - animation_asset is not None and motion_data is not None - ), 'Cannot provide both animation_asset and motion_data' # get sequence settings if seq_fps is None: seq_fps = get_sequence_fps(sequence) @@ -736,9 +749,6 @@ def add_spawnable_actor_to_sequence( seq_fps: Optional[float] = None, seq_length: Optional[int] = None, ) -> Dict[str, Any]: - assert not ( - animation_asset is not None and motion_data is not None - ), 'Cannot provide both animation_asset and motion_data' # get sequence settings if seq_fps is None: seq_fps = get_sequence_fps(sequence) @@ -794,6 +804,33 @@ def add_spawnable_actor_to_sequence( } +def add_audio_to_sequence( + sequence: unreal.LevelSequence, + audio_asset: unreal.SoundWave, + start_frame: Optional[int] = None, + end_frame: Optional[int] = None, +) -> Dict[str, Any]: + # ------- add audio track ------- # + audio_track: unreal.MovieSceneAudioTrack = sequence.add_master_track(unreal.MovieSceneAudioTrack) + audio_section: unreal.MovieSceneAudioSection = audio_track.add_section() + audio_track.set_display_name(audio_asset.get_name()) + + # ------- set start frame ------- # + if start_frame is None: + start_frame = 0 + audio_section.set_start_frame(start_frame=start_frame) + + # ------- set end frame ------- # + if end_frame is None: + duration = audio_asset.get_editor_property('duration') + audio_section.set_end_frame_seconds(end_time=duration) + else: + audio_section.set_end_frame(end_frame=end_frame) + audio_section.set_sound(audio_asset) + + return {'audio_track': {'track': audio_track, 'section': audio_section}} + + def generate_sequence( sequence_dir: str, sequence_name: str, @@ -1082,9 +1119,6 @@ def add_actor( AssertionError: If `animation_asset` and `motion_data` are both provided. Only one can be provided. """ assert cls.sequence is not None, 'Sequence not initialized' - assert not ( - animation_asset is not None and motion_data is not None - ), 'Cannot provide both animation_asset and motion_data' if animation_asset and isinstance(animation_asset, str): animation_asset = unreal.load_asset(animation_asset) if isinstance(actor, str): @@ -1114,6 +1148,31 @@ def add_actor( ) cls.bindings[actor_name] = bindings + @classmethod + def add_audio( + cls, + audio_asset: Union[str, unreal.SoundWave], + start_frame: Optional[int] = None, + end_frame: Optional[int] = None, + ): + """Spawn an audio in sequence. + + Args: + audio_asset (Union[str, unreal.SoundWave]): audio path (e.g. '/Game/audio_sample') / loaded asset (via `unreal.load_asset('/Game/audio_sample')`) + start_frame (Optional[int], optional): start frame of the audio. Defaults to None. + end_frame (Optional[int], optional): end frame of the audio. Defaults to None. + Raises: + AssertionError: If `cls.sequence` is not initialized. + """ + assert cls.sequence is not None, 'Sequence not initialized' + if isinstance(audio_asset, str): + audio_asset = unreal.load_asset(audio_asset) + + bindings = add_audio_to_sequence( + sequence=cls.sequence, audio_asset=audio_asset, start_frame=start_frame, end_frame=end_frame + ) + cls.bindings[audio_asset.get_name()] = bindings + if __name__ == '__main__': Sequence.new('/Game/NewMap', 'test1') diff --git a/src/XRFeitoriaUnreal/Content/Python/utils.py b/src/XRFeitoriaUnreal/Content/Python/utils.py index 027835ba..cb487f13 100644 --- a/src/XRFeitoriaUnreal/Content/Python/utils.py +++ b/src/XRFeitoriaUnreal/Content/Python/utils.py @@ -100,29 +100,32 @@ def import_asset( continue unreal.log(f'Importing asset: {path}') - # assetsTools = unreal.AssetToolsHelpers.get_asset_tools() - # assetImportData = unreal.AutomatedAssetImportData() - # assetImportData.destination_path = dst_dir - # assetImportData.filenames = [p] - # assets: List[unreal.Object] = assetsTools.import_assets_automated(assetImportData) - # asset_paths.extend([asset.get_path_name().split('.')[0] for asset in assets]) - - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - import_options = unreal.FbxImportUI() - import_options.set_editor_property('import_animations', True) - - import_task = unreal.AssetImportTask() - import_task.set_editor_property('automated', True) - import_task.set_editor_property('destination_name', '') - import_task.set_editor_property('destination_path', dst_dir) - import_task.set_editor_property('filename', path) - import_task.set_editor_property('replace_existing', replace) - import_task.set_editor_property('options', import_options) - - import_tasks = [import_task] - asset_tools.import_asset_tasks(import_tasks) - asset_paths.extend([path.split('.')[0] for path in import_task.get_editor_property('imported_object_paths')]) - + if path.endswith('.fbx'): + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + import_options = unreal.FbxImportUI() + import_options.set_editor_property('import_animations', True) + + import_task = unreal.AssetImportTask() + import_task.set_editor_property('automated', True) + import_task.set_editor_property('destination_name', '') + import_task.set_editor_property('destination_path', dst_dir) + import_task.set_editor_property('filename', path) + import_task.set_editor_property('replace_existing', replace) + import_task.set_editor_property('options', import_options) + + import_tasks = [import_task] + asset_tools.import_asset_tasks(import_tasks) + asset_paths.extend( + [path.split('.')[0] for path in import_task.get_editor_property('imported_object_paths')] + ) + else: + assetsTools = unreal.AssetToolsHelpers.get_asset_tools() + assetImportData = unreal.AutomatedAssetImportData() + assetImportData.destination_path = dst_dir + assetImportData.filenames = [path] + assetImportData.replace_existing = replace + assets: List[unreal.Object] = assetsTools.import_assets_automated(assetImportData) + asset_paths.extend([asset.get_path_name().split('.')[0] for asset in assets]) unreal.EditorAssetLibrary.save_directory(dst_dir, False, True) # save assets unreal.log(f'Imported asset: {path}') return asset_paths diff --git a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin index f19539ac..ba1c3756 100644 --- a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin +++ b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin @@ -34,6 +34,10 @@ "Name": "PythonScriptPlugin", "Enabled": true }, + { + "Name": "PythonFoundationPackages", + "Enabled": true + }, { "Name": "SequencerScripting", "Enabled": true diff --git a/xrfeitoria/actor/actor_base.py b/xrfeitoria/actor/actor_base.py index 2123ed9b..5628e48a 100644 --- a/xrfeitoria/actor/actor_base.py +++ b/xrfeitoria/actor/actor_base.py @@ -3,7 +3,6 @@ from typing import Optional, Tuple from loguru import logger -from typing_extensions import Self from ..data_structure.constants import PathLike, Vector from ..object.object_base import ObjectBase @@ -57,7 +56,7 @@ def import_from_file( rotation: 'Vector' = None, scale: 'Vector' = None, stencil_value: int = 1, - ) -> Self: + ) -> 'ActorBase': """Imports an actor from a file and returns its corresponding actor. For Blender, support files in types: fbx, obj, abc, ply, stl. @@ -77,7 +76,7 @@ def import_from_file( Ref to :ref:`FAQ-stencil-value` for details. Returns: - Self: the actor object. + ActorBase: the actor object. """ if actor_name is None: actor_name = cls._object_utils.generate_obj_name(obj_type='actor') diff --git a/xrfeitoria/data_structure/models.py b/xrfeitoria/data_structure/models.py index 292ca7d0..4156f11c 100644 --- a/xrfeitoria/data_structure/models.py +++ b/xrfeitoria/data_structure/models.py @@ -250,6 +250,7 @@ class AntiAliasSetting(BaseModel): ) export_vertices: bool = Field(default=False, description='Whether to export vertices of the render job.') export_skeleton: bool = Field(default=False, description='Whether to export skeleton of the render job.') + export_audio: bool = Field(default=False, description='Whether to export audio of the render job.') class Config: use_enum_values = True diff --git a/xrfeitoria/material/material_base.py b/xrfeitoria/material/material_base.py index 0c8c3e5d..b66ad624 100644 --- a/xrfeitoria/material/material_base.py +++ b/xrfeitoria/material/material_base.py @@ -2,9 +2,6 @@ from pathlib import Path from typing import Optional -from loguru import logger -from typing_extensions import Self - from ..data_structure.constants import PathLike from ..object.object_utils import ObjectUtilsBase @@ -22,14 +19,14 @@ def __init__(self, name: str) -> None: self._name = name @classmethod - def new(cls, mat_name: str) -> Self: + def new(cls, mat_name: str) -> 'MaterialBase': """Add a new material. Args: mat_name (str): Name of the material. Returns: - Self: The instance of the material. + MaterialBase: Material object. """ cls._new_material_in_engine(mat_name) return cls(mat_name) diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index f4737e3a..06e2dbaa 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -43,6 +43,7 @@ def add_job( anti_aliasing: 'Optional[RenderJob.AntiAliasSetting]' = None, export_vertices: bool = False, export_skeleton: bool = False, + export_audio: bool = False, ) -> None: """Add a rendering job to the renderer queue. @@ -58,6 +59,7 @@ def add_job( anti_aliasing (Optional[RenderJobUnreal.AntiAliasSetting], optional): Anti aliasing setting. Defaults to None. export_vertices (bool, optional): Whether to export vertices. Defaults to False. export_skeleton (bool, optional): Whether to export skeleton. Defaults to False. + export_audio (bool, optional): Whether to export audio. Defaults to False. Note: The motion blur is turned off by default. If you want to turn it on, please set ``r.MotionBlurQuality`` to a non-zero value in ``console_variables``. @@ -84,6 +86,7 @@ def add_job( anti_aliasing=anti_aliasing, export_vertices=export_vertices, export_skeleton=export_skeleton, + export_audio=export_audio, ) cls._add_job_in_engine(job.model_dump(mode='json')) cls.render_queue.append(job) diff --git a/xrfeitoria/sequence/sequence_base.py b/xrfeitoria/sequence/sequence_base.py index f7a77912..a53d5c64 100644 --- a/xrfeitoria/sequence/sequence_base.py +++ b/xrfeitoria/sequence/sequence_base.py @@ -3,7 +3,6 @@ from typing import Dict, List, Literal, Optional, Tuple, Union from loguru import logger -from typing_extensions import Self from .. import _tls from ..actor.actor_base import ActorBase @@ -30,7 +29,7 @@ class SequenceBase(ABC): _renderer = RendererBase __platform__: EngineEnum = _tls.cache.get('platform', None) - def __enter__(self) -> Self: + def __enter__(self) -> 'SequenceBase': return self def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 6bd65dd7..31042c39 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -60,6 +60,7 @@ def add_to_renderer( anti_aliasing: 'Optional[RenderJobUnreal.AntiAliasSetting]' = None, export_vertices: bool = False, export_skeleton: bool = False, + export_audio: bool = False, ) -> None: """Add the sequence to the renderer's job queue. Can only be called after the sequence is instantiated using @@ -78,6 +79,7 @@ def add_to_renderer( The anti-aliasing settings for the render job. Defaults to None. export_vertices (bool, optional): Whether to export vertices. Defaults to False. export_skeleton (bool, optional): Whether to export the skeleton. Defaults to False. + export_audio (bool, optional): Whether to export audio. Defaults to False. Examples: >>> import xrfeitoria as xf @@ -106,6 +108,7 @@ def add_to_renderer( anti_aliasing=anti_aliasing, export_vertices=export_vertices, export_skeleton=export_skeleton, + export_audio=export_audio, ) logger.info( f'[cyan]Added[/cyan] sequence "{cls.name}" to [bold]`Renderer`[/bold] ' @@ -141,14 +144,7 @@ def spawn_actor( Returns: ActorUnreal: The spawned actor object. - - Raises: - AssertionError: If both `anim_asset_path` and `motion_data` are provided. Only one of them can be provided. """ - assert not ( - anim_asset_path is not None and motion_data is not None - ), 'Cannot provide both `anim_asset_path` and `motion_data`' - transform_keys = SeqTransKey( frame=0, location=location, rotation=rotation, scale=scale, interpolation='CONSTANT' ) @@ -189,14 +185,7 @@ def spawn_actor_with_keys( Returns: ActorUnreal: The spawned actor. - - Raises: - AssertionError: If both `anim_asset_path` and `motion_data` are provided. Only one of them can be provided. """ - assert not ( - anim_asset_path is not None and motion_data is not None - ), 'Cannot provide both `anim_asset_path` and `motion_data`' - if not isinstance(transform_keys, list): transform_keys = [transform_keys] transform_keys = [i.model_dump() for i in transform_keys] @@ -217,6 +206,22 @@ def spawn_actor_with_keys( ) return ActorUnreal(actor_name) + @classmethod + def add_audio( + cls, + audio_asset_path: str, + start_frame: Optional[int] = None, + end_frame: Optional[int] = None, + ) -> None: + """Add an audio track to the sequence. + + Args: + audio_asset_path (str): The path to the audio asset in the engine. + start_frame (Optional[int], optional): The start frame of the audio track. Defaults to None. + end_frame (Optional[int], optional): The end frame of the audio track. Defaults to None. + """ + cls._add_audio_in_engine(audio_asset_path=audio_asset_path, start_frame=start_frame, end_frame=end_frame) + @classmethod def get_map_path(cls) -> str: """Returns the path to the map corresponding to the sequence in the Unreal @@ -279,6 +284,10 @@ def _open(cls, seq_name: str, seq_dir: 'Optional[str]' = None) -> None: ###### RPC METHODS (Private) ######## ##################################### + @staticmethod + def _get_default_seq_path_in_engine() -> str: + return XRFeitoriaUnrealFactory.constants.DEFAULT_SEQUENCE_PATH + @staticmethod def _get_seq_info_in_engine( seq_name: str, @@ -502,3 +511,18 @@ def _spawn_shape_in_engine( transform_keys=transform_keys, stencil_value=stencil_value, ) + + # ------ add audio -------- # + @staticmethod + def _add_audio_in_engine( + audio_asset_path: str, + start_frame: 'Optional[int]' = None, + end_frame: 'Optional[int]' = None, + ): + # check asset + unreal_functions.check_asset_in_engine(audio_asset_path, raise_error=True) + XRFeitoriaUnrealFactory.Sequence.add_audio( + audio_asset=audio_asset_path, + start_frame=start_frame, + end_frame=end_frame, + ) diff --git a/xrfeitoria/sequence/sequence_unreal.pyi b/xrfeitoria/sequence/sequence_unreal.pyi index 1468b86e..6b55cd06 100644 --- a/xrfeitoria/sequence/sequence_unreal.pyi +++ b/xrfeitoria/sequence/sequence_unreal.pyi @@ -31,13 +31,13 @@ class SequenceUnreal(SequenceBase): anti_aliasing: 'Optional[RenderJobUnreal.AntiAliasSetting]' = None, export_vertices: bool = False, export_skeleton: bool = False, + export_audio: bool = False, ) -> None: ... @classmethod def spawn_camera( cls, location: Vector, rotation: Vector, fov: float = ..., camera_name: str = ... ) -> CameraUnreal: ... @classmethod - @classmethod def spawn_actor( cls, actor_asset_path: str, @@ -86,6 +86,13 @@ class SequenceUnreal(SequenceBase): anim_asset_path: Optional[str] = ..., ) -> None: ... @classmethod + def add_audio( + cls, + audio_asset_path: str, + start_frame: Optional[int] = None, + end_frame: Optional[int] = None, + ) -> None: ... + @classmethod def get_map_path(cls) -> str: ... @classmethod def get_seq_path(cls) -> str: ... @@ -95,3 +102,5 @@ class SequenceUnreal(SequenceBase): def set_camera_cut_playback(cls, start_frame: Optional[int] = None, end_frame: Optional[int] = None) -> None: ... @classmethod def _open(cls, seq_name: str, seq_dir: 'Optional[str]' = ...) -> None: ... + @staticmethod + def _get_default_seq_path_in_engine() -> str: ... diff --git a/xrfeitoria/sequence/sequence_wrapper.py b/xrfeitoria/sequence/sequence_wrapper.py index e9d4bb46..7da7d8c7 100644 --- a/xrfeitoria/sequence/sequence_wrapper.py +++ b/xrfeitoria/sequence/sequence_wrapper.py @@ -185,6 +185,9 @@ def sequence_wrapper_unreal( Returns: SequenceUnreal: The created SequenceUnreal object. """ + + default_sequence_path = SequenceUnreal._get_default_seq_path_in_engine() + seq_dir = seq_dir or default_sequence_path if unreal_functions.check_asset_in_engine(f'{seq_dir}/{seq_name}') and not replace: SequenceUnreal._open(seq_name=seq_name, seq_dir=seq_dir) else: diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index c68ff9c2..0e00288e 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -1,3 +1,4 @@ +"""Motion data structure and related functions.""" from collections import OrderedDict from functools import partial from pathlib import Path @@ -5,7 +6,6 @@ import numpy as np from scipy.spatial.transform import Rotation as spRotation -from typing_extensions import Self from ...data_structure.constants import PathLike from .constants import ( @@ -21,6 +21,8 @@ ConverterType = Callable[[np.ndarray], np.ndarray] +__all__ = ['Motion', 'SMPLMotion', 'SMPLXMotion', 'get_humandata'] + class Converter: @classmethod @@ -110,51 +112,51 @@ def __init__( def _bone2idx(self, bone_name) -> Optional[int]: return self.BONE_NAME_TO_IDX.get(bone_name) - def get_transl(self, frame=0) -> np.ndarray: + def _get_transl(self, frame=0) -> np.ndarray: return self.transl[frame, :3] - def get_global_orient(self, frame=0) -> np.ndarray: + def _get_global_orient(self, frame=0) -> np.ndarray: return self.global_orient[frame, :3] - def get_bone_rotvec(self, bone_name, frame=0) -> np.ndarray: + def _get_bone_rotvec(self, bone_name, frame=0) -> np.ndarray: idx = self._bone2idx(bone_name) if idx == 0: - return self.get_global_orient(frame) + return self._get_global_orient(frame) elif idx: return self.body_poses[frame, idx, :3] else: return np.zeros([3], dtype=np.float32) - def get_bone_rotation(self, bone_name: str, frame=0) -> spRotation: - rotvec = self.get_bone_rotvec(bone_name, frame) + def _get_bone_rotation(self, bone_name: str, frame=0) -> spRotation: + rotvec = self._get_bone_rotvec(bone_name, frame) return spRotation.from_rotvec(rotvec) # type: ignore def get_bone_matrix_basis(self, bone_name: str, frame=0) -> np.ndarray: """pose2rest: relative to the bone space at rest pose. - Result: - np.ndarray: transform matrix like - [ - [R, T], - [0, 1] - ] + Args: + bone_name (str): bone name + frame (int, optional): frame index. Defaults to 0. + + Returns: + np.ndarray: transform matrix like [ [R, T], [0, 1] ] """ idx = self._bone2idx(bone_name) if idx == 0: - transl = self.get_transl(frame) + transl = self._get_transl(frame) else: transl = np.zeros(3) - rot = self.get_bone_rotation(bone_name, frame) + rot = self._get_bone_rotation(bone_name, frame) matrix_basis = rot.as_matrix() matrix_basis = np.pad(matrix_basis, (0, 1)) matrix_basis[:3, 3] = transl matrix_basis[3, 3] = 1 return matrix_basis - def get_parent_bone_name(self, bone_name: str) -> Optional[str]: + def _get_parent_bone_name(self, bone_name: str) -> Optional[str]: ... - def convert_fps_smplx_data(self, smplx_data: Dict[str, np.ndarray], scaling: int) -> Dict[str, np.ndarray]: + def _convert_fps_smplx_data(self, smplx_data: Dict[str, np.ndarray], scaling: int) -> Dict[str, np.ndarray]: for key, value in smplx_data.items(): if key in ['betas']: continue @@ -168,9 +170,8 @@ def convert_fps(self, fps: float): fps (float): The desired frames per second. Raises: - NotImplementedError: - - If the desired fps is greater than the current fps, motion interpolation is not supported. - - If the desired fps is less than the current fps, motion interpolation is not supported. + NotImplementedError: If the fps is greater than the current fps. + NotImplementedError: If the fps is less than the current fps when undividable. """ if fps == self.fps: return @@ -183,9 +184,9 @@ def convert_fps(self, fps: float): self.global_orient: np.ndarray = self.global_orient[::scaling, :] self.n_frames = self.body_poses.shape[0] if hasattr(self, 'smpl_data'): - self.smpl_data = self.convert_fps_smplx_data(self.smpl_data, scaling) + self.smpl_data = self._convert_fps_smplx_data(self.smpl_data, scaling) if hasattr(self, 'smplx_data'): - self.smplx_data = self.convert_fps_smplx_data(self.smplx_data, scaling) + self.smplx_data = self._convert_fps_smplx_data(self.smplx_data, scaling) self.fps = fps elif fps > self.fps: # TODO: motion interpolation @@ -199,6 +200,9 @@ def slice_motion(self, frame_interval: int): Args: frame_interval (int): The frame interval to use for slicing the motion sequence. + + Raises: + TypeError: If the frame interval is not an integer. """ assert isinstance(frame_interval, int), TypeError(f'scaling={frame_interval} should be int') @@ -207,9 +211,9 @@ def slice_motion(self, frame_interval: int): self.global_orient: np.ndarray = self.global_orient[::frame_interval, :] self.n_frames = self.body_poses.shape[0] if hasattr(self, 'smpl_data'): - self.smpl_data = self.convert_fps_smplx_data(self.smpl_data, frame_interval) + self.smpl_data = self._convert_fps_smplx_data(self.smpl_data, frame_interval) if hasattr(self, 'smplx_data'): - self.smplx_data = self.convert_fps_smplx_data(self.smplx_data, frame_interval) + self.smplx_data = self._convert_fps_smplx_data(self.smplx_data, frame_interval) def sample_motion(self, n_frames: int): """Randomly sample motions, picking n_frames from the original motion sequence. @@ -217,6 +221,9 @@ def sample_motion(self, n_frames: int): Args: n_frames (int): The number of frames to sample. Randomly sampled from the original motion sequence. + + Raises: + AssertionError: If the number of frames to sample is less than or equal to 0. """ assert n_frames > 0, f'n_frames={n_frames}' if n_frames == self.n_frames: @@ -289,7 +296,7 @@ def get_motion_data(self) -> List[Dict[str, Dict[str, List[float]]]]: motion_data.append(frame_motion_data) return motion_data - def copy(self) -> Self: + def copy(self) -> 'Motion': """Return a copy of the motion instance.""" return self.__class__( transl=self.transl.copy(), @@ -331,7 +338,7 @@ def from_smpl_data( insert_rest_pose: bool = False, global_orient_adj: Optional[spRotation] = GLOBAL_ORIENT_ADJUSTMENT, vector_convertor: Optional[ConverterType] = Converter.vec_humandata2smplx, - ) -> Self: + ) -> 'SMPLMotion': """Create SMPLMotion instance from smpl_data. `smpl_data` should be a dict like object, @@ -396,16 +403,16 @@ def from_smpl_data( instance.smpl_data = smpl_data return instance - def get_bone_rotvec(self, bone_name, frame=0) -> np.ndarray: + def _get_bone_rotvec(self, bone_name, frame=0) -> np.ndarray: idx = self._bone2idx(bone_name) if idx == 0: - return self.get_global_orient(frame) + return self._get_global_orient(frame) elif idx: return self.body_poses[frame, idx, :3] else: return np.zeros([3], dtype=np.float32) - def get_parent_bone_name(self, bone_name) -> Optional[str]: + def _get_parent_bone_name(self, bone_name) -> Optional[str]: idx = self._bone2idx(bone_name) if idx is None: raise ValueError(f'bone.name="{bone_name}" not in smpl skeleton.') @@ -428,22 +435,6 @@ def dump_humandata( ) -> None: """Dump the motion data to a humandata file at the given `filepath`. - HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md - - The humandata file is a npz file containing the following keys: - ``` - motion_data = { - '__data_len__': n_frames, - 'smpl': { - 'betas': betas, # (1, 10) - 'transl': transl, # (n_frames, 3) - 'global_orient': global_orient, # (n_frames, 3) - 'body_pose': body_pose, # (n_frames, 69) - }, - 'meta': {'gender': 'neutral'}, # optional - } - ``` - Args: filepath (PathLike): The filepath to dump the motion data to. betas (np.ndarray): The betas array. @@ -452,6 +443,24 @@ def dump_humandata( transl_offset (np.ndarray): The translation offset. Defaults to np.zeros(3). root_location_t0 (Optional[np.ndarray]): The root location at time 0. Defaults to None. pelvis_location_t0 (Optional[np.ndarray]): The pelvis location at time 0. Defaults to None. + + Note: + HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md + + The humandata file is a npz file containing the following keys: + + .. code-block:: python + + motion_data = { + '__data_len__': n_frames, + 'smpl': { + 'betas': betas, # (1, 10) + 'transl': transl, # (n_frames, 3) + 'global_orient': global_orient, # (n_frames, 3) + 'body_pose': body_pose, # (n_frames, 69) + }, + 'meta': {'gender': 'neutral'}, # optional + } """ humandata = get_humandata( smpl_x_data=self.smplx_data, @@ -467,7 +476,7 @@ def dump_humandata( filepath.parent.mkdir(parents=True, exist_ok=True) np.savez(filepath, **humandata) - def copy(self) -> Self: + def copy(self) -> 'SMPLMotion': """Return a copy of the motion instance.""" instance = self.__class__( transl=self.transl.copy(), @@ -512,7 +521,7 @@ def from_smplx_data( flat_hand_mean: bool = False, global_orient_adj: Optional[spRotation] = GLOBAL_ORIENT_ADJUSTMENT, vector_convertor: Optional[Callable[[np.ndarray], np.ndarray]] = Converter.vec_humandata2smplx, - ) -> Self: + ) -> 'SMPLXMotion': """Create SMPLXMotion instance from smplx_data. `smplx_data` should be a dict like object, @@ -608,7 +617,7 @@ def from_smplx_data( return instance @classmethod - def from_amass_data(cls, amass_data, insert_rest_pose: bool, flat_hand_mean: bool = True) -> Self: + def from_amass_data(cls, amass_data, insert_rest_pose: bool, flat_hand_mean: bool = True) -> 'SMPLXMotion': """Create a Motion instance from AMASS data (SMPLX) Args: @@ -674,7 +683,7 @@ def from_amass_data(cls, amass_data, insert_rest_pose: bool, flat_hand_mean: boo return cls.from_smplx_data(smplx_data, insert_rest_pose=False, fps=fps, flat_hand_mean=flat_hand_mean) - def get_parent_bone_name(self, bone_name) -> Optional[str]: + def _get_parent_bone_name(self, bone_name) -> Optional[str]: idx = self._bone2idx(bone_name) if idx is None: raise ValueError(f'bone.name="{bone_name}" not in smplx skeleton.') @@ -697,28 +706,6 @@ def dump_humandata( ) -> None: """Dump the motion data to a humandata file at the given `filepath`. - HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md - - The humandata file is a npz file containing the following keys: - ```python - humandata = { - '__data_len__': n_frames, - 'smplx': { - 'betas': betas, # (1, 10) - 'transl': transl, # (n_frames, 3) - 'global_orient': global_orient, # (n_frames, 3) - 'body_pose': body_pose, # (n_frames, 63) - 'jaw_pose': jaw_pose, # (n_frames, 3) - 'leye_pose': leye_pose, # (n_frames, 3) - 'reye_pose': reye_pose, # (n_frames, 3) - 'left_hand_pose': left_hand_pose, # (n_frames, 45) - 'right_hand_pose': right_hand_pose, # (n_frames, 45) - 'expression': expression, # (n_frames, 10) - }, - 'meta': {'gender': 'neutral'}, # optional - } - ``` - Args: filepath (PathLike): The filepath to dump the motion data to. betas (np.ndarray): The betas array. @@ -727,6 +714,31 @@ def dump_humandata( transl_offset (np.ndarray): The translation offset. Defaults to np.zeros(3). root_location_t0 (Optional[np.ndarray]): The root location at time 0. Defaults to None. pelvis_location_t0 (Optional[np.ndarray]): The pelvis location at time 0. Defaults to None. + + Note: + + HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md + + The humandata file is a npz file containing the following keys: + + .. code-block:: python + + humandata = { + '__data_len__': n_frames, + 'smplx': { + 'betas': betas, # (1, 10) + 'transl': transl, # (n_frames, 3) + 'global_orient': global_orient, # (n_frames, 3) + 'body_pose': body_pose, # (n_frames, 63) + 'jaw_pose': jaw_pose, # (n_frames, 3) + 'leye_pose': leye_pose, # (n_frames, 3) + 'reye_pose': reye_pose, # (n_frames, 3) + 'left_hand_pose': left_hand_pose, # (n_frames, 45) + 'right_hand_pose': right_hand_pose, # (n_frames, 45) + 'expression': expression, # (n_frames, 10) + }, + 'meta': {'gender': 'neutral'}, # optional + } """ humandata = get_humandata( smpl_x_data=self.smplx_data, @@ -742,7 +754,7 @@ def dump_humandata( filepath.parent.mkdir(parents=True, exist_ok=True) np.savez(filepath, **humandata) - def copy(self) -> Self: + def copy(self) -> 'SMPLXMotion': """Return a copy of the motion instance.""" instance = self.__class__( transl=self.transl.copy(), diff --git a/xrfeitoria/utils/anim/transform3d.py b/xrfeitoria/utils/anim/transform3d.py index b3f54a19..d716f822 100644 --- a/xrfeitoria/utils/anim/transform3d.py +++ b/xrfeitoria/utils/anim/transform3d.py @@ -3,7 +3,6 @@ import numpy as np import numpy.typing as npt from scipy.spatial.transform import Rotation as spRotation -from typing_extensions import Self class Matrix: @@ -23,25 +22,25 @@ def __init__(self, mat: Optional[npt.ArrayLike] = None): ) self.data = mat - def to_4x4(self) -> Self: + def to_4x4(self) -> 'Matrix': mat = np.eye(4) old = self.data[:4, :4] mat[: old.shape[0], : old.shape[1]] = old return Matrix(mat) - def to_3x3(self) -> Self: + def to_3x3(self) -> 'Matrix': mat = np.eye(3) old = self.data[:3, :3] mat[: old.shape[0], : old.shape[1]] = old return Matrix(mat) - def to_2x2(self) -> Self: + def to_2x2(self) -> 'Matrix': mat = np.eye(2) old = self.data[:2, :2] mat[: old.shape[0], : old.shape[1]] = old return Matrix(mat) - def inverted(self) -> Self: + def inverted(self) -> 'Matrix': return Matrix(np.linalg.inv(self.data)) def decompose(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: @@ -56,14 +55,14 @@ def _to_matmul_type(value) -> np.ndarray: except Exception as e: raise TypeError(f'Unsupported operand type for @, value: {value}') - def __matmul__(self, other) -> Self: + def __matmul__(self, other) -> 'Matrix': right = self._to_matmul_type(other) left = self.data if self.data.shape[1] != other.data.shape[0]: raise ValueError('Matrix multiplication: shape mismatch: ' f'{left.shape} @ {right.shape}') return Matrix(left @ right) - def __rmatmul__(self, other) -> Self: + def __rmatmul__(self, other) -> 'Matrix': left = self._to_matmul_type(other) right = self.data if self.data.shape[1] != other.data.shape[0]: @@ -139,30 +138,27 @@ def __repr__(self) -> str: return self.__str__() -def decompose_trs( - mat: npt.ArrayLike, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +def decompose_trs(mat: npt.ArrayLike) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Decompose a 4x4 transformation matrix into 3 parts: translation, rotation, scale. - Parameters - ---------- - mat : npt.ArrayLike - A transformation matrix of shape (N, 4, 4) or (4, 4) - e.g. - [ - [1, 0, 0, x], - [0, 1, 0, y], - [0, 0, 1, z], - [0, 0, 0, 1], - ] - - Returns - ------- - Tuple[np.ndarray, np.ndarray, np.ndarray] - - translation(x, y, z): np.ndarray of shape (N, 3) or (3,) - - quaternion(w, x, y, z): np.ndarray of shape (N, 4) - - scaling(x, y, z): np.ndarray of shape (N, 3) or (3,) + Args: + mat: npt.ArrayLike + A transformation matrix of shape (N, 4, 4) or (4, 4) + e.g. + [ + [1, 0, 0, x], + [0, 1, 0, y], + [0, 0, 1, z], + [0, 0, 0, 1], + ] + + Returns: + Tuple[np.ndarray, np.ndarray, np.ndarray]: + + - translation(x, y, z): np.ndarray of shape (N, 3) or (3,) + - quaternion(w, x, y, z): np.ndarray of shape (N, 4) + - scaling(x, y, z): np.ndarray of shape (N, 3) or (3,) """ mat = np.array(mat) ndim = mat.ndim diff --git a/xrfeitoria/utils/anim/utils.py b/xrfeitoria/utils/anim/utils.py index ba92e3aa..84ac0b3e 100644 --- a/xrfeitoria/utils/anim/utils.py +++ b/xrfeitoria/utils/anim/utils.py @@ -1,3 +1,4 @@ +"""Utilities for animation data loading and dumping.""" from pathlib import Path from typing import Union @@ -8,7 +9,7 @@ def load_amass_motion(input_amass_smplx_path: PathLike) -> SMPLXMotion: - """Load AMASS SMPLX motion data. + """Load AMASS SMPLX motion data. Only for SMPLX motion for now. Args: input_amass_smplx_path (PathLike): Path to AMASS SMPLX motion data. @@ -28,13 +29,14 @@ def load_amass_motion(input_amass_smplx_path: PathLike) -> SMPLXMotion: def load_humandata_motion(input_humandata_path: PathLike) -> Union[SMPLMotion, SMPLXMotion]: """Load humandata SMPL / SMPLX motion data. + HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md Args: input_humandata_path (PathLike): Path to humandata SMPL / SMPLX motion data. Returns: - Motion: Motion data, which consists of data read from humandata file. + Union[SMPLMotion, SMPLXMotion]: Motion data, which consists of data read from humandata file. """ input_humandata_path = Path(input_humandata_path).resolve() if not input_humandata_path.exists(): @@ -52,31 +54,38 @@ def load_humandata_motion(input_humandata_path: PathLike) -> Union[SMPLMotion, S return src_motion -def dump_humandata(motion: SMPLXMotion, save_filepath: PathLike, meta_filepath: PathLike): - """Dump human data to a file. +def dump_humandata(motion: SMPLXMotion, save_filepath: PathLike, meta_filepath: PathLike) -> None: + """Dump human data to a file. This function must be associate with a meta file + provided by SMPL-XL. Args: - motion (SMPLXMotion): The SMPLXMotion object containing the motion data. + motion (SMPLXMotion): Motion data to dump. save_filepath (PathLike): The file path to save the dumped data. meta_filepath (PathLike): The file path to the meta information, storing the parameters of the SMPL-XL model. - The meta information is stored in the following format: - ```python - # meta = np.load(meta_filepath, allow_pickle=True) - meta = { - 'smplx': { - 'betas': betas, # (10,) - 'global_orient': global_orient, # (1, 3) - 'transl': transl, # (1, 3) - 'root_location_t0': root_location_t0, # (1, 3) - 'pelvis_location_t0': pelvis_location_t0, # (1, 3) - }, - 'meta': {'gender': 'neutral'} - } - ``` - - Returns: - None + Note: + HumanData is a structure of smpl/smplx data defined in https://github.com/open-mmlab/mmhuman3d/blob/main/docs/human_data.md + + The humandata file is a npz file containing the following keys: + + .. code-block:: python + + humandata = { + '__data_len__': n_frames, + 'smplx': { + 'betas': betas, # (1, 10) + 'transl': transl, # (n_frames, 3) + 'global_orient': global_orient, # (n_frames, 3) + 'body_pose': body_pose, # (n_frames, 63) + 'jaw_pose': jaw_pose, # (n_frames, 3) + 'leye_pose': leye_pose, # (n_frames, 3) + 'reye_pose': reye_pose, # (n_frames, 3) + 'left_hand_pose': left_hand_pose, # (n_frames, 45) + 'right_hand_pose': right_hand_pose, # (n_frames, 45) + 'expression': expression, # (n_frames, 10) + }, + 'meta': {'gender': 'neutral'}, # optional + } """ meta_info = np.load(meta_filepath, allow_pickle=True) smplx = meta_info['smplx'].item() diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 77270c91..a227032d 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -39,6 +39,15 @@ # XXX: hardcode download url oss_root = 'https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria' plugin_infos_json = Path(__file__).parent.resolve() / 'plugin_infos.json' +plugin_info_type = TypedDict( + 'PluginInfo', + { + 'plugin_name': str, + 'plugin_version': str, + 'engine_version': str, + 'platform': Literal['Windows', 'Linux', 'Darwin'], + }, +) def _rmtree(path: Path) -> None: @@ -478,17 +487,7 @@ def _start_rpc(self, background: bool = True, project_path: Optional[Path] = '') @property @lru_cache - def plugin_info( - self, - ) -> TypedDict( - 'PluginInfo', - { - 'plugin_name': str, - 'plugin_version': str, - 'engine_version': str, - 'platform': Literal['Windows', 'Linux', 'Darwin'], - }, - ): + def plugin_info(self) -> plugin_info_type: # plugin_infos = { "0.5.0": { "XRFeitoria": "0.5.0", "XRFeitoriaBpy": "0.5.0", "XRFeitoriaUnreal": "0.5.0" }, ... } plugin_infos: Dict[str, Dict[str, str]] = json.loads(plugin_infos_json.read_text()) plugin_versions = sorted((map(parse, plugin_infos.keys()))) From 9766571a5dced6b712fe9c351ec2132077e70179 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 18 Jan 2024 17:30:08 +0800 Subject: [PATCH 081/110] pre-commit --- src/XRFeitoriaUnreal/Content/Python/sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index f56a6d4e..3f6918f2 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -441,7 +441,7 @@ def get_transform_from_bone_data(bone_data: Dict[str, List[float]]): # TODO: set key type to STATIC if 'curve' in bone_data.keys(): rig_section.add_scalar_parameter_key( - parameter_name=f"{bone_name}_CURVE_CONTROL", + parameter_name=f'{bone_name}_CURVE_CONTROL', time=get_time(binding.sequence, frame), value=bone_data['curve'], ) From c1df2fa1e2a66f822fd85744858125eaa6e7c933 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 18 Jan 2024 21:24:03 +0800 Subject: [PATCH 082/110] [Fix] misc bugs --- xrfeitoria/__init__.py | 16 +++++++++++++--- xrfeitoria/renderer/renderer_unreal.py | 3 +++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/xrfeitoria/__init__.py b/xrfeitoria/__init__.py index 883e5dc5..72b021b7 100644 --- a/xrfeitoria/__init__.py +++ b/xrfeitoria/__init__.py @@ -1,11 +1,20 @@ import threading -_tls = threading.local() -_tls.cache = {'platform': None, 'engine_process': None, 'unreal_project_path': None} - __all__ = ['__version__', 'init_blender', 'init_unreal'] +class CacheThread: + def __init__(self): + self.global_vars = {} + + @property + def cache(self): + key = threading.current_thread().ident + if key not in self.global_vars: + self.global_vars[key] = {'platform': None, 'engine_process': None, 'unreal_project_path': None} + return self.global_vars[key] + + def _get_version() -> str: from importlib.metadata import PackageNotFoundError, version @@ -17,5 +26,6 @@ def _get_version() -> str: return __version__ +_tls = CacheThread() __version__ = _get_version() from .factory import init_blender, init_unreal diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index 06e2dbaa..cf80cc9d 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -148,6 +148,9 @@ def render_jobs(cls) -> None: break if 'Render completed. Success: True' in data: break + if 'Render completed. Success: False' in data: + logger.error('[red]Render Failed[/red]') + break logger.info(f'(engine) {data}') except BlockingIOError: pass From 98d82821fbf7d2d82cbddbb144c04db7a88826bf Mon Sep 17 00:00:00 2001 From: yangzhitao Date: Fri, 19 Jan 2024 16:38:22 +0800 Subject: [PATCH 083/110] accept XRFEITORIA_VERSION from env --- xrfeitoria/utils/runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index a227032d..2e57168d 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -1,5 +1,6 @@ """Runner for starting blender or unreal as a rpc server.""" import json +import os import platform import re import shutil @@ -491,7 +492,7 @@ def plugin_info(self) -> plugin_info_type: # plugin_infos = { "0.5.0": { "XRFeitoria": "0.5.0", "XRFeitoriaBpy": "0.5.0", "XRFeitoriaUnreal": "0.5.0" }, ... } plugin_infos: Dict[str, Dict[str, str]] = json.loads(plugin_infos_json.read_text()) plugin_versions = sorted((map(parse, plugin_infos.keys()))) - _version = parse(__version__) + _version = parse(os.environ.get('XRFEITORIA__VERSION') or __version__) # find compatible version, lower bound, e.g. 0.5.1 => 0.5.0 if _version in plugin_versions: From b0a437adcf47710d1f3302f79eec2853a72912e3 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 19 Jan 2024 20:30:13 +0800 Subject: [PATCH 084/110] [Fix] Should add `ImportError` in except --- tests/unreal/main.py | 4 ++-- xrfeitoria/actor/actor_blender.py | 2 +- xrfeitoria/camera/camera_blender.py | 2 +- xrfeitoria/data_structure/constants.py | 1 + xrfeitoria/material/material_blender.py | 5 ----- xrfeitoria/renderer/renderer_blender.py | 9 +++++---- xrfeitoria/renderer/renderer_unreal.py | 2 +- xrfeitoria/rpc/factory.py | 5 +++-- xrfeitoria/sequence/sequence_base.py | 2 +- xrfeitoria/sequence/sequence_blender.py | 8 ++++++-- xrfeitoria/sequence/sequence_unreal.py | 2 +- xrfeitoria/utils/runner.py | 2 +- xrfeitoria/utils/viewer.py | 13 +++++++------ 13 files changed, 30 insertions(+), 27 deletions(-) diff --git a/tests/unreal/main.py b/tests/unreal/main.py index 98a6ec75..3ccc30b8 100644 --- a/tests/unreal/main.py +++ b/tests/unreal/main.py @@ -17,8 +17,8 @@ def main(debug: bool = False, background: bool = False): logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=root / 'unreal.log') - init_test(debug=debug, background=background, dev=True) - with _init_unreal(background=background) as xf_runner: + with _init_unreal(background=background, dev_plugin=True) as xf_runner: + init_test(debug=debug, background=background) actor_test(debug=debug, background=background) camera_test(debug=debug, background=background) sequence_test(debug=debug, background=background) diff --git a/xrfeitoria/actor/actor_blender.py b/xrfeitoria/actor/actor_blender.py index 9e25dcd4..03fd37f2 100644 --- a/xrfeitoria/actor/actor_blender.py +++ b/xrfeitoria/actor/actor_blender.py @@ -19,7 +19,7 @@ try: from ..data_structure.models import TransformKeys # isort:skip -except ModuleNotFoundError: +except (ImportError, ModuleNotFoundError): pass diff --git a/xrfeitoria/camera/camera_blender.py b/xrfeitoria/camera/camera_blender.py index 33fdf19b..4a22d5b9 100644 --- a/xrfeitoria/camera/camera_blender.py +++ b/xrfeitoria/camera/camera_blender.py @@ -15,7 +15,7 @@ try: from ..data_structure.models import TransformKeys # isort:skip -except ModuleNotFoundError: +except (ImportError, ModuleNotFoundError): pass diff --git a/xrfeitoria/data_structure/constants.py b/xrfeitoria/data_structure/constants.py index 1ff26e86..5d511fea 100644 --- a/xrfeitoria/data_structure/constants.py +++ b/xrfeitoria/data_structure/constants.py @@ -130,6 +130,7 @@ class RenderOutputEnumUnreal(EnumBase): skeleton = 'skeleton' actor_infos = 'actor_infos' camera_params = cam_param_dir + audio = 'Audio' class InterpolationEnumUnreal(EnumBase): diff --git a/xrfeitoria/material/material_blender.py b/xrfeitoria/material/material_blender.py index 4b5a2ef8..9df23cab 100644 --- a/xrfeitoria/material/material_blender.py +++ b/xrfeitoria/material/material_blender.py @@ -13,11 +13,6 @@ except ModuleNotFoundError: pass -try: - from ..data_structure.models import TransformKeys # isort:skip -except ModuleNotFoundError: - pass - @remote_blender(dec_class=True, suffix='_in_engine') class MaterialBlender(MaterialBase): diff --git a/xrfeitoria/renderer/renderer_blender.py b/xrfeitoria/renderer/renderer_blender.py index 0e371cbf..1de22e34 100644 --- a/xrfeitoria/renderer/renderer_blender.py +++ b/xrfeitoria/renderer/renderer_blender.py @@ -24,14 +24,15 @@ from .renderer_base import RendererBase, render_status try: - from ..data_structure.models import RenderJobBlender, RenderPass # isort:skip + # only for linting, not imported in runtime + from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py except ModuleNotFoundError: pass + try: - # only for linting, not imported in runtime - from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py -except ModuleNotFoundError: + from ..data_structure.models import RenderJobBlender, RenderPass # isort:skip +except (ImportError, ModuleNotFoundError): pass diff --git a/xrfeitoria/renderer/renderer_unreal.py b/xrfeitoria/renderer/renderer_unreal.py index cf80cc9d..872702c5 100644 --- a/xrfeitoria/renderer/renderer_unreal.py +++ b/xrfeitoria/renderer/renderer_unreal.py @@ -20,7 +20,7 @@ try: from ..data_structure.models import RenderJobUnreal as RenderJob from ..data_structure.models import RenderPass -except ModuleNotFoundError: +except (ImportError, ModuleNotFoundError): pass diff --git a/xrfeitoria/rpc/factory.py b/xrfeitoria/rpc/factory.py index 006ed52f..becde246 100644 --- a/xrfeitoria/rpc/factory.py +++ b/xrfeitoria/rpc/factory.py @@ -191,8 +191,9 @@ def _register(cls, function: Callable) -> List[str]: logger.log('RPC', f'response: {response}') except ConnectionRefusedError: - server_name = os.environ.get(f'RPC_SERVER_{cls.rpc_client.port}', cls.rpc_client.port) - raise ConnectionRefusedError(f'No connection could be made with "{server_name}"') + if cls.rpc_client: + server_name = os.environ.get(f'RPC_SERVER_{cls.rpc_client.port}', cls.rpc_client.port) + raise ConnectionRefusedError(f'No connection could be made with "{server_name}"') return code diff --git a/xrfeitoria/sequence/sequence_base.py b/xrfeitoria/sequence/sequence_base.py index a53d5c64..0e6b523e 100644 --- a/xrfeitoria/sequence/sequence_base.py +++ b/xrfeitoria/sequence/sequence_base.py @@ -14,7 +14,7 @@ try: from ..data_structure.models import SequenceTransformKey, TransformKeys, RenderPass # isort:skip -except ModuleNotFoundError: +except (ImportError, ModuleNotFoundError): SequenceTransformKey = TransformKeys = RenderPass = None diff --git a/xrfeitoria/sequence/sequence_blender.py b/xrfeitoria/sequence/sequence_blender.py index a88cdce3..10d3853f 100644 --- a/xrfeitoria/sequence/sequence_blender.py +++ b/xrfeitoria/sequence/sequence_blender.py @@ -12,11 +12,15 @@ try: import bpy # isort:skip - from ..data_structure.models import RenderPass, TransformKeys # isort:skip from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py except ModuleNotFoundError: pass +try: + from ..data_structure.models import RenderPass, TransformKeys # isort:skip +except (ImportError, ModuleNotFoundError): + pass + @remote_blender(dec_class=True, suffix='_in_engine') class SequenceBlender(SequenceBase): @@ -63,7 +67,7 @@ def add_to_renderer( cls, output_path: PathLike, resolution: Tuple[int, int], - render_passes: List[RenderPass], + render_passes: 'List[RenderPass]', **kwargs, ): cls._renderer.add_job( diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 31042c39..e3493de4 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -21,7 +21,7 @@ from ..data_structure.models import RenderJobUnreal, RenderPass from ..data_structure.models import SequenceTransformKey as SeqTransKey from ..data_structure.models import TransformKeys -except ModuleNotFoundError: +except (ImportError, ModuleNotFoundError): pass diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 2e57168d..d681e448 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -340,7 +340,7 @@ def start(self) -> None: logger.info(f'RPC server started at port {self.port}') # check if engine process is alive in a separate thread - threading.Thread(target=self._receive_stdout).start() + threading.Thread(target=self._receive_stdout, daemon=True).start() threading.Thread(target=self.check_engine_alive, daemon=True).start() def wait_for_start(self, process: subprocess.Popen) -> None: diff --git a/xrfeitoria/utils/viewer.py b/xrfeitoria/utils/viewer.py index e1f76115..31fc8363 100644 --- a/xrfeitoria/utils/viewer.py +++ b/xrfeitoria/utils/viewer.py @@ -6,6 +6,7 @@ import numpy as np from ..data_structure.constants import PathLike +from ..data_structure.models import RenderOutputEnumBlender os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '1' @@ -104,12 +105,12 @@ class Viewer: """ # folder names of each data modal - IMG = 'img' - MASK = 'mask' - DEPTH = 'depth' - FLOW = 'flow' - NORMAL = 'normal' - DIFFUSE = 'diffuse' + IMG = RenderOutputEnumBlender.img.name + MASK = RenderOutputEnumBlender.mask.name + DEPTH = RenderOutputEnumBlender.depth.name + FLOW = RenderOutputEnumBlender.flow.name + NORMAL = RenderOutputEnumBlender.normal.name + DIFFUSE = RenderOutputEnumBlender.diffuse.name def __init__(self, sequence_dir: PathLike) -> None: """Initialize with the sequence directory. From 2a6702e17807146f3b471f55953c1e45aee628d0 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Sun, 21 Jan 2024 22:59:13 +0800 Subject: [PATCH 085/110] Refactor error message in RPCRunner class --- xrfeitoria/utils/runner.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index d681e448..934c4ab6 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -235,11 +235,12 @@ def reuse(self) -> bool: raise RuntimeError( f'RPC server in `RPC_PORT={self.port}` already started! ' 'This is raised when calling `init_blender` or `init_unreal` with `new_progress=True`' - 'when an existing server (blender or unreal) is already running. \n' + 'if an existing server (blender or unreal) is already running. \n' '3 ways to get around this: \n' - ' - set `new_process=False` for using the existing server. \n' - ' - stop the server (engine process) and try again; \n' - " - change the rpc port via system env 'RPC_PORT' and try again." + ' - Set `new_process=False` for using the existing server. \n' + ' - Stop the server (engine process) and try again; \n' + " - Change the rpc port via system env 'RPC_PORT' and try again.\n" + 'For multi-processing, please refer to https://xrfeitoria.readthedocs.io/en/latest/faq.html#rpc-port' ) except ConnectionRefusedError: return False From d12997e5605aca7f4609166e499951542288c186 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 22 Jan 2024 15:49:29 +0800 Subject: [PATCH 086/110] `engine_running` set to False when accidentally exit --- xrfeitoria/utils/runner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 934c4ab6..65da5583 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -290,6 +290,7 @@ def check_engine_alive(self) -> None: if self.engine_process.poll() is not None: logger.error(self.get_process_output(self.engine_process)) logger.error('[red]RPC server stopped unexpectedly, check the engine output above[/red]') + self.engine_running = False # for multi-processing factory.RPCFactory.clear() raise RuntimeError('RPC server stopped unexpectedly') time.sleep(5) @@ -306,6 +307,7 @@ def check_engine_alive_psutil(self) -> None: while self.engine_running: if not p.is_running(): logger.error('[red]RPC server stopped unexpectedly[/red]') + self.engine_running = False # for multi-processing factory.RPCFactory.clear() raise RuntimeError('RPC server stopped unexpectedly') time.sleep(5) From eb1c5c74dc91f3287ba93136d9753da04a089a28 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 22 Jan 2024 19:40:39 +0800 Subject: [PATCH 087/110] add instructions on develop plugins --- README.md | 19 +++++- docs/en/faq.rst | 62 ++++++++++++++++++- src/XRFeitoriaBpy/__init__.py | 1 + .../Private/CustomMoviePipelineOutput.cpp | 5 +- .../Private/XRFeitoriaUnreal.cpp | 4 +- .../Public/CustomMoviePipelineOutput.h | 9 +-- .../Public/MoviePipelineMeshOperator.h | 2 +- tests/unreal/main.py | 7 ++- xrfeitoria/factory.py | 2 + xrfeitoria/utils/anim/motion.py | 12 ++-- xrfeitoria/utils/publish_plugins.py | 5 +- xrfeitoria/utils/runner.py | 49 ++++++++++----- 12 files changed, 137 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 422602bf..3e1e3485 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,29 @@ The reference documentation is available on [readthedocs](https://xrfeitoria.rea There are several [tutorials](/tutorials/). You can read them [here](https://xrfeitoria.readthedocs.io/en/latest/src/Tutorials.html). - ### Sample codes There are several [samples](/samples/). Please follow the instructions [here](/samples/README.md). +### Use plugins under development + +Details can be found [here](https://xrfeitoria.readthedocs.io/en/latest/faq.html#how-to-use-the-plugin-of-blender-unreal-under-development). + +If you want to publish plugins of your own, you can use the following command: + +```powershell +# install xrfeitoria first +cd xrfeitoria +pip install . + +# for instance, build plugins for Blender, UE 5.1, UE 5.2, and UE 5.3 on Windows. +# using powershell where backtick(`) is the line continuation character. +python -m xrfeitoria.utils.publish_plugins ` + -u "C:/Program Files/Epic Games/UE_5.1/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" ` + -u "C:/Program Files/Epic Games/UE_5.2/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" ` + -u "C:/Program Files/Epic Games/UE_5.3/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" +``` ## :rocket: Amazing Projects Using XRFeitoria diff --git a/docs/en/faq.rst b/docs/en/faq.rst index 020da560..cf43d83b 100644 --- a/docs/en/faq.rst +++ b/docs/en/faq.rst @@ -7,13 +7,15 @@ We list some common troubles faced by many users and their corresponding solutio Feel free to enrich the list if you find any frequent issues and have ways to help others to solve them. If the contents here do not cover your issue, do not hesitate to create an issue! +----------- + API ---- -.. _FAQ-stencil-value: +.. _FAQ-Plugin: How to use the plugin of Blender/Unreal under development -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ First you should clone the repo of XRFeitoria, and maybe modify the code of the plugin under ``src/XRFeitoriaBlender`` or ``src/XRFeitoriaUnreal``. Then you can use the plugin under development by setting ``dev_plugin=True`` in :class:`init_blender ` or :class:`init_unreal `. @@ -45,6 +47,59 @@ You can install the plugin by: # or through the code in tests python -m tests.unreal.init --dev [-b] + +Build plugins +^^^^^^^^^^^^^^ + +If you want to publish plugins of your own, you can use the following command: + +.. code-block:: powershell + :linenos: + + # install xrfeitoria first + cd xrfeitoria + pip install . + + # for instance, build plugins for Blender, UE 5.1, UE 5.2, and UE 5.3 on Windows. + # using powershell where backtick(`) is the line continuation character. + python -m xrfeitoria.utils.publish_plugins ` + -u "C:/Program Files/Epic Games/UE_5.1/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" ` + -u "C:/Program Files/Epic Games/UE_5.2/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" ` + -u "C:/Program Files/Epic Games/UE_5.3/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" + +Please check the path ``./src/dist`` for the generated plugins. +``XRFeitoriaBlender`` will be archived by default, and ``XRFeitoriaUnreal`` will only be built when you specify the Unreal editor path. +Make sure you have installed the corresponding Unreal Engine and Visual Studio before building the unreal plugin. + +Find out the plugin version in ``./xrfeitoria/version.py``. Or by: + +.. code-block:: bash + + >>> python -c "import xrfeitoria; print(xrfeitoria.__version__)" + 0.6.1.dev10+gd12997e.d20240122 + +You can set the environment variable ``XRFEITORIA__DIST_ROOT`` and ``XRFEITORIA__VERSION`` to change the plugins used by XRFeitoria. +And run your code ``xxx.py`` like: + +.. tabs:: + .. tab:: UNIX + + .. code-block:: bash + + XRFEITORIA__DIST_ROOT=/path/to/src/dist \ + XRFEITORIA__VERSION=0.6.1.dev10+gd12997e.d20240122 \ + python xxx.py + + .. tab:: Windows + + .. code-block:: powershell + + $env:XRFEITORIA__DIST_ROOT="C:/path/to/src/dist"; ` + $env:XRFEITORIA__VERSION="0.6.1.dev10+gd12997e.d20240122"; ` + python xxx.py + +.. _FAQ-stencil-value: + What is ``stencil_value`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -92,10 +147,11 @@ you can set the environment variable ``BLENDER_PORT`` or ``UNREAL_PORT`` to chan .. tab:: Windows - .. code-block:: bash + .. code-block:: powershell $env:BLENDER_PORT=50051; python xxx.py +----------- Known Issues ------------- diff --git a/src/XRFeitoriaBpy/__init__.py b/src/XRFeitoriaBpy/__init__.py index ff22e14c..98f25074 100644 --- a/src/XRFeitoriaBpy/__init__.py +++ b/src/XRFeitoriaBpy/__init__.py @@ -8,6 +8,7 @@ 'blender': (3, 3, 0), 'category': 'Tools', } +__version__ = version = '0.6.0' def register(): diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp index 883607c1..df344c9f 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp @@ -13,10 +13,11 @@ #include "Misc/StringFormatArg.h" #include "Misc/FileHelper.h" #include "Misc/FrameRate.h" -#include "HAL/PlatformFilemanager.h" -#include "HAL/PlatformTime.h" #include "Misc/Paths.h" +// #include "HAL/PlatformFilemanager.h" +// #include "HAL/PlatformTime.h" + #include "Camera/CameraActor.h" #include "Camera/CameraComponent.h" #include "Engine/StaticMeshActor.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp index 765153bc..9caf2409 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp @@ -8,12 +8,12 @@ #include "CustomMoviePipelineOutput.h" #include "CustomMoviePipelineDeferredPass.h" -#define LOCTEXT_NAMESPACE "FXRFeitoriaGearModule" +#define LOCTEXT_NAMESPACE "FXRFeitoriaUnrealModule" void FXRFeitoriaUnrealModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module - UE_LOG(LogTemp, Log, TEXT( "XRFeitoriaGear Loaded. Doing initialization." )); + UE_LOG(LogTemp, Log, TEXT( "XRFeitoriaUnreal Loaded. Doing initialization." )); URendererSettings* Settings = GetMutableDefault(); #if ENGINE_MAJOR_VERSION == 5 diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h index c0a91a49..4d6a034f 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h @@ -42,12 +42,13 @@ THIRD_PARTY_INCLUDES_END #include "ImageWriteTask.h" #include "ImagePixelData.h" -#include "HAL/PlatformFilemanager.h" -#include "HAL/FileManager.h" +// #include "HAL/PlatformFilemanager.h" +// #include "HAL/FileManager.h" +// #include "HAL/PlatformTime.h" + #include "Misc/FileHelper.h" #include "Async/Async.h" #include "Misc/Paths.h" -#include "HAL/PlatformTime.h" #include "Math/Float16.h" #include "MovieRenderPipelineCoreModule.h" #include "MoviePipelineOutputSetting.h" @@ -105,7 +106,7 @@ struct XRFEITORIAUNREAL_API FCustomMoviePipelineRenderPass // UMaterialInterface* Material; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render Pass") - ECustomImageFormat Extension; + ECustomImageFormat Extension = ECustomImageFormat::PNG; FString SPassName; diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h index c8e7ed77..07fc6c46 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h @@ -6,7 +6,7 @@ #include "MoviePipelineOutputSetting.h" #include "MoviePipelineMasterConfig.h" #include "Misc/FileHelper.h" -#include "HAL/PlatformFilemanager.h" +// #include "HAL/PlatformFilemanager.h" #if WITH_EDITOR #include "MovieSceneExportMetadata.h" diff --git a/tests/unreal/main.py b/tests/unreal/main.py index 3ccc30b8..408a9da8 100644 --- a/tests/unreal/main.py +++ b/tests/unreal/main.py @@ -14,10 +14,10 @@ root = Path(__file__).parent -def main(debug: bool = False, background: bool = False): +def main(debug: bool = False, dev: bool = False, background: bool = False): logger = setup_logger(level='DEBUG' if debug else 'INFO', log_path=root / 'unreal.log') - with _init_unreal(background=background, dev_plugin=True) as xf_runner: + with _init_unreal(background=background, dev_plugin=dev) as xf_runner: init_test(debug=debug, background=background) actor_test(debug=debug, background=background) camera_test(debug=debug, background=background) @@ -31,6 +31,7 @@ def main(debug: bool = False, background: bool = False): args = argparse.ArgumentParser() args.add_argument('--debug', action='store_true') + args.add_argument('--dev', action='store_true') args = args.parse_args() - main(debug=args.debug) + main(debug=args.debug, dev=args.dev) diff --git a/xrfeitoria/factory.py b/xrfeitoria/factory.py index e86f1677..a98dc0b7 100644 --- a/xrfeitoria/factory.py +++ b/xrfeitoria/factory.py @@ -196,6 +196,7 @@ def __init__( If ``dev_plugin=True``, the plugin under local directory would be used, which is under ``src/XRFeitoriaBlender``. You should git clone first, and then use this option if you want to develop the plugin. + Please ref to :ref:`FAQ-Plugin`. .. code-block:: bash :linenos: @@ -281,6 +282,7 @@ def __init__( If ``dev_plugin=True``, the plugin under local directory would be used, which is under ``src/XRFeitoriaUnreal``. You should git clone first, and then use this option if you want to develop the plugin. + Please ref to :ref:`FAQ-Plugin`. .. code-block:: bash :linenos: diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index 0e00288e..d57101c9 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -342,10 +342,8 @@ def from_smpl_data( """Create SMPLMotion instance from smpl_data. `smpl_data` should be a dict like object, - with required keys: - ["body_pose", "global_orient"] - and optional key: - ["transl"]. + with required keys: ['betas', 'body_pose', 'global_orient'] + and optional key: ['transl'] Args: smpl_data: dict with require keys ["body_pose", "global_orient"] @@ -525,10 +523,8 @@ def from_smplx_data( """Create SMPLXMotion instance from smplx_data. `smplx_data` should be a dict like object, - with required keys: - ["body_pose", "global_orient"] - and optional key: - ["transl"] + with required keys: ['betas', "body_pose", "global_orient"] + and optional key: ['transl', 'jaw_pose', 'leye_pose', 'reye_pose', 'left_hand_pose', 'right_hand_pose', 'expression'] Args: smplx_data: require keys ["body_pose", "global_orient"] diff --git a/xrfeitoria/utils/publish_plugins.py b/xrfeitoria/utils/publish_plugins.py index 3590a87f..7e46ce64 100644 --- a/xrfeitoria/utils/publish_plugins.py +++ b/xrfeitoria/utils/publish_plugins.py @@ -21,6 +21,7 @@ project_root = root.parents[1] src_root = project_root / 'src' dist_root = src_root / 'dist' +dist_root.mkdir(exist_ok=True, parents=True) @contextmanager @@ -84,6 +85,9 @@ def update_bpy_version(bpy_init_file: Path): content = bpy_init_file.read_text() # update version content = re.sub(pattern=r"'version': \(.*\)", repl=f"'version': {__version_tuple__}", string=content) + content = re.sub( + pattern=r'__version__ = version = .*', repl=f"__version__ = version = '{__version__}'", string=content + ) bpy_init_file.write_text(content) logger.info(f'Updated "{bpy_init_file}" with version {__version__}') @@ -164,7 +168,6 @@ def wrapper( -u "C:/Program Files/Epic Games/UE_5.2/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" -u "C:/Program Files/Epic Games/UE_5.3/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" """ - print(unreal_exec) setup_logger(level='INFO') build_blender() if len(unreal_exec) > 0: diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 65da5583..17705a10 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -38,7 +38,7 @@ from .setup import get_exec_path # XXX: hardcode download url -oss_root = 'https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria' +dist_root = os.environ.get('XRFEITORIA__DIST_ROOT') or 'https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria' plugin_infos_json = Path(__file__).parent.resolve() / 'plugin_infos.json' plugin_info_type = TypedDict( 'PluginInfo', @@ -417,7 +417,7 @@ def dst_plugin_dir(self) -> Path: @property @lru_cache - def dst_plugin_version(self) -> str: + def installed_plugin_version(self) -> str: """Get plugin version installed.""" assert ( self.dst_plugin_dir.exists() @@ -426,10 +426,9 @@ def dst_plugin_version(self) -> str: if self.engine_type == EngineEnum.blender: init_file = self.dst_plugin_dir / '__init__.py' _content = init_file.read_text() - _match = re.search(r"'version': (.*?),\n", _content) + _match = re.search(r"__version__ = version = '(.*?)'", _content) if _match: - dst_plugin_version_tuple = eval(_match.group(1)) - dst_plugin_version = '.'.join(map(str, dst_plugin_version_tuple)) + dst_plugin_version = _match.groups()[0] else: raise ValueError("Failed to extract plugin version from '__init__.py'") elif self.engine_type == EngineEnum.unreal: @@ -495,7 +494,7 @@ def plugin_info(self) -> plugin_info_type: # plugin_infos = { "0.5.0": { "XRFeitoria": "0.5.0", "XRFeitoriaBpy": "0.5.0", "XRFeitoriaUnreal": "0.5.0" }, ... } plugin_infos: Dict[str, Dict[str, str]] = json.loads(plugin_infos_json.read_text()) plugin_versions = sorted((map(parse, plugin_infos.keys()))) - _version = parse(os.environ.get('XRFEITORIA__VERSION') or __version__) + _version = parse(__version__) # find compatible version, lower bound, e.g. 0.5.1 => 0.5.0 if _version in plugin_versions: @@ -503,6 +502,11 @@ def plugin_info(self) -> plugin_info_type: else: _idx = bisect_left(plugin_versions, parse(__version__)) - 1 compatible_version = plugin_versions[_idx] + + # read from env (highest priority) + if os.environ.get('XRFEITORIA__VERSION'): + compatible_version = parse(os.environ['XRFEITORIA__VERSION']) + logger.debug(f'Compatible plugin version: {compatible_version}') # get link @@ -514,7 +518,7 @@ def plugin_info(self) -> plugin_info_type: plugin_name = plugin_name_blender engine_version = 'None' # support all blender versions _platform = 'None' # support all platforms - plugin_version = plugin_infos[str(compatible_version)][plugin_name] + plugin_version = os.environ.get('XRFEITORIA__VERSION') or plugin_infos[str(compatible_version)][plugin_name] # e.g. XRFeitoriaBpy-0.5.0-None-None # e.g. XRFeitoriaUnreal-0.5.0-Unreal5.1-Windows return dict( @@ -527,18 +531,26 @@ def plugin_info(self) -> plugin_info_type: @property @lru_cache def plugin_url(self) -> Optional[str]: - # e.g. https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/plugins/XRFeitoriaBpy-0.5.0-None-None.zip - # e.g. https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/plugins/XRFeitoriaUnreal-0.5.0-Unreal5.1-Windows.zip - return f'{oss_root}/plugins/{plugin_name_pattern.format(**self.plugin_info)}.zip' + if dist_root.startswith('http'): + # e.g. https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/plugins/XRFeitoriaBpy-0.5.0-None-None.zip + # e.g. https://openxrlab-share.oss-cn-hongkong.aliyuncs.com/xrfeitoria/plugins/XRFeitoriaUnreal-0.5.0-Unreal5.1-Windows.zip + return f'{dist_root}/plugins/{plugin_name_pattern.format(**self.plugin_info)}.zip' + else: + # e.g. /path/to/dist/XRFeitoriaBpy-0.5.0-None-None.zip + # e.g. /path/to/dist/XRFeitoriaUnreal-0.5.0-Unreal5.1-Windows.zip + return f'{dist_root}/{plugin_name_pattern.format(**self.plugin_info)}.zip' def _install_plugin(self) -> None: """Install plugin.""" if self.dst_plugin_dir.exists(): - if parse(self.dst_plugin_version) < parse(self.plugin_info['plugin_version']): + if parse(self.installed_plugin_version) < parse(self.plugin_info['plugin_version']): self.replace_plugin = True - if parse(self.dst_plugin_version) > parse(self.plugin_info['plugin_version']) and not self.replace_plugin: + if ( + parse(self.installed_plugin_version) > parse(self.plugin_info['plugin_version']) + and not self.replace_plugin + ): logger.warning( - f'Plugin installed in "{self.dst_plugin_dir.as_posix()}" is in version {self.dst_plugin_version}, ' + f'Plugin installed in "{self.dst_plugin_dir.as_posix()}" is in version {self.installed_plugin_version}, ' f'newer than version {self.plugin_info["plugin_version"]} which is required by {package_name}-{__version__}. ' 'May cause unexpected errors.' ) @@ -608,7 +620,10 @@ def get_src_plugin_path(self) -> Path: logger.debug(f'Downloaded Plugin "{src_plugin_path.as_posix()}" exists') return src_plugin_path - plugin_path = self._download(url=self.plugin_url, dst_dir=src_plugin_root) + if self.plugin_url.startswith('http'): + plugin_path = self._download(url=self.plugin_url, dst_dir=src_plugin_root) + else: + plugin_path = Path(self.plugin_url) if plugin_path != src_plugin_path: shutil.move(plugin_path, src_plugin_path) return src_plugin_path @@ -715,7 +730,7 @@ def get_src_plugin_path(self) -> Path: 'Or clone the source code and build the plugin from source. ' 'https://github.com/openxrlab/xrfeitoria.git' ) - else: + elif self.plugin_url.startswith('http'): src_plugin_root = tmp_dir / 'plugins' src_plugin_compress = src_plugin_root / Path(self.plugin_url).name # with suffix (.zip) src_plugin_path = src_plugin_compress.with_suffix('') # without suffix (.zip) @@ -731,6 +746,10 @@ def get_src_plugin_path(self) -> Path: plugin_compress = self._download(url=self.plugin_url, dst_dir=src_plugin_root) shutil.unpack_archive(plugin_compress, src_plugin_root) assert src_plugin_path.exists(), f'Failed to download plugin to {src_plugin_path}' + else: + # custom dist path + plugin_compress = Path(self.plugin_url) + src_plugin_path = plugin_compress.with_suffix('') # without suffix (.zip) return src_plugin_path @staticmethod From 93729c2d417d5ecef31a95f66800a241cf621656 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 22 Jan 2024 20:17:46 +0800 Subject: [PATCH 088/110] optimize `MotionFrame` --- src/XRFeitoriaBpy/constants.py | 3 ++- src/XRFeitoriaBpy/core/factory.py | 6 +++--- src/XRFeitoriaUnreal/Content/Python/constants.py | 1 + src/XRFeitoriaUnreal/Content/Python/sequence.py | 16 +++++++--------- xrfeitoria/data_structure/constants.py | 3 ++- xrfeitoria/sequence/sequence_unreal.py | 12 ++++++------ xrfeitoria/sequence/sequence_unreal.pyi | 6 +++--- xrfeitoria/utils/anim/motion.py | 8 ++++---- xrfeitoria/utils/functions/blender_functions.py | 6 +++--- 9 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/XRFeitoriaBpy/constants.py b/src/XRFeitoriaBpy/constants.py index e672e6b7..0ed10206 100644 --- a/src/XRFeitoriaBpy/constants.py +++ b/src/XRFeitoriaBpy/constants.py @@ -1,6 +1,6 @@ from enum import Enum from pathlib import Path -from typing import List, Optional, Tuple, Type, TypeVar, Union +from typing import Dict, List, Optional, Tuple, Type, TypeVar, Union import bpy @@ -8,6 +8,7 @@ Tuple3 = Tuple[float, float, float] PathLike = Union[str, Path] +MotionFrame = Dict[str, Dict[str, Union[float, List[float]]]] class EnumBase(str, Enum): diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 10516fb5..16464178 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -8,7 +8,7 @@ import numpy as np from .. import logger -from ..constants import Tuple3 +from ..constants import MotionFrame, Tuple3 class XRFeitoriaBlenderFactory: @@ -1104,7 +1104,7 @@ def apply_action_to_actor(action: 'bpy.types.Action', actor: 'bpy.types.Object') actor.animation_data.action = action def apply_motion_data_to_action( - motion_data: 'List[Dict[str, Dict[str, List[float]]]]', + motion_data: 'List[MotionFrame]', action: 'bpy.types.Action', scale: float = 1.0, ) -> None: @@ -1158,7 +1158,7 @@ def _get_fcurve(data_path: str, index: int): # fcurve.keyframe_points[f].co = (f, val) fcurve.keyframe_points.insert(frame=f, value=val, options={'FAST'}) - def apply_motion_data_to_actor(motion_data: 'List[Dict[str, Dict[str, List[float]]]]', actor_name: str) -> None: + def apply_motion_data_to_actor(motion_data: 'List[MotionFrame]', actor_name: str) -> None: """Applies motion data to a given actor. Args: diff --git a/src/XRFeitoriaUnreal/Content/Python/constants.py b/src/XRFeitoriaUnreal/Content/Python/constants.py index c70ce505..5191b026 100644 --- a/src/XRFeitoriaUnreal/Content/Python/constants.py +++ b/src/XRFeitoriaUnreal/Content/Python/constants.py @@ -192,3 +192,4 @@ def __post_init__(self): TransformKeys = Union[List[SequenceTransformKey], SequenceTransformKey] +MotionFrame = Dict[str, Dict[str, Union[float, List[float]]]] diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 3f6918f2..8bcb1184 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -6,6 +6,7 @@ DEFAULT_SEQUENCE_DATA_ASSET, DEFAULT_SEQUENCE_PATH, ENGINE_MAJOR_VERSION, + MotionFrame, SequenceTransformKey, SubSystem, TransformKeys, @@ -391,15 +392,12 @@ def add_animation_to_binding( set_animation_by_section(animation_section, animation_asset, animation_length, seq_fps) -def add_fk_motion_to_binding( - binding: unreal.SequencerBindingProxy, - motion_data: List[Dict[str, Dict[str, Union[float, List[float]]]]], -) -> None: +def add_fk_motion_to_binding(binding: unreal.SequencerBindingProxy, motion_data: List[MotionFrame]) -> None: """Add FK motion to the given actor binding. Args: binding (unreal.SequencerBindingProxy): The binding of actor in sequence to add FK motion to. - motion_data (List[Dict[str, Dict[str, Union[float, List[float]]]]]): The FK motion data. + motion_data (List[MotionFrame]): The FK motion data. """ rig_track: unreal.MovieSceneControlRigParameterTrack = ( unreal.ControlRigSequencerLibrary.find_or_create_control_rig_track( @@ -682,7 +680,7 @@ def add_actor_to_sequence( actor_transform_keys: Optional[Union[SequenceTransformKey, List[SequenceTransformKey]]] = None, actor_stencil_value: int = 1, animation_asset: Optional[unreal.AnimSequence] = None, - motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, + motion_data: Optional[List[MotionFrame]] = None, seq_fps: Optional[float] = None, seq_length: Optional[int] = None, ) -> Dict[str, Any]: @@ -743,7 +741,7 @@ def add_spawnable_actor_to_sequence( actor_name: str, actor_asset: Union[unreal.SkeletalMesh, unreal.StaticMesh], animation_asset: Optional[unreal.AnimSequence] = None, - motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, + motion_data: Optional[List[MotionFrame]] = None, actor_transform_keys: Optional[Union[SequenceTransformKey, List[SequenceTransformKey]]] = None, actor_stencil_value: int = 1, seq_fps: Optional[float] = None, @@ -1102,7 +1100,7 @@ def add_actor( transform_keys: 'Optional[TransformKeys]' = None, stencil_value: int = 1, animation_asset: 'Optional[Union[str, unreal.AnimSequence]]' = None, - motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, + motion_data: 'Optional[List[MotionFrame]]' = None, ) -> None: """Spawn an actor in sequence. @@ -1112,7 +1110,7 @@ def add_actor( transform_keys (Optional[TransformKeys]): List of transform keys. Defaults to None. stencil_value (int): Stencil value of actor, used for specifying the mask color for this actor (mask id). Defaults to 1. animation_asset (Optional[Union[str, unreal.AnimSequence]]): animation path (e.g. '/Game/Anim') / loaded asset (via `unreal.load_asset('/Game/Anim')`). Can be None which means no animation. - motion_data (Optional[List[Dict[str, Dict[str, List[float]]]]]): The motion data used for FK animation. + motion_data (Optional[List[MotionFrame]]): The motion data used for FK animation. Raises: AssertionError: If `cls.sequence` is not initialized. diff --git a/xrfeitoria/data_structure/constants.py b/xrfeitoria/data_structure/constants.py index 5d511fea..0266d34d 100644 --- a/xrfeitoria/data_structure/constants.py +++ b/xrfeitoria/data_structure/constants.py @@ -1,6 +1,6 @@ from enum import Enum from pathlib import Path -from typing import Optional, Tuple, TypedDict, Union +from typing import Dict, List, Optional, Tuple, TypedDict, Union ##### Typing Constants ##### @@ -8,6 +8,7 @@ Matrix = Tuple[Vector, Vector, Vector] Transform = Tuple[Vector, Vector, Vector] PathLike = Union[str, Path] +MotionFrame = Dict[str, Dict[str, Union[float, List[float]]]] actor_info_type = TypedDict('actor_info', {'actor_name': str, 'mask_color': Tuple[int, int, int]}) ##### Package Constants ##### diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index e3493de4..4ae7eec7 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -4,7 +4,7 @@ from ..actor.actor_unreal import ActorUnreal from ..camera.camera_unreal import CameraUnreal -from ..data_structure.constants import PathLike, Vector +from ..data_structure.constants import MotionFrame, PathLike, Vector from ..object.object_utils import ObjectUtilsUnreal from ..renderer.renderer_unreal import RendererUnreal from ..rpc import remote_unreal @@ -125,7 +125,7 @@ def spawn_actor( actor_name: Optional[str] = None, stencil_value: int = 1, anim_asset_path: 'Optional[str]' = None, - motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, + motion_data: 'Optional[List[MotionFrame]]' = None, ) -> ActorUnreal: """Spawns an actor in the Unreal Engine at the specified location, rotation, and scale. @@ -140,7 +140,7 @@ def spawn_actor( stencil_value (int in [0, 255], optional): The stencil value to use for the spawned actor. Defaults to 1. Ref to :ref:`FAQ-stencil-value` for details. anim_asset_path (Optional[str], optional): The engine path to the animation asset of the actor. Defaults to None. - motion_data (Optional[List[Dict[str, Dict[str, List[float]]]]]): The motion data used for FK animation. + motion_data (Optional[List[MotionFrame]]): The motion data used for FK animation. Returns: ActorUnreal: The spawned actor object. @@ -169,7 +169,7 @@ def spawn_actor_with_keys( actor_name: Optional[str] = None, stencil_value: int = 1, anim_asset_path: 'Optional[str]' = None, - motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, + motion_data: 'Optional[]' = None, ) -> ActorUnreal: """Spawns an actor in the Unreal Engine with the given asset path, transform keys, actor name, stencil value, and animation asset path. @@ -181,7 +181,7 @@ def spawn_actor_with_keys( stencil_value (int in [0, 255], optional): The stencil value to use for the spawned actor. Defaults to 1. Ref to :ref:`FAQ-stencil-value` for details. anim_asset_path (Optional[str], optional): The engine path to the animation asset of the actor. Defaults to None. - motion_data (Optional[List[Dict[str, Dict[str, List[float]]]]]): The motion data used for FK animation. + motion_data (Optional[List[MotionFrame]]): The motion data used for FK animation. Returns: ActorUnreal: The spawned actor. @@ -468,7 +468,7 @@ def _spawn_actor_in_engine( actor_asset_path: str, transform_keys: 'Union[List[Dict], Dict]', anim_asset_path: 'Optional[str]' = None, - motion_data: 'Optional[List[Dict[str, Dict[str, List[float]]]]]' = None, + motion_data: 'Optional[List[MotionFrame]]' = None, actor_name: str = 'Actor', stencil_value: int = 1, ) -> None: diff --git a/xrfeitoria/sequence/sequence_unreal.pyi b/xrfeitoria/sequence/sequence_unreal.pyi index 6b55cd06..2038e6e4 100644 --- a/xrfeitoria/sequence/sequence_unreal.pyi +++ b/xrfeitoria/sequence/sequence_unreal.pyi @@ -2,7 +2,7 @@ from typing import Dict, List, Optional, Tuple from ..actor.actor_unreal import ActorUnreal from ..camera.camera_unreal import CameraUnreal -from ..data_structure.constants import PathLike, Vector +from ..data_structure.constants import MotionFrame, PathLike, Vector from ..data_structure.models import RenderJobUnreal, RenderPass, TransformKeys from ..object.object_utils import ObjectUtilsUnreal from ..renderer.renderer_unreal import RendererUnreal @@ -47,7 +47,7 @@ class SequenceUnreal(SequenceBase): actor_name: Optional[str] = None, stencil_value: int = 1, anim_asset_path: Optional[str] = None, - motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, + motion_data: Optional[List[MotionFrame]] = None, ) -> ActorUnreal: ... @classmethod def spawn_actor_with_keys( @@ -57,7 +57,7 @@ class SequenceUnreal(SequenceBase): actor_name: Optional[str] = None, stencil_value: int = 1, anim_asset_path: Optional[str] = None, - motion_data: Optional[List[Dict[str, Dict[str, List[float]]]]] = None, + motion_data: Optional[List[MotionFrame]] = None, ) -> ActorUnreal: ... @classmethod def use_camera( diff --git a/xrfeitoria/utils/anim/motion.py b/xrfeitoria/utils/anim/motion.py index d57101c9..7f7d5f1e 100644 --- a/xrfeitoria/utils/anim/motion.py +++ b/xrfeitoria/utils/anim/motion.py @@ -7,7 +7,7 @@ import numpy as np from scipy.spatial.transform import Rotation as spRotation -from ...data_structure.constants import PathLike +from ...data_structure.constants import MotionFrame, PathLike from .constants import ( NUM_SMPLX_BODYJOINTS, SMPL_IDX_TO_JOINTS, @@ -272,7 +272,7 @@ def insert_rest_pose(self): continue self.smplx_data[key] = np.insert(arr, 0, 0, axis=0) - def get_motion_data(self) -> List[Dict[str, Dict[str, List[float]]]]: + def get_motion_data(self) -> List[MotionFrame]: """Returns a list of dictionaries containing `rotation` and `location` for each bone of each frame in the animation. @@ -280,9 +280,9 @@ def get_motion_data(self) -> List[Dict[str, Dict[str, List[float]]]]: 'rotation' and 'location' keys, which correspond to the rotation and location of the bone in that frame. Returns: - List[Dict[str, Dict[str, List[float]]]]: A list of dictionaries containing motion data for each frame of the animation. + List[MotionFrame]: A list of dictionaries containing motion data for each frame of the animation. """ - motion_data: List[Dict[str, Dict[str, List[float]]]] = [] + motion_data: List[MotionFrame] = [] for frame in range(self.n_frames): frame_motion_data = {} for bone_name in self.BONE_NAMES: diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index e377ea1e..57e49049 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Dict, List, Literal, Optional, Tuple -from ...data_structure.constants import ImportFileFormatEnum, PathLike, Vector +from ...data_structure.constants import ImportFileFormatEnum, MotionFrame, PathLike, Vector from ...rpc import remote_blender try: @@ -67,11 +67,11 @@ def import_file(file_path: 'PathLike') -> None: @remote_blender() -def apply_motion_data_to_actor(motion_data: 'List[Dict[str, Dict[str, List[float]]]]', actor_name: str) -> None: +def apply_motion_data_to_actor(motion_data: 'List[MotionFrame]', actor_name: str) -> None: """Applies motion data to a given actor in Blender. Args: - motion_data (List[Dict[str, Dict[str, List[float]]]]): A list of dictionaries containing motion data for the actor. + motion_data (List[MotionFrame]): A list of dictionaries containing motion data for the actor. actor_name (str): The name of the actor to apply the motion data to. """ XRFeitoriaBlenderFactory.apply_motion_data_to_actor(motion_data=motion_data, actor_name=actor_name) From 66684244b9cfc9cbcc0a36c9a5b0a66fb3ecafda Mon Sep 17 00:00:00 2001 From: yangzhitao Date: Mon, 22 Jan 2024 20:59:00 +0800 Subject: [PATCH 089/110] fix use of ImageFileFormatEnum --- src/XRFeitoriaBpy/core/renderer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/XRFeitoriaBpy/core/renderer.py b/src/XRFeitoriaBpy/core/renderer.py index 1105bd1a..17e70a10 100644 --- a/src/XRFeitoriaBpy/core/renderer.py +++ b/src/XRFeitoriaBpy/core/renderer.py @@ -86,18 +86,18 @@ def set_file_format(context: bpy.types.Context, file_format: ImageFileFormatEnum @staticmethod def set_slot_to_jpg(node_slot: bpy.types.NodeOutputFileSlotFile): node_slot.use_node_format = False - node_slot.format.file_format = ImageFileFormatEnum.jpeg + node_slot.format.file_format = ImageFileFormatEnum.jpeg.value @staticmethod def set_slot_to_png(node_slot: bpy.types.NodeOutputFileSlotFile): node_slot.use_node_format = False - node_slot.format.file_format = ImageFileFormatEnum.png + node_slot.format.file_format = ImageFileFormatEnum.png.value @staticmethod def set_slot_to_exr(node_slot: bpy.types.NodeOutputFileSlotFile): """Set depth to save as float (EXR)""" node_slot.use_node_format = False - node_slot.format.file_format = ImageFileFormatEnum.exr + node_slot.format.file_format = ImageFileFormatEnum.exr.value node_slot.format.color_depth = '32' @staticmethod From a3d02267b252b1ac0d9a91099d96c230ef4cfa1e Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Mon, 22 Jan 2024 21:27:39 +0800 Subject: [PATCH 090/110] Add FAQ section to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3e1e3485..2c3a52de 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ python -m xrfeitoria.utils.publish_plugins ` -u "C:/Program Files/Epic Games/UE_5.3/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" ``` +### Frequently Asked Questions + +Please refer to [FAQ](https://xrfeitoria.readthedocs.io/en/latest/faq.html). + ## :rocket: Amazing Projects Using XRFeitoria From 22d5912eaf46f3d18e2421ac6e349e46640d59ce Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Tue, 23 Jan 2024 13:30:10 +0800 Subject: [PATCH 091/110] Fix sequence binding issue and stop engine process --- .../Content/Python/sequence.py | 25 +++++++++++------- xrfeitoria/utils/runner.py | 26 +++++++++++++------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 8bcb1184..76e2c303 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -856,6 +856,8 @@ class Sequence: sequence_path = None sequence_data_asset: unreal.DataAsset = None # contains sequence_path and map_path sequence: unreal.LevelSequence = None + # TODO: make this work + # Currently if there's value in bindings and exited accidentally, the value will be kept and cause error bindings: Dict[str, Dict[str, Any]] = {} START_FRAME = -1 @@ -931,13 +933,16 @@ def new( if seq_dir is None: seq_dir = DEFAULT_SEQUENCE_PATH - data_asset_path = f'{seq_dir}/{seq_name}{data_asset_suffix}' - if unreal.EditorAssetLibrary.does_asset_exist(f'{seq_dir}/{seq_name}'): + seq_path = f'{seq_dir}/{seq_name}' + data_asset_path = f'{seq_path}{data_asset_suffix}' + if unreal.EditorAssetLibrary.does_asset_exist(seq_path) or unreal.EditorAssetLibrary.does_asset_exist( + data_asset_path + ): if replace: - unreal.EditorAssetLibrary.delete_asset(f'{seq_dir}/{seq_name}') + unreal.EditorAssetLibrary.delete_asset(seq_path) unreal.EditorAssetLibrary.delete_asset(data_asset_path) else: - raise Exception(f'Sequence `{seq_dir}/{seq_name}` already exists, use `replace=True` to replace it') + raise Exception(f'Sequence `{seq_path}` already exists, use `replace=True` to replace it') unreal.EditorLoadingAndSavingUtils.load_map(map_path) cls.map_path = map_path @@ -947,7 +952,7 @@ def new( seq_fps=seq_fps, seq_length=seq_length, ) - cls.sequence_path = f'{seq_dir}/{seq_name}' + cls.sequence_path = seq_path cls.sequence_data_asset = cls.new_data_asset( asset_path=data_asset_path, @@ -1081,7 +1086,7 @@ def add_camera( camera_transform_keys=transform_keys, camera_fov=fov, ) - cls.bindings[camera_name] = bindings + # cls.bindings[camera_name] = bindings else: camera = utils_actor.get_actor_by_name(camera_name) bindings = add_camera_to_sequence( @@ -1090,7 +1095,7 @@ def add_camera( camera_transform_keys=transform_keys, camera_fov=fov, ) - cls.bindings[camera_name] = bindings + # cls.bindings[camera_name] = bindings @classmethod def add_actor( @@ -1132,7 +1137,7 @@ def add_actor( actor_transform_keys=transform_keys, actor_stencil_value=stencil_value, ) - cls.bindings[actor_name] = bindings + # cls.bindings[actor_name] = bindings else: actor = utils_actor.get_actor_by_name(actor_name) @@ -1144,7 +1149,7 @@ def add_actor( animation_asset=animation_asset, motion_data=motion_data, ) - cls.bindings[actor_name] = bindings + # cls.bindings[actor_name] = bindings @classmethod def add_audio( @@ -1169,7 +1174,7 @@ def add_audio( bindings = add_audio_to_sequence( sequence=cls.sequence, audio_asset=audio_asset, start_frame=start_frame, end_frame=end_frame ) - cls.bindings[audio_asset.get_name()] = bindings + # cls.bindings[audio_asset.get_name()] = bindings if __name__ == '__main__': diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index 17705a10..a10b6f54 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -119,6 +119,10 @@ def __init__( self.background = background self.debug = logger._core.min_level <= 10 # DEBUG level + # child threads + self.thread_engine_alive: Optional[threading.Thread] = None + self.thread_receive_stdout: Optional[threading.Thread] = None + if reload_rpc_code: # clear registered functions and classes for reloading factory.RPCFactory.registered_function_names.clear() @@ -197,6 +201,13 @@ def stop(self) -> None: # clear rpc server factory.RPCFactory.clear() + # stop threads + self.engine_running = False + if self.thread_receive_stdout: + self.thread_receive_stdout.join() + if self.thread_engine_alive: + self.thread_engine_alive.join() + # stop engine process process = self.engine_process if process is not None: @@ -207,12 +218,11 @@ def stop(self) -> None: logger.debug(f'Killing child process {child.pid}') child.kill() process.kill() - self.engine_running = False self.engine_process = None self.engine_pid = None - if hasattr(_tls, 'cache'): # prevent to be called from another thread - _tls.cache['engine_process'] = None - _tls.cache['engine_pid'] = None + # prevent to be called from another thread + _tls.cache['engine_process'] = None + _tls.cache['engine_pid'] = None else: logger.info(':bell: [bold red]Exiting runner[/bold red], reused engine process remains') @@ -328,7 +338,7 @@ def start(self) -> None: """Start rpc server.""" if not self.new_process: self.engine_running = True - threading.Thread(target=self.check_engine_alive_psutil, daemon=True).start() + self.thread_engine_alive = threading.Thread(target=self.check_engine_alive_psutil, daemon=True).start() return with self.console.status('Initializing RPC server...') as status: @@ -343,8 +353,8 @@ def start(self) -> None: logger.info(f'RPC server started at port {self.port}') # check if engine process is alive in a separate thread - threading.Thread(target=self._receive_stdout, daemon=True).start() - threading.Thread(target=self.check_engine_alive, daemon=True).start() + self.thread_receive_stdout = threading.Thread(target=self._receive_stdout, daemon=True).start() + self.thread_engine_alive = threading.Thread(target=self.check_engine_alive, daemon=True).start() def wait_for_start(self, process: subprocess.Popen) -> None: """Wait 3 minutes for RPC server to start. @@ -542,7 +552,7 @@ def plugin_url(self) -> Optional[str]: def _install_plugin(self) -> None: """Install plugin.""" - if self.dst_plugin_dir.exists(): + if self.dst_plugin_dir.exists() and not self.replace_plugin: if parse(self.installed_plugin_version) < parse(self.plugin_info['plugin_version']): self.replace_plugin = True if ( From ada29d7894665109441189be31d0a1da886d5d50 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Tue, 23 Jan 2024 18:46:14 +0800 Subject: [PATCH 092/110] Update read XRFeitoria version --- .gitignore | 1 + docs/en/faq.rst | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 2d1d49de..709bb750 100644 --- a/.gitignore +++ b/.gitignore @@ -515,6 +515,7 @@ src/XRFeitoriaUnreal/Binaries/ **/output/ # Python +scripts/ xrfeitoria/version.py # Tutorials diff --git a/docs/en/faq.rst b/docs/en/faq.rst index cf43d83b..eb7a79ab 100644 --- a/docs/en/faq.rst +++ b/docs/en/faq.rst @@ -79,23 +79,23 @@ Find out the plugin version in ``./xrfeitoria/version.py``. Or by: 0.6.1.dev10+gd12997e.d20240122 You can set the environment variable ``XRFEITORIA__DIST_ROOT`` and ``XRFEITORIA__VERSION`` to change the plugins used by XRFeitoria. -And run your code ``xxx.py`` like: +Run your code ``xxx.py`` like: .. tabs:: .. tab:: UNIX .. code-block:: bash - XRFEITORIA__DIST_ROOT=/path/to/src/dist \ - XRFEITORIA__VERSION=0.6.1.dev10+gd12997e.d20240122 \ + XRFEITORIA__VERSION=$(python -c "import xrfeitoria; print(xrfeitoria.__version__)") \ + XRFEITORIA__DIST_ROOT=src/dist \ python xxx.py .. tab:: Windows .. code-block:: powershell - $env:XRFEITORIA__DIST_ROOT="C:/path/to/src/dist"; ` - $env:XRFEITORIA__VERSION="0.6.1.dev10+gd12997e.d20240122"; ` + $env:XRFEITORIA__VERSION=$(python -c "import xrfeitoria; print(xrfeitoria.__version__)") + $env:XRFEITORIA__DIST_ROOT="src/dist"; ` python xxx.py .. _FAQ-stencil-value: From 5369d558b3eb220f6451947abae85c95fc2a8fdf Mon Sep 17 00:00:00 2001 From: yangzhitao Date: Tue, 23 Jan 2024 19:35:39 +0800 Subject: [PATCH 093/110] Fix UnicodeEncodingError during logging --- xrfeitoria/utils/tools.py | 59 +++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index 74463883..62d29bce 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -1,12 +1,13 @@ """Utils tools for logging and progress bar.""" - +import os +import sys from pathlib import Path from typing import Iterable, Literal, Optional, Sequence, Tuple, Union import loguru from loguru import logger -from rich import print as rprint +from rich.console import Console from rich.progress import ( BarColumn, MofNCompleteColumn, @@ -84,12 +85,21 @@ def setup_logging( if cls.is_setup: return logger + cls.setup_encoding() + # add custom level called RPC, which is the minimum level logger.level('RPC', no=1, color='', icon='📢') logger.remove() # remove default logger - logger.add(sink=lambda msg: rprint(msg, end=''), level=level, format=cls.logger_format) - # logger.add(RichHandler(level=level, rich_tracebacks=True, markup=True), level=level, format='{message}') + # logger.add(sink=lambda msg: rprint(msg, end=''), level=level, format=cls.logger_format) + + c = Console( + # width=sys.maxsize, # disable wrapping + log_time=False, + log_path=False, + log_time_format='', + ) + logger.add(sink=lambda msg: c.print(msg, end=''), level=level, format=cls.logger_format) if log_path: # add file logger log_path = Path(log_path).resolve() @@ -102,6 +112,38 @@ def setup_logging( cls.is_setup = True return logger + @staticmethod + def setup_encoding( + encoding: Optional[str] = None, + errorhandler: Literal['ignore', 'replace', 'backslashreplace', 'xmlcharrefreplace'] = 'backslashreplace', + ): + """Modify `PYTHONIOENCODING` to prevent suppress UnicodeEncodeError caused by logging of emojis. + It will affect the default behavior of `sys.stdin`, `sys.stdout` and `sys.stderr`. + Ref: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONIOENCODING + + Args: + errorhandler (Literal['ignore', 'replace', 'backslashreplace', 'xmlcharrefreplace'], optional): + specify which error handler to handle unsupported characters. 'strict' is forbidden. Defaults to 'replace'. + Ref to https://docs.python.org/3/library/stdtypes.html#str.encode + """ + encodingname, _, handler = os.environ.get("PYTHONIOENCODING", "").lower().partition(":") + encoding = encodingname if encodingname else encoding + # Set an errorhandler except for "strict" + if handler in ("ignore", "replace", "backslashreplace", "xmlcharrefreplace"): + errorhandler = handler + elif handler in ("", "strict"): + print( + f'PYTHONIOENCODING is going to use "strict" errorhandler, which could raise errors during logging. Reset to "{errorhandler}"', + file=sys.stderr, + ) + else: + print( + f'PYTHONIOENCODING is set with invalid errorhandler "{handler}". Reset to "{errorhandler}"', + file=sys.stderr, + ) + # if not encoding.lower().startswith("utf"): + os.environ["PYTHONIOENCODING"] = f"{encoding or ''}:{errorhandler}" + def setup_logger( level: Literal['RPC', 'TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO', @@ -123,7 +165,14 @@ def setup_logger( log_path (Path, optional): path to save the log file. Defaults to None. replace (bool, optional): replace the log file if exists. Defaults to True. """ - return LoggerWrapper.setup_logging(level, log_path, replace) + try: + return LoggerWrapper.setup_logging(level, log_path, replace) + except Exception as e: + import traceback + + print(repr(e)) + print(traceback.format_exc()) + raise e #### (rich) progress bar #### From ba1d16310831bb2ecae420a2e21e620208e0dfa6 Mon Sep 17 00:00:00 2001 From: yangzhitao Date: Tue, 23 Jan 2024 20:03:01 +0800 Subject: [PATCH 094/110] cancel logging wraps introduced by rich --- xrfeitoria/utils/tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index 62d29bce..1e6c4d0d 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -92,9 +92,8 @@ def setup_logging( logger.remove() # remove default logger # logger.add(sink=lambda msg: rprint(msg, end=''), level=level, format=cls.logger_format) - c = Console( - # width=sys.maxsize, # disable wrapping + width=sys.maxsize, # disable wrapping log_time=False, log_path=False, log_time_format='', From f93d86b1b32bd33c3591db273416b4e56a000f7d Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Tue, 23 Jan 2024 21:26:54 +0800 Subject: [PATCH 095/110] Fix type hint and pre-commit --- xrfeitoria/sequence/sequence_unreal.py | 2 +- xrfeitoria/utils/tools.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xrfeitoria/sequence/sequence_unreal.py b/xrfeitoria/sequence/sequence_unreal.py index 4ae7eec7..b5d059e9 100644 --- a/xrfeitoria/sequence/sequence_unreal.py +++ b/xrfeitoria/sequence/sequence_unreal.py @@ -169,7 +169,7 @@ def spawn_actor_with_keys( actor_name: Optional[str] = None, stencil_value: int = 1, anim_asset_path: 'Optional[str]' = None, - motion_data: 'Optional[]' = None, + motion_data: 'Optional[List[MotionFrame]]' = None, ) -> ActorUnreal: """Spawns an actor in the Unreal Engine with the given asset path, transform keys, actor name, stencil value, and animation asset path. diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index 1e6c4d0d..97ad47c2 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -125,12 +125,12 @@ def setup_encoding( specify which error handler to handle unsupported characters. 'strict' is forbidden. Defaults to 'replace'. Ref to https://docs.python.org/3/library/stdtypes.html#str.encode """ - encodingname, _, handler = os.environ.get("PYTHONIOENCODING", "").lower().partition(":") + encodingname, _, handler = os.environ.get('PYTHONIOENCODING', '').lower().partition(':') encoding = encodingname if encodingname else encoding # Set an errorhandler except for "strict" - if handler in ("ignore", "replace", "backslashreplace", "xmlcharrefreplace"): + if handler in ('ignore', 'replace', 'backslashreplace', 'xmlcharrefreplace'): errorhandler = handler - elif handler in ("", "strict"): + elif handler in ('', 'strict'): print( f'PYTHONIOENCODING is going to use "strict" errorhandler, which could raise errors during logging. Reset to "{errorhandler}"', file=sys.stderr, @@ -141,7 +141,7 @@ def setup_encoding( file=sys.stderr, ) # if not encoding.lower().startswith("utf"): - os.environ["PYTHONIOENCODING"] = f"{encoding or ''}:{errorhandler}" + os.environ['PYTHONIOENCODING'] = f"{encoding or ''}:{errorhandler}" def setup_logger( From ac18fc9c6afb7554248a8c01fba0c2a151370da7 Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Tue, 23 Jan 2024 23:41:59 +0800 Subject: [PATCH 096/110] add support for ue5.1 --- src/XRFeitoriaUnreal/Content/Python/sequence.py | 13 +++++++++---- xrfeitoria/sequence/sequence_wrapper.py | 6 +++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 76e2c303..4fea1e18 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -6,6 +6,7 @@ DEFAULT_SEQUENCE_DATA_ASSET, DEFAULT_SEQUENCE_PATH, ENGINE_MAJOR_VERSION, + ENGINE_MINOR_VERSION, MotionFrame, SequenceTransformKey, SubSystem, @@ -418,10 +419,14 @@ def add_fk_motion_to_binding(binding: unreal.SequencerBindingProxy, motion_data: rig_proxies = unreal.ControlRigSequencerLibrary.get_control_rigs(binding.sequence) for rig_proxy in rig_proxies: - ### TODO: judge if the track belongs to this actor - unreal.ControlRigSequencerLibrary.set_control_rig_apply_mode( - rig_proxy.control_rig, unreal.ControlRigFKRigExecuteMode.ADDITIVE - ) + if ENGINE_MAJOR_VERSION == 5 and ENGINE_MINOR_VERSION < 2: + msg = 'FKRigExecuteMode is not supported in < UE5.2, may cause unexpected result using FK motion.' + unreal.log_warning(msg) + else: + ### TODO: judge if the track belongs to this actor + unreal.ControlRigSequencerLibrary.set_control_rig_apply_mode( + rig_proxy.control_rig, unreal.ControlRigFKRigExecuteMode.ADDITIVE + ) def get_transform_from_bone_data(bone_data: Dict[str, List[float]]): quat: Tuple[float, float, float, float] = bone_data.get('rotation') diff --git a/xrfeitoria/sequence/sequence_wrapper.py b/xrfeitoria/sequence/sequence_wrapper.py index 7da7d8c7..4dd0d09b 100644 --- a/xrfeitoria/sequence/sequence_wrapper.py +++ b/xrfeitoria/sequence/sequence_wrapper.py @@ -188,7 +188,11 @@ def sequence_wrapper_unreal( default_sequence_path = SequenceUnreal._get_default_seq_path_in_engine() seq_dir = seq_dir or default_sequence_path - if unreal_functions.check_asset_in_engine(f'{seq_dir}/{seq_name}') and not replace: + if ( + unreal_functions.check_asset_in_engine(f'{seq_dir}/{seq_name}') + and unreal_functions.check_asset_in_engine(f'{seq_dir}/{seq_name}_data') + and not replace + ): SequenceUnreal._open(seq_name=seq_name, seq_dir=seq_dir) else: SequenceUnreal._new( From 3d05f326e581dd97c5cb0f489649ec99d125b43d Mon Sep 17 00:00:00 2001 From: meihaiyi Date: Tue, 23 Jan 2024 23:42:20 +0800 Subject: [PATCH 097/110] no print --- xrfeitoria/utils/tools.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/xrfeitoria/utils/tools.py b/xrfeitoria/utils/tools.py index 97ad47c2..1ec42253 100644 --- a/xrfeitoria/utils/tools.py +++ b/xrfeitoria/utils/tools.py @@ -130,16 +130,16 @@ def setup_encoding( # Set an errorhandler except for "strict" if handler in ('ignore', 'replace', 'backslashreplace', 'xmlcharrefreplace'): errorhandler = handler - elif handler in ('', 'strict'): - print( - f'PYTHONIOENCODING is going to use "strict" errorhandler, which could raise errors during logging. Reset to "{errorhandler}"', - file=sys.stderr, - ) - else: - print( - f'PYTHONIOENCODING is set with invalid errorhandler "{handler}". Reset to "{errorhandler}"', - file=sys.stderr, - ) + # elif handler in ('', 'strict'): + # print( + # f'PYTHONIOENCODING is going to use "strict" errorhandler, which could raise errors during logging. Reset to "{errorhandler}"', + # file=sys.stderr, + # ) + # else: + # print( + # f'PYTHONIOENCODING is set with invalid errorhandler "{handler}". Reset to "{errorhandler}"', + # file=sys.stderr, + # ) # if not encoding.lower().startswith("utf"): os.environ['PYTHONIOENCODING'] = f"{encoding or ''}:{errorhandler}" From 58f1ada79c17df94c10a55633a41dc965a4a2816 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 24 Jan 2024 13:15:19 +0800 Subject: [PATCH 098/110] Optimize --- samples/unreal/07_amass.py | 2 +- src/XRFeitoriaBpy/core/factory.py | 20 ++++++++++++------- src/XRFeitoriaUnreal/Content/Python/utils.py | 4 ++-- xrfeitoria/cmd/blender/vis_smplx.py | 2 +- xrfeitoria/object/object_utils.py | 2 +- xrfeitoria/renderer/renderer_blender.py | 2 +- .../utils/functions/blender_functions.py | 2 +- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/samples/unreal/07_amass.py b/samples/unreal/07_amass.py index 8fa83e29..e0e8ae22 100644 --- a/samples/unreal/07_amass.py +++ b/samples/unreal/07_amass.py @@ -35,7 +35,7 @@ # 3. Define the output file path seq_name = 'seq_amass' -output_path = Path(__file__).parents[2].resolve() / 'output/samples/unreal' / Path(__file__).stem +output_path = Path(__file__).resolve().parents[2] / 'output/samples/unreal' / Path(__file__).stem output_path.mkdir(parents=True, exist_ok=True) saved_humandata_file = output_path / 'output.npz' diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 16464178..831df2bb 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -2,7 +2,7 @@ import math from contextlib import contextmanager from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Dict, List, Literal, NamedTuple, Optional, Tuple, Union import bpy import numpy as np @@ -11,6 +11,16 @@ from ..constants import MotionFrame, Tuple3 +class SequenceProperties(NamedTuple): + level: bpy.types.Scene + fps: int + frame_start: int + frame_end: int + frame_current: int + resolution_x: int + resolution_y: int + + class XRFeitoriaBlenderFactory: ##################################### ######## Scene & Collection ######### @@ -146,18 +156,14 @@ def set_sequence_properties( collection.sequence_properties.resolution_x = resolution_x collection.sequence_properties.resolution_y = resolution_y - def get_sequence_properties( - collection: 'bpy.types.Collection', - ) -> 'Tuple[bpy.types.Scene, int, int, int, int, int, int]': + def get_sequence_properties(collection: 'bpy.types.Collection') -> SequenceProperties: """Get the sequence properties. Args: collection (bpy.types.Collection): Collection of the sequence. Returns: - Tuple[bpy.types.Scene, int, int, int, int, int, int]: - The level(scene), FPS of the sequence, Start frame of the sequence, End frame of the sequence, Current frame of the sequence, - Resolution_x of the sequence, Resolution_y of the sequence. + SequenceProperties: Sequence properties. """ level = collection.sequence_properties.level fps = collection.sequence_properties.fps diff --git a/src/XRFeitoriaUnreal/Content/Python/utils.py b/src/XRFeitoriaUnreal/Content/Python/utils.py index cb487f13..fb81817b 100644 --- a/src/XRFeitoriaUnreal/Content/Python/utils.py +++ b/src/XRFeitoriaUnreal/Content/Python/utils.py @@ -116,7 +116,7 @@ def import_asset( import_tasks = [import_task] asset_tools.import_asset_tasks(import_tasks) asset_paths.extend( - [path.split('.')[0] for path in import_task.get_editor_property('imported_object_paths')] + [path.partition('.')[0] for path in import_task.get_editor_property('imported_object_paths')] ) else: assetsTools = unreal.AssetToolsHelpers.get_asset_tools() @@ -125,7 +125,7 @@ def import_asset( assetImportData.filenames = [path] assetImportData.replace_existing = replace assets: List[unreal.Object] = assetsTools.import_assets_automated(assetImportData) - asset_paths.extend([asset.get_path_name().split('.')[0] for asset in assets]) + asset_paths.extend([asset.get_path_name().partition('.')[0] for asset in assets]) unreal.EditorAssetLibrary.save_directory(dst_dir, False, True) # save assets unreal.log(f'Imported asset: {path}') return asset_paths diff --git a/xrfeitoria/cmd/blender/vis_smplx.py b/xrfeitoria/cmd/blender/vis_smplx.py index 48c5b431..58a94e65 100644 --- a/xrfeitoria/cmd/blender/vis_smplx.py +++ b/xrfeitoria/cmd/blender/vis_smplx.py @@ -25,7 +25,7 @@ @remote_blender() -def add_smplx(betas: 'Tuple[float, ...]' = [0.0] * 10, gender: str = 'neutral') -> str: +def add_smplx(betas: 'Tuple[float, ...]' = (0.0,) * 10, gender: str = 'neutral') -> str: """Add smplx mesh to scene and return the name of the armature and the mesh. Args: diff --git a/xrfeitoria/object/object_utils.py b/xrfeitoria/object/object_utils.py index 5d201e37..7ee5eae5 100644 --- a/xrfeitoria/object/object_utils.py +++ b/xrfeitoria/object/object_utils.py @@ -8,7 +8,7 @@ from ..utils.functions import blender_functions try: - # only for linting, not imported in runtime + # linting and for engine import bpy import unreal from unreal_factory import XRFeitoriaUnrealFactory # defined in src/XRFeitoriaUnreal/Content/Python diff --git a/xrfeitoria/renderer/renderer_blender.py b/xrfeitoria/renderer/renderer_blender.py index 1de22e34..fdf570c5 100644 --- a/xrfeitoria/renderer/renderer_blender.py +++ b/xrfeitoria/renderer/renderer_blender.py @@ -24,7 +24,7 @@ from .renderer_base import RendererBase, render_status try: - # only for linting, not imported in runtime + # linting and for engine from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py except ModuleNotFoundError: pass diff --git a/xrfeitoria/utils/functions/blender_functions.py b/xrfeitoria/utils/functions/blender_functions.py index 57e49049..321352e4 100644 --- a/xrfeitoria/utils/functions/blender_functions.py +++ b/xrfeitoria/utils/functions/blender_functions.py @@ -7,7 +7,7 @@ from ...rpc import remote_blender try: - # only for linting, not imported in runtime + # linting and for engine import bpy from XRFeitoriaBpy import logger # defined in src/XRFeitoriaBpy/__init__.py from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py From 182d4e9cc9366ffdde10e1105c1fb7a622c14ee7 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 24 Jan 2024 13:27:00 +0800 Subject: [PATCH 099/110] Fix upper/lower case --- src/XRFeitoriaUnreal/Content/Python/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/utils.py b/src/XRFeitoriaUnreal/Content/Python/utils.py index fb81817b..45b7aef1 100644 --- a/src/XRFeitoriaUnreal/Content/Python/utils.py +++ b/src/XRFeitoriaUnreal/Content/Python/utils.py @@ -100,7 +100,7 @@ def import_asset( continue unreal.log(f'Importing asset: {path}') - if path.endswith('.fbx'): + if path.lower().endswith('.fbx'): asset_tools = unreal.AssetToolsHelpers.get_asset_tools() import_options = unreal.FbxImportUI() import_options.set_editor_property('import_animations', True) From 17a3e7aec1fb07bb619b36e2739adad91903b6d9 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 24 Jan 2024 13:30:21 +0800 Subject: [PATCH 100/110] resolve().parents --- samples/blender/01_add_shapes.py | 2 +- samples/blender/02_add_cameras.py | 2 +- samples/blender/03_basic_render.py | 2 +- samples/blender/04_staticmesh_render.py | 2 +- samples/blender/05_skeletalmesh_render.py | 2 +- samples/blender/06_custom_usage.py | 2 +- samples/blender/07_amass.py | 2 +- samples/unreal/01_add_shapes.py | 2 +- samples/unreal/02_add_cameras.py | 2 +- samples/unreal/03_basic_render.py | 2 +- samples/unreal/04_staticmesh_render.py | 2 +- samples/unreal/05_skeletalmesh_render.py | 2 +- samples/unreal/06_custom_usage.py | 2 +- tests/blender/actor.py | 2 +- tests/blender/camera.py | 2 +- tests/blender/level.py | 2 +- tests/blender/main.py | 2 +- tests/blender/sequence.py | 2 +- tests/unreal/sequence.py | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/samples/blender/01_add_shapes.py b/samples/blender/01_add_shapes.py index 046580b7..4179b058 100644 --- a/samples/blender/01_add_shapes.py +++ b/samples/blender/01_add_shapes.py @@ -10,7 +10,7 @@ from ..config import blender_exec -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'blender.log' diff --git a/samples/blender/02_add_cameras.py b/samples/blender/02_add_cameras.py index 420dc046..41d1aa33 100644 --- a/samples/blender/02_add_cameras.py +++ b/samples/blender/02_add_cameras.py @@ -11,7 +11,7 @@ from ..config import blender_exec -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'blender.log' diff --git a/samples/blender/03_basic_render.py b/samples/blender/03_basic_render.py index 9abf1add..0f284102 100644 --- a/samples/blender/03_basic_render.py +++ b/samples/blender/03_basic_render.py @@ -11,7 +11,7 @@ from ..config import assets_path, blender_exec -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'blender.log' diff --git a/samples/blender/04_staticmesh_render.py b/samples/blender/04_staticmesh_render.py index f491b947..5bc937c2 100644 --- a/samples/blender/04_staticmesh_render.py +++ b/samples/blender/04_staticmesh_render.py @@ -14,7 +14,7 @@ from ..config import assets_path, blender_exec from ..utils import visualize_vertices -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'blender.log' diff --git a/samples/blender/05_skeletalmesh_render.py b/samples/blender/05_skeletalmesh_render.py index 5e4082a9..a16a337d 100644 --- a/samples/blender/05_skeletalmesh_render.py +++ b/samples/blender/05_skeletalmesh_render.py @@ -12,7 +12,7 @@ from ..config import assets_path, blender_exec -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'blender.log' diff --git a/samples/blender/06_custom_usage.py b/samples/blender/06_custom_usage.py index 6d1f560b..bf1020f0 100644 --- a/samples/blender/06_custom_usage.py +++ b/samples/blender/06_custom_usage.py @@ -11,7 +11,7 @@ from ..config import blender_exec -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'blender.log' diff --git a/samples/blender/07_amass.py b/samples/blender/07_amass.py index 91dc28dd..1f1534ef 100644 --- a/samples/blender/07_amass.py +++ b/samples/blender/07_amass.py @@ -36,7 +36,7 @@ # 3. Define the output file path seq_name = 'seq_amass' -output_path = Path(__file__).parents[2].resolve() / 'output/samples/blender' / Path(__file__).stem +output_path = Path(__file__).resolve().parents[2] / 'output/samples/blender' / Path(__file__).stem output_path.mkdir(parents=True, exist_ok=True) saved_humandata_file = output_path / 'output.npz' saved_blend_file = output_path / 'output.blend' diff --git a/samples/unreal/01_add_shapes.py b/samples/unreal/01_add_shapes.py index 3b8bd823..a0906c92 100644 --- a/samples/unreal/01_add_shapes.py +++ b/samples/unreal/01_add_shapes.py @@ -11,7 +11,7 @@ from ..config import unreal_exec, unreal_project -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') default_level = '/Game/Levels/Default' diff --git a/samples/unreal/02_add_cameras.py b/samples/unreal/02_add_cameras.py index bb34785b..53cd8c74 100644 --- a/samples/unreal/02_add_cameras.py +++ b/samples/unreal/02_add_cameras.py @@ -11,7 +11,7 @@ from ..config import unreal_exec, unreal_project -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'unreal.log' diff --git a/samples/unreal/03_basic_render.py b/samples/unreal/03_basic_render.py index 1fef2518..9a72741c 100644 --- a/samples/unreal/03_basic_render.py +++ b/samples/unreal/03_basic_render.py @@ -11,7 +11,7 @@ from ..config import unreal_exec, unreal_project -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'unreal.log' diff --git a/samples/unreal/04_staticmesh_render.py b/samples/unreal/04_staticmesh_render.py index 9d3d7130..6406fca8 100644 --- a/samples/unreal/04_staticmesh_render.py +++ b/samples/unreal/04_staticmesh_render.py @@ -14,7 +14,7 @@ from ..config import assets_path, unreal_exec, unreal_project from ..utils import visualize_vertices -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'unreal.log' diff --git a/samples/unreal/05_skeletalmesh_render.py b/samples/unreal/05_skeletalmesh_render.py index 0e120b8d..45c28160 100644 --- a/samples/unreal/05_skeletalmesh_render.py +++ b/samples/unreal/05_skeletalmesh_render.py @@ -11,7 +11,7 @@ from ..config import assets_path, unreal_exec, unreal_project -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'unreal.log' diff --git a/samples/unreal/06_custom_usage.py b/samples/unreal/06_custom_usage.py index adf56f1f..782db5e1 100644 --- a/samples/unreal/06_custom_usage.py +++ b/samples/unreal/06_custom_usage.py @@ -11,7 +11,7 @@ from ..config import unreal_exec, unreal_project -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/samples/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') log_path = output_path / 'unreal.log' diff --git a/tests/blender/actor.py b/tests/blender/actor.py index 8cbd485c..1ae7fd44 100644 --- a/tests/blender/actor.py +++ b/tests/blender/actor.py @@ -12,7 +12,7 @@ from ..config import assets_path from ..utils import __timer__, _init_blender -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') bunny_obj = assets_path['bunny'] diff --git a/tests/blender/camera.py b/tests/blender/camera.py index b55f1f9d..0a033d99 100644 --- a/tests/blender/camera.py +++ b/tests/blender/camera.py @@ -10,7 +10,7 @@ from ..utils import __timer__, _init_blender -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') diff --git a/tests/blender/level.py b/tests/blender/level.py index 44ab0d7b..edf165c3 100644 --- a/tests/blender/level.py +++ b/tests/blender/level.py @@ -11,7 +11,7 @@ from ..config import assets_path from ..utils import __timer__, _init_blender -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') diff --git a/tests/blender/main.py b/tests/blender/main.py index 7833683c..98f8e566 100644 --- a/tests/blender/main.py +++ b/tests/blender/main.py @@ -12,7 +12,7 @@ from .level import level_test from .sequence import sequence_test -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/tests/blender' output_path = root / 'output' / Path(__file__).parent.relative_to(root) diff --git a/tests/blender/sequence.py b/tests/blender/sequence.py index 9b0c7e77..17839e6c 100644 --- a/tests/blender/sequence.py +++ b/tests/blender/sequence.py @@ -14,7 +14,7 @@ from ..config import assets_path from ..utils import __timer__, _init_blender -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/tests/blender/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') diff --git a/tests/unreal/sequence.py b/tests/unreal/sequence.py index fb2ac1ad..32118ed5 100644 --- a/tests/unreal/sequence.py +++ b/tests/unreal/sequence.py @@ -12,7 +12,7 @@ from ..config import assets_path from ..utils import __timer__, _init_unreal, visualize_vertices -root = Path(__file__).parents[2].resolve() +root = Path(__file__).resolve().parents[2] # output_path = '~/xrfeitoria/output/tests/unreal/{file_name}' output_path = root / 'output' / Path(__file__).relative_to(root).with_suffix('') seq_name = 'seq_test' From c13ee4073b09f6b677f201db6eff6a78a5348431 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 24 Jan 2024 13:47:00 +0800 Subject: [PATCH 101/110] catch RPC runner start and stop exceptions --- xrfeitoria/factory.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/xrfeitoria/factory.py b/xrfeitoria/factory.py index a98dc0b7..b778ace5 100644 --- a/xrfeitoria/factory.py +++ b/xrfeitoria/factory.py @@ -217,9 +217,14 @@ def __init__( background=background, new_process=new_process, ) - self._rpc_runner.start() - self.utils.init_scene_and_collection(default_level_blender, self._cleanup) - self.utils.set_env_color(color=(1.0, 1.0, 1.0, 1.0)) + try: + self._rpc_runner.start() + self.utils.init_scene_and_collection(default_level_blender, self._cleanup) + self.utils.set_env_color(color=(1.0, 1.0, 1.0, 1.0)) + except Exception as e: + self.logger.error(e) + self._rpc_runner.stop() + raise e def __enter__(self) -> 'init_blender': return self @@ -303,8 +308,13 @@ def __init__( background=background, new_process=new_process, ) - # xf_runner.Renderer.clear() - self._rpc_runner.start() + try: + self._rpc_runner.start() + # xf_runner.Renderer.clear() + except Exception as e: + self.logger.error(e) + self._rpc_runner.stop() + raise e def __enter__(self) -> 'init_unreal': return self From 12e956ee2f1d873cd5958d6b9994ed4f44b56a3d Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 24 Jan 2024 15:57:24 +0800 Subject: [PATCH 102/110] optimize order --- src/XRFeitoriaUnreal/Content/Python/sequence.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 4fea1e18..615f0dfc 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -417,12 +417,12 @@ def add_fk_motion_to_binding(binding: unreal.SequencerBindingProxy, motion_data: bone_name = f'{bone_name}_CONTROL' assert bone_name in param_names, RuntimeError(f'bone name: {bone_name} not in param names: {param_names}') - rig_proxies = unreal.ControlRigSequencerLibrary.get_control_rigs(binding.sequence) - for rig_proxy in rig_proxies: - if ENGINE_MAJOR_VERSION == 5 and ENGINE_MINOR_VERSION < 2: - msg = 'FKRigExecuteMode is not supported in < UE5.2, may cause unexpected result using FK motion.' - unreal.log_warning(msg) - else: + if ENGINE_MAJOR_VERSION == 5 and ENGINE_MINOR_VERSION < 2: + msg = 'FKRigExecuteMode is not supported in < UE5.2, may cause unexpected result using FK motion.' + unreal.log_warning(msg) + else: + rig_proxies = unreal.ControlRigSequencerLibrary.get_control_rigs(binding.sequence) + for rig_proxy in rig_proxies: ### TODO: judge if the track belongs to this actor unreal.ControlRigSequencerLibrary.set_control_rig_apply_mode( rig_proxy.control_rig, unreal.ControlRigFKRigExecuteMode.ADDITIVE From 46fee8777588c9b361728c00a5860d3d6da73f48 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 24 Jan 2024 16:39:02 +0800 Subject: [PATCH 103/110] optimize publish plugin --- src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin | 3 ++- xrfeitoria/utils/publish_plugins.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin index ba1c3756..960a431b 100644 --- a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin +++ b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin @@ -6,8 +6,9 @@ "Description": "OpenXRLab Synthetic Data Rendering Toolbox", "Category": "Scripting", "CreatedBy": "OpenXRLab", - "CreatedByURL": "", + "CreatedByURL": "https://openxrlab.org.cn", "DocsURL": "https://xrfeitoria.readthedocs.io/en/latest/", + "WhitelistPlatforms":["Win64", "Mac", "Linux"], "MarketplaceURL": "", "SupportURL": "", "CanContainContent": true, diff --git a/xrfeitoria/utils/publish_plugins.py b/xrfeitoria/utils/publish_plugins.py index 7e46ce64..1e00fe18 100644 --- a/xrfeitoria/utils/publish_plugins.py +++ b/xrfeitoria/utils/publish_plugins.py @@ -39,6 +39,7 @@ def _make_archive( src_folder: Path, dst_path: Optional[Path] = None, folder_name_inside_zip: Optional[str] = None, + filter_names: List[str] = ['.git', '.idea', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate'], ) -> Path: """Make archive of plugin folder. @@ -49,6 +50,8 @@ def _make_archive( zip_name (Optional[str], optional): name of the archive file. E.g. dst_name='plugin', the archive file would be ``plugin.zip``. Defaults to None, fallback to {plugin_folder.name}. folder_name (Optional[str], optional): name of the root folder in the archive. + filter_names (List[str], optional): list of folder names to be ignored. + Defaults to ['.git', '.idea', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate']. """ import zipfile @@ -60,7 +63,6 @@ def _make_archive( if dst_path.exists(): dst_path.unlink() - filter_names = ['.git', '.idea', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate'] with zipfile.ZipFile(dst_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: for file in src_folder.rglob('*'): # filter @@ -138,10 +140,22 @@ def build_unreal(unreal_exec_list: List[Path]): plugin_version=__version__, engine_version=engine_version, platform=platform.system(), - ) # e.g. XRFeitoriaUnreal-0.5.0-None-Windows + ) # e.g. XRFeitoriaUnreal-0.6.0-Unreal5.3-Windows + plugin_src_name = plugin_name_pattern.format( + plugin_name=plugin_name_unreal, + plugin_version=__version__, + engine_version=engine_version, + platform='Source', + ) # e.g. XRFeitoriaUnreal-0.6.0-Unreal5.3-Source dist_path = dist_root / plugin_name subprocess.call([uat_path, 'BuildPlugin', f'-Plugin={uplugin_path}', f'-Package={dist_path}']) _make_archive(src_folder=dist_path) + _make_archive( + src_folder=dist_path, + dst_path=dist_root / f'{plugin_src_name}.zip', + folder_name_inside_zip=plugin_name_unreal, + filter_names=['.DS_Store', '__pycache__', 'Intermediate', 'Binaries'], + ) logger.info(f'Plugin for {engine_version}: "{dist_path}.zip"') From 3290e457a1101c052aa501d41cda3fcede729765 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Wed, 24 Jan 2024 16:46:36 +0800 Subject: [PATCH 104/110] fix bug --- src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin index 960a431b..4056834d 100644 --- a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin +++ b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin @@ -8,9 +8,8 @@ "CreatedBy": "OpenXRLab", "CreatedByURL": "https://openxrlab.org.cn", "DocsURL": "https://xrfeitoria.readthedocs.io/en/latest/", - "WhitelistPlatforms":["Win64", "Mac", "Linux"], + "SupportURL": "https://xrfeitoria.readthedocs.io/en/latest/", "MarketplaceURL": "", - "SupportURL": "", "CanContainContent": true, "IsBetaVersion": false, "IsExperimentalVersion": false, @@ -19,7 +18,8 @@ { "Name": "XRFeitoriaUnreal", "Type": "Editor", - "LoadingPhase": "Default" + "LoadingPhase": "Default", + "WhitelistPlatforms":["Win64", "Mac", "Linux"], } ], "Plugins": [ From a9b42d3c799c0a78dcc495e92b70f576022b7216 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 25 Jan 2024 13:08:41 +0800 Subject: [PATCH 105/110] Update copyright information and add MarketplaceURL --- .../Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs | 2 +- src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs index 999501ed..e1f2a1b9 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright OpenXRLab 2024. All Rights Reserved. using UnrealBuildTool; diff --git a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin index 4056834d..ba5df286 100644 --- a/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin +++ b/src/XRFeitoriaUnreal/XRFeitoriaUnreal.uplugin @@ -9,7 +9,7 @@ "CreatedByURL": "https://openxrlab.org.cn", "DocsURL": "https://xrfeitoria.readthedocs.io/en/latest/", "SupportURL": "https://xrfeitoria.readthedocs.io/en/latest/", - "MarketplaceURL": "", + "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/ace631ac6e664dd281ab66bcae56ad55", "CanContainContent": true, "IsBetaVersion": false, "IsExperimentalVersion": false, @@ -19,7 +19,7 @@ "Name": "XRFeitoriaUnreal", "Type": "Editor", "LoadingPhase": "Default", - "WhitelistPlatforms":["Win64", "Mac", "Linux"], + "WhitelistPlatforms":["Win64", "Mac", "Linux"] } ], "Plugins": [ From f5b734141d5637e0bbba17de122334c09df158a7 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Thu, 25 Jan 2024 13:11:45 +0800 Subject: [PATCH 106/110] Update copyright notices --- .../Source/XRFeitoriaUnreal/Private/Annotator.cpp | 3 +-- .../Private/CustomMoviePipelineDeferredPass.cpp | 3 +-- .../XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp | 3 +-- .../XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp | 3 +-- .../XRFeitoriaUnreal/Private/XF_BlueprintFunctionLibrary.cpp | 3 +-- .../Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp | 2 +- .../Source/XRFeitoriaUnreal/Public/Annotator.h | 2 +- .../XRFeitoriaUnreal/Public/CustomMoviePipelineDeferredPass.h | 2 +- .../Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h | 2 +- .../Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h | 2 +- .../XRFeitoriaUnreal/Public/XF_BlueprintFunctionLibrary.h | 2 +- .../Source/XRFeitoriaUnreal/Public/XRFeitoriaUnreal.h | 2 +- .../Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs | 2 +- 13 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/Annotator.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/Annotator.cpp index 5ce05dc6..c239aab9 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/Annotator.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/Annotator.cpp @@ -1,5 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. - +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #include "Annotator.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineDeferredPass.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineDeferredPass.cpp index 577e2b42..4722f5f5 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineDeferredPass.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineDeferredPass.cpp @@ -1,5 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. - +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #include "CustomMoviePipelineDeferredPass.h" #include "CustomMoviePipelineOutput.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp index df344c9f..8220f5a9 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/CustomMoviePipelineOutput.cpp @@ -1,5 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. - +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #include "CustomMoviePipelineOutput.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp index eb9f5195..b491c499 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/MoviePipelineMeshOperator.cpp @@ -1,5 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. - +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #include "MoviePipelineMeshOperator.h" #include "XF_BlueprintFunctionLibrary.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XF_BlueprintFunctionLibrary.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XF_BlueprintFunctionLibrary.cpp index 020040e3..1cd89d7e 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XF_BlueprintFunctionLibrary.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XF_BlueprintFunctionLibrary.cpp @@ -1,5 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. - +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #include "XF_BlueprintFunctionLibrary.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp index 9caf2409..3327cead 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Private/XRFeitoriaUnreal.cpp @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #include "XRFeitoriaUnreal.h" #include "Engine/RendererSettings.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/Annotator.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/Annotator.h index 8e26d20f..e94377c4 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/Annotator.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/Annotator.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #pragma once diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineDeferredPass.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineDeferredPass.h index e6ffaa8f..1963517b 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineDeferredPass.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineDeferredPass.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #pragma once diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h index 4d6a034f..136e76c1 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/CustomMoviePipelineOutput.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #pragma once diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h index 07fc6c46..af17eb01 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/MoviePipelineMeshOperator.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #pragma once diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XF_BlueprintFunctionLibrary.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XF_BlueprintFunctionLibrary.h index 2c8cf495..41910f62 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XF_BlueprintFunctionLibrary.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XF_BlueprintFunctionLibrary.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #pragma once #include "Serialization/Archive.h" diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XRFeitoriaUnreal.h b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XRFeitoriaUnreal.h index b21a8ba4..ad6d3d3a 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XRFeitoriaUnreal.h +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/Public/XRFeitoriaUnreal.h @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. #pragma once diff --git a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs index e1f2a1b9..a0ea0481 100644 --- a/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs +++ b/src/XRFeitoriaUnreal/Source/XRFeitoriaUnreal/XRFeitoriaUnreal.Build.cs @@ -1,4 +1,4 @@ -// Copyright OpenXRLab 2024. All Rights Reserved. +// Copyright OpenXRLab 2023-2024. All Rights Reserved. using UnrealBuildTool; From 6ec287755d8d27ec5a11e8578c65e2581ffc02c9 Mon Sep 17 00:00:00 2001 From: Mei Date: Fri, 26 Jan 2024 13:03:42 +0800 Subject: [PATCH 107/110] Update src/XRFeitoriaBpy/core/factory.py Co-authored-by: YANG Zhitao --- src/XRFeitoriaBpy/core/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XRFeitoriaBpy/core/factory.py b/src/XRFeitoriaBpy/core/factory.py index 831df2bb..31d537a0 100644 --- a/src/XRFeitoriaBpy/core/factory.py +++ b/src/XRFeitoriaBpy/core/factory.py @@ -173,7 +173,7 @@ def get_sequence_properties(collection: 'bpy.types.Collection') -> SequencePrope resolution_x = collection.sequence_properties.resolution_x resolution_y = collection.sequence_properties.resolution_y - return level, fps, frame_start, frame_end, frame_current, resolution_x, resolution_y + return SequenceProperties(level, fps, frame_start, frame_end, frame_current, resolution_x, resolution_y) def open_sequence(seq_name: str) -> 'bpy.types.Scene': """Open the given sequence. From 9dca9a9d5c62d5cb66b3c1a5b226f1ee5282aa65 Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 26 Jan 2024 13:11:20 +0800 Subject: [PATCH 108/110] `filter_names` set to tuple --- xrfeitoria/utils/publish_plugins.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/xrfeitoria/utils/publish_plugins.py b/xrfeitoria/utils/publish_plugins.py index 1e00fe18..99a77c7a 100644 --- a/xrfeitoria/utils/publish_plugins.py +++ b/xrfeitoria/utils/publish_plugins.py @@ -8,7 +8,7 @@ import subprocess from contextlib import contextmanager from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Tuple from loguru import logger @@ -39,7 +39,7 @@ def _make_archive( src_folder: Path, dst_path: Optional[Path] = None, folder_name_inside_zip: Optional[str] = None, - filter_names: List[str] = ['.git', '.idea', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate'], + filter_names: Tuple[str, ...] = ('.git', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate'), ) -> Path: """Make archive of plugin folder. @@ -50,8 +50,8 @@ def _make_archive( zip_name (Optional[str], optional): name of the archive file. E.g. dst_name='plugin', the archive file would be ``plugin.zip``. Defaults to None, fallback to {plugin_folder.name}. folder_name (Optional[str], optional): name of the root folder in the archive. - filter_names (List[str], optional): list of folder names to be ignored. - Defaults to ['.git', '.idea', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate']. + filter_names (Tuple[str, ...], optional): names of folders to be ignored. + Defaults to ('.git', '.vscode', '.gitignore', '.DS_Store', '__pycache__', 'Intermediate'). """ import zipfile @@ -130,7 +130,7 @@ def build_unreal(unreal_exec_list: List[Path]): dir_plugin = src_root / plugin_name_unreal uplugin_path = dir_plugin / f'{plugin_name_unreal}.uplugin' update_uplugin_version(uplugin_path) - logger.info(f'Compiling plugin for Unreal Engine...') + logger.info('Compiling plugin for Unreal Engine...') for unreal_exec in unreal_exec_list: uat_path = unreal_exec.parents[2] / 'Build/BatchFiles/RunUAT.bat' unreal_infos = UnrealRPCRunner._get_engine_info(unreal_exec) @@ -154,7 +154,7 @@ def build_unreal(unreal_exec_list: List[Path]): src_folder=dist_path, dst_path=dist_root / f'{plugin_src_name}.zip', folder_name_inside_zip=plugin_name_unreal, - filter_names=['.DS_Store', '__pycache__', 'Intermediate', 'Binaries'], + filter_names=('.DS_Store', '__pycache__', 'Intermediate', 'Binaries'), ) logger.info(f'Plugin for {engine_version}: "{dist_path}.zip"') From dfcb3db42fbfd2833897e425cf4eadebe271d12e Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 26 Jan 2024 13:11:24 +0800 Subject: [PATCH 109/110] bug fix --- src/XRFeitoriaUnreal/Content/Python/sequence.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/XRFeitoriaUnreal/Content/Python/sequence.py b/src/XRFeitoriaUnreal/Content/Python/sequence.py index 615f0dfc..6a667efa 100644 --- a/src/XRFeitoriaUnreal/Content/Python/sequence.py +++ b/src/XRFeitoriaUnreal/Content/Python/sequence.py @@ -813,22 +813,24 @@ def add_audio_to_sequence( start_frame: Optional[int] = None, end_frame: Optional[int] = None, ) -> Dict[str, Any]: + fps = get_sequence_fps(sequence) # ------- add audio track ------- # - audio_track: unreal.MovieSceneAudioTrack = sequence.add_master_track(unreal.MovieSceneAudioTrack) + audio_track: unreal.MovieSceneAudioTrack = sequence.add_track(unreal.MovieSceneAudioTrack) audio_section: unreal.MovieSceneAudioSection = audio_track.add_section() audio_track.set_display_name(audio_asset.get_name()) # ------- set start frame ------- # if start_frame is None: start_frame = 0 - audio_section.set_start_frame(start_frame=start_frame) # ------- set end frame ------- # if end_frame is None: duration = audio_asset.get_editor_property('duration') - audio_section.set_end_frame_seconds(end_time=duration) - else: - audio_section.set_end_frame(end_frame=end_frame) + end_frame = start_frame + int(duration * fps) + audio_section.set_end_frame(end_frame=end_frame) + audio_section.set_start_frame(start_frame=start_frame) + + # ------- set audio ------- # audio_section.set_sound(audio_asset) return {'audio_track': {'track': audio_track, 'section': audio_section}} From a8eda2e8a07eb3b540bbf78ed9015927ca0cb58b Mon Sep 17 00:00:00 2001 From: HaiyiMei Date: Fri, 26 Jan 2024 16:44:05 +0800 Subject: [PATCH 110/110] Fix plugin version extraction in RPCRunner --- xrfeitoria/utils/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrfeitoria/utils/runner.py b/xrfeitoria/utils/runner.py index a10b6f54..a66a07ac 100644 --- a/xrfeitoria/utils/runner.py +++ b/xrfeitoria/utils/runner.py @@ -440,7 +440,7 @@ def installed_plugin_version(self) -> str: if _match: dst_plugin_version = _match.groups()[0] else: - raise ValueError("Failed to extract plugin version from '__init__.py'") + dst_plugin_version = '0.0.0' # cannot find version elif self.engine_type == EngineEnum.unreal: uplugin_file = self.dst_plugin_dir / f'{plugin_name_unreal}.uplugin' dst_plugin_version = json.loads(uplugin_file.read_text())['VersionName']