"""Remote functions for blender."""
from pathlib import Path
from typing import Dict, List, Literal, Optional, Tuple
from ...data_structure.constants import ImportFileFormatEnum, MotionFrame, PathLike, Vector
from ...rpc import remote_blender
try:
# 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
except ImportError:
pass
[docs]
@remote_blender()
def render_viewport(animation: bool = False) -> None:
"""Render the current viewport. Disable multiview when rendering, and restore it
after rendering.
Args:
animation (bool, optional): Render animation. Defaults to False.
"""
# disable multiview to avoid error
_multiview = bpy.context.scene.render.use_multiview
bpy.context.scene.render.use_multiview = False
if bpy.app.background:
bpy.ops.render.render(animation=animation)
else:
bpy.ops.render.render('INVOKE_DEFAULT', animation=animation)
# restore multiview
bpy.context.scene.render.use_multiview = _multiview
[docs]
@remote_blender()
def import_file(file_path: 'PathLike') -> None:
"""Import file to blender. This function will not return an instance of the imported actor.
The file type is determined by the file extension.
Supported file types: fbx, obj, abc, ply, stl.
Note:
For fbx file, only support binary format. ASCII format is not supported.
Ref: https://docs.blender.org/manual/en/3.6/addons/import_export/scene_fbx.html#id4
"""
file_type = Path(file_path).suffix[1:].lower() # remove dot, lower case
try:
file_type = ImportFileFormatEnum[file_type]
except KeyError:
raise TypeError(f'File type {file_type} not supported')
if file_type == ImportFileFormatEnum.fbx:
XRFeitoriaBlenderFactory.import_fbx(fbx_file=file_path)
elif file_type == ImportFileFormatEnum.obj:
XRFeitoriaBlenderFactory.import_obj(obj_file=file_path)
elif file_type == ImportFileFormatEnum.abc:
XRFeitoriaBlenderFactory.import_alembic(alembic_file=file_path)
elif file_type == ImportFileFormatEnum.ply:
XRFeitoriaBlenderFactory.import_ply(ply_file=file_path)
elif file_type == ImportFileFormatEnum.stl:
XRFeitoriaBlenderFactory.import_stl(stl_file=file_path)
elif file_type == ImportFileFormatEnum.glb:
XRFeitoriaBlenderFactory.import_glb(glb_file=file_path)
[docs]
@remote_blender()
def apply_motion_data_to_actor(
motion_data: 'List[MotionFrame]',
actor_name: str,
is_first_frame_as_origin: bool = True,
) -> None:
"""Applies motion data to a given actor in Blender.
Args:
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.
is_first_frame_as_origin (bool, optional): Whether to set the first frame as the origin. Defaults to True.
"""
XRFeitoriaBlenderFactory.apply_motion_data_to_actor(
motion_data=motion_data, actor_name=actor_name, is_first_frame_as_origin=is_first_frame_as_origin
)
[docs]
@remote_blender()
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.
mesh_name (str): Name of the mesh.
"""
XRFeitoriaBlenderFactory.apply_shape_keys_to_mesh(shape_keys=shape_keys, mesh_name=mesh_name)
[docs]
@remote_blender()
def is_background_mode(warning: bool = False) -> bool:
"""Check whether Blender is running in background mode.
Returns:
bool: True if Blender is running in background mode.
"""
_mode = bpy.app.background
if warning and _mode:
from XRFeitoriaBpy import logger
logger.warning('Properties (location, rotation, scale, etc.) cannot be guaranteed in background mode.')
return _mode
[docs]
@remote_blender()
def cleanup_unused():
"""Cleanup all the unused data in the current blend file."""
bpy.ops.outliner.orphans_purge(do_local_ids=True)
[docs]
@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.
pack (bool, optional): Automatically pack all external data into .blend file. Defaults to False.
Raises:
Exception: Failed to set autopack.
"""
try:
bpy.data.use_autopack = pack
except Exception as e:
raise Exception(f'Failed to set autopack: {e}')
# fallback to current file path
if save_path is None:
save_path = bpy.data.filepath
# path.resolve() would do Network Drive Handling like:
# X:/path/to/file.blend -> //xxx.xxx.xxx.xxx/Drive/path/to/file.blend
# which made Blender failed to save (Windows)
save_path = Path(save_path).absolute()
# set suffix to .blend
if save_path.suffix != '.blend':
save_path = save_path.with_suffix('.blend')
save_path.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=save_path.as_posix())
[docs]
@remote_blender()
def set_env_color(color: 'Tuple[float, float, float, float]', intensity: float = 1.0):
"""Set the color of the environment light.
Args:
color (Tuple[float, float, float, float]): RGBA color of the environment light.
intensity (float, optional): Intensity of the environment light. Defaults to 1.0.
"""
scene = XRFeitoriaBlenderFactory.get_active_scene()
scene_world = bpy.data.worlds.new(name=scene.name)
scene.world = scene_world
scene.world.use_nodes = True
node = scene.world.node_tree.nodes['Background']
node.inputs['Color'].default_value = color
node.inputs['Strength'].default_value = intensity
[docs]
@remote_blender()
def set_hdr_map(hdr_map_path: 'PathLike') -> None:
"""Set HDR map of the active scene.
Args:
hdr_map_path ('PathLike'): Path of the HDR map file.
"""
scene = XRFeitoriaBlenderFactory.get_active_scene()
XRFeitoriaBlenderFactory.set_hdr_map(scene=scene, hdr_map_path=hdr_map_path)
[docs]
@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)
[docs]
@remote_blender()
def get_frame_range() -> 'Tuple[int, int]':
"""Get the frame range of the active scene.
Returns:
Tuple[int, int]: Start frame and End frame.
"""
scene = XRFeitoriaBlenderFactory.get_active_scene()
return XRFeitoriaBlenderFactory.get_frame_range(scene)
[docs]
@remote_blender()
def set_frame_range(start: int, end: int) -> None:
"""Set frame range of the active scene.
Args:
start (int): Start frame.
end (int): End frame.
"""
logger.info(f'Override frame range to [{start}, {end}]')
scene = XRFeitoriaBlenderFactory.get_active_scene()
XRFeitoriaBlenderFactory.set_frame_range(scene=scene, start=start, end=end)
[docs]
@remote_blender()
def set_frame_current(frame: int) -> None:
"""Set current frame of the active scene.
Args:
frame (int): Frame number.
"""
scene = XRFeitoriaBlenderFactory.get_active_scene()
XRFeitoriaBlenderFactory.set_frame_current(scene=scene, frame=frame)
[docs]
@remote_blender()
def get_keys_range() -> 'Tuple[int, int]':
"""Get the max keyframe range of all the objects in the active scene.
Returns:
Tuple[int, int]: Start frame and End frame.
"""
scene = XRFeitoriaBlenderFactory.get_active_scene()
return XRFeitoriaBlenderFactory.get_keys_range(scene)
[docs]
@remote_blender()
def get_all_object_in_seq(seq_name: str, obj_type: 'Literal["MESH", "ARMATURE", "CAMERA"]') -> 'List[str]':
"""Get all the objects in the given sequence.
Args:
seq_name (str): Name of the sequence.
obj_type (Literal["MESH", "ARMATURE", "CAMERA"]): Object type.
Returns:
List[str]: List of object names.
"""
return XRFeitoriaBlenderFactory.get_all_object_in_seq(seq_name=seq_name, obj_type=obj_type)
[docs]
@remote_blender()
def get_all_object_in_current_level(obj_type: 'Literal["MESH", "ARMATURE", "CAMERA"]') -> 'List[str]':
"""Get all the objects in the current level.
Args:
obj_type (Literal["MESH";, "ARMATURE";, "CAMERA";]): Object type.
Returns:
List[str]: List of object names.
"""
return XRFeitoriaBlenderFactory.get_all_object_in_current_level(obj_type=obj_type)
[docs]
@remote_blender()
def new_level(name: str) -> None:
"""Create a new level.
Args:
name (str): Name of the level.
"""
level_scene = XRFeitoriaBlenderFactory.new_scene(name)
level_collection = XRFeitoriaBlenderFactory.new_collection(name)
XRFeitoriaBlenderFactory.link_collection_to_scene(level_collection, level_scene)
[docs]
@remote_blender()
def export_vertices(export_path: 'PathLike', use_animation: bool = True) -> None:
"""Export vertices in the world space of all objects in the active scene to npz
file, with animation (export N frame) if use_animation is True. export_path will be
an npz file with a single key 'verts' which is a numpy array in shape of (N, V, 3),
where N is the number of frames, V is the number of vertices.
Args:
export_path (PathLike): Path to export vertices. (Require a folder)
use_animation (bool, optional): Export with animation. Defaults to True.
"""
scene = XRFeitoriaBlenderFactory.get_active_scene()
XRFeitoriaBlenderFactory.export_vertices(scene=scene, export_path=export_path, use_animation=use_animation)
[docs]
@remote_blender()
def get_rotation_to_look_at(location: 'Vector', target: 'Vector') -> 'Vector':
"""Get the rotation of an object to look at another object.
Args:
location (Vector): Location of the object.
target (Vector): Location of the target object.
Returns:
Vector: Rotation of the object.
"""
import math
rotation = XRFeitoriaBlenderFactory.get_rotation_to_look_at(location=location, target=target)
return tuple(math.degrees(r) for r in rotation)
[docs]
@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()
[docs]
@remote_blender()
def init_scene_and_collection(name: str, cleanup: bool = False) -> None:
"""Init the default scene and default collection.
Args:
name (str): Name of the default scene and default collection.
cleanup (bool, optional): Clean up the all the scenes, collections and objects. Defaults to False.
"""
if cleanup:
XRFeitoriaBlenderFactory.delete_all()
cleanup_unused()
# get or create collection
if name not in bpy.data.collections.keys():
_collection = XRFeitoriaBlenderFactory.new_collection(name)
else:
_collection = XRFeitoriaBlenderFactory.get_collection(name)
# get or create scene
if name not in bpy.data.scenes.keys():
if cleanup:
_scene = XRFeitoriaBlenderFactory.get_active_scene()
_scene.name = name
else:
_scene = XRFeitoriaBlenderFactory.new_scene(name=name)
else:
_scene = XRFeitoriaBlenderFactory.get_scene(name)
# link collection to scene
XRFeitoriaBlenderFactory.link_collection_to_scene(_collection, _scene)
# set scene and collection active
XRFeitoriaBlenderFactory.set_scene_active(_scene)
XRFeitoriaBlenderFactory.set_collection_active(_collection)
[docs]
@remote_blender()
def enable_gpu(gpu_num: int = 1):
"""Enable GPU in blender.
Args:
gpu_num (int, optional): Number of GPUs to use. Defaults to 1.
"""
XRFeitoriaBlenderFactory.enable_gpu(gpu_num=gpu_num)
[docs]
@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.')