import math
from abc import ABC, abstractmethod
from typing import Dict, List, Literal, Tuple
from ..data_structure.constants import Transform, Vector, xf_obj_name
from ..rpc import remote_blender, remote_unreal
from ..utils import Validator
from ..utils.functions import blender_functions
try:
# linting and for engine
import bpy
import unreal
from unreal_factory import XRFeitoriaUnrealFactory # defined in src/XRFeitoriaUnreal/Content/Python
from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py
except ModuleNotFoundError:
pass
[docs]
class ObjectUtilsBase(ABC):
"""Base class for object utils."""
##########################
# ------- Getter ------- #
##########################
[docs]
@classmethod
def get_location(cls, name: str) -> Vector:
"""Get location of the object.
Args:
name (str): Name of the object.
Returns:
Vector: Location of the object.
"""
cls.validate_name(name)
return cls._get_location_in_engine(name)
[docs]
@classmethod
def get_rotation(cls, name: str) -> Vector:
"""Get rotation of the object.
Args:
name (str): Name of the object.
Returns:
Vector: Rotation of the object.
"""
cls.validate_name(name)
return cls._get_rotation_in_engine(name)
[docs]
@classmethod
def get_scale(cls, name: str) -> Vector:
"""Get scale of the object.
Args:
name (str): Name of the object.
Returns:
Vector: Scale of the object.
"""
cls.validate_name(name)
return cls._get_scale_in_engine(name)
[docs]
@classmethod
def get_dimensions(cls, name: str) -> Vector:
"""Get dimensions of the object.
Args:
name (str): Name of the object.
Returns:
Vector: Dimensions of the object.
"""
return cls._get_dimensions_in_engine(name)
[docs]
@classmethod
def get_bound_box(cls, name: str) -> Vector:
"""Get bounding box of the object.
Args:
name (str): Name of the object.
Returns:
Vector: Bounding box of the object.
"""
return cls._get_bound_box_in_engine(name)
##########################
# ------- Setter ------- #
##########################
[docs]
@classmethod
def set_location(cls, name: str, location: Vector):
"""Set location of the object.
Args:
name (str): Name of the object.
location (Vector): Location of the object.
"""
cls.validate_name(name)
Validator.validate_vector(location, 3)
cls._set_location_in_engine(name, location)
[docs]
@classmethod
def set_rotation(cls, name: str, rotation: Vector):
"""Set rotation of the object.
Args:
name (str): Name of the object.
rotation (Vector): Rotation of the object.
"""
cls.validate_name(name)
Validator.validate_vector(rotation, 3)
cls._set_rotation_in_engine(name, rotation)
[docs]
@classmethod
def set_scale(cls, name, scale: Vector):
"""Set scale of the object.
Args:
name (str): Name of the object.
scale (Vector): Scale of the object.
"""
cls.validate_name(name)
Validator.validate_vector(scale, 3)
cls._set_scale_in_engine(name, scale)
[docs]
@classmethod
def set_name(cls, name: str, new_name: str):
"""Set a new name for the object.
Args:
name (str): Original name of the object.
new_name (str): New name of the object.
"""
if name == new_name:
return
cls.validate_name(name)
cls.validate_new_name(new_name)
cls._set_name_in_engine(name, new_name)
@classmethod
def generate_obj_name(cls, obj_type: 'Literal["camera", "actor"]') -> str:
return cls._generate_obj_name_in_engine(obj_type)
##########################
# ----- Engine API ----- #
##########################
# ----- Getter ----- #
@staticmethod
@abstractmethod
def _get_transform_in_engine(name: str) -> 'Transform':
raise NotImplementedError
@staticmethod
@abstractmethod
def _get_location_in_engine(name) -> 'Vector':
raise NotImplementedError
@staticmethod
@abstractmethod
def _get_rotation_in_engine(name) -> 'Vector':
raise NotImplementedError
@staticmethod
@abstractmethod
def _get_scale_in_engine(name) -> 'Vector':
raise NotImplementedError
@staticmethod
@abstractmethod
def _get_all_objects_in_engine() -> 'List[str]':
raise NotImplementedError
@staticmethod
@abstractmethod
def _generate_obj_name_in_engine(obj_type: str) -> str:
raise NotImplementedError
# ----- Setter ----- #
@staticmethod
@abstractmethod
def _set_transform_in_engine(name: str, location: 'Vector', rotation: 'Vector', scale: 'Vector'):
raise NotImplementedError
@staticmethod
@abstractmethod
def _set_location_in_engine(name: str, location: 'Vector'):
raise NotImplementedError
@staticmethod
@abstractmethod
def _set_rotation_in_engine(name: str, rotation: 'Vector'):
raise NotImplementedError
@staticmethod
@abstractmethod
def _set_scale_in_engine(name: str, scale: 'Vector'):
raise NotImplementedError
@staticmethod
@abstractmethod
def _set_name_in_engine(name: str, new_name: str):
raise NotImplementedError
# ------- Delete ------- #
@classmethod
def delete_obj(cls, name):
cls.validate_name(name)
cls._delete_obj_in_engine(name)
@staticmethod
@abstractmethod
def _delete_obj_in_engine(name):
raise NotImplementedError
# ----- Validator ------ #
@classmethod
def validate_name(cls, name):
objects = cls._get_all_objects_in_engine()
if name not in objects:
raise ValueError(f"Invalid name, '{name}' does not exist in scene.")
@classmethod
def validate_new_name(cls, name):
objects = cls._get_all_objects_in_engine()
if name in objects:
raise ValueError(f"Invalid name, '{name}' already exists in scene.")
[docs]
@remote_blender(dec_class=True)
class ObjectUtilsBlender(ObjectUtilsBase):
"""Object utils class for Blender."""
##########################
# ------- Setter ------- #
##########################
[docs]
@classmethod
def set_origin(cls, name: str) -> None:
"""Set origin of the object to its center.
Args:
name (str): Name of the object.
"""
cls.validate_name(name)
cls._set_origin_in_engine(name=name)
[docs]
@classmethod
def set_dimensions(cls, name: str, dimensions: 'Vector') -> None:
"""Set dimensions of the object.
Args:
name (str): Name of the object.
dimensions (Vector): Dimensions of the object.
"""
cls.validate_name(name)
cls._set_dimensions_in_engine(name=name, dimensions=dimensions)
##########################
# ----- Engine API ----- #
##########################
# ------- Getter ------- #
@staticmethod
def _get_dimensions_in_engine(name: str) -> 'Vector':
"""Get dimensions of the object in Blender.
Args:
name (str): Name of the object.
Returns:
Vector: Dimensions of the object.
"""
# return bpy.data.objects[name].dimensions.to_tuple()
obj = bpy.data.objects[name]
bbox_min, bbox_max = XRFeitoriaBlenderFactory.get_bound_box_in_world_space(obj)
return (bbox_max[0] - bbox_min[0], bbox_max[1] - bbox_min[1], bbox_max[2] - bbox_min[2])
@staticmethod
def _get_bound_box_in_engine(name: str) -> 'Tuple[Vector, Vector]':
"""Get bounding box of the object in Blender.
Args:
name (str): Name of the object.
Returns:
Vector: Bounding box of the object.
"""
obj = bpy.data.objects[name]
return XRFeitoriaBlenderFactory.get_bound_box_in_world_space(obj)
@staticmethod
def _get_transform_in_engine(name: str) -> 'Transform':
"""Get transform (location, rotation and scale) of the object in Blender.
Args:
name (str): Name of the object.
Returns:
Transform: Transform (location, rotation and scale).
"""
actor = bpy.data.objects[name]
location = actor.location.to_tuple()
rotation = tuple(math.degrees(r) for r in actor.rotation_euler) # convert to degrees
scale = actor.scale.to_tuple()
blender_functions.is_background_mode(warning=True)
return location, rotation, scale
@staticmethod
def _get_location_in_engine(name: str) -> 'Vector':
"""Get location of the object in Blender.
Args:
name (str): Name of the object.
Returns:
Vector: Location.
"""
blender_functions.is_background_mode(warning=True)
return bpy.data.objects[name].location.to_tuple()
@staticmethod
def _get_rotation_in_engine(name: str) -> 'Vector':
"""Get rotation of the object in Blender.
Args:
name (str): Name of the object.
Returns:
Vector: Rotation.
"""
blender_functions.is_background_mode(warning=True)
return tuple(math.degrees(r) for r in bpy.data.objects[name].rotation_euler) # convert to degrees
@staticmethod
def _get_scale_in_engine(name) -> 'Vector':
"""Get scale of the object in Blender.
Args:
name (str): Name of the object.
Returns:
Vector: Scale.
"""
blender_functions.is_background_mode(warning=True)
return bpy.data.objects[name].scale.to_tuple()
@staticmethod
def _get_all_objects_in_engine() -> 'List[str]':
"""Get all objects in this blend file.
Returns:
List[str]: Name of all objects.
"""
return bpy.data.objects.keys()
@staticmethod
def _generate_obj_name_in_engine(obj_type: 'Literal["camera", "actor"]') -> str:
"""Generate a name for the new object.
Args:
obj_type (str): Type of the object. It can be either 'camera' or 'actor'.
Returns:
str: Name of the new object.
"""
objs = [obj for obj in bpy.data.objects if obj_type in obj.name and obj.name.startswith(xf_obj_name[:3])]
# return f'[XF]{obj_type}-{collection.name}-{(len(objs)+1):03}'
return xf_obj_name.format(obj_type=obj_type, obj_idx=(len(objs) + 1))
##########################
# ------- Setter ------- #
##########################
@staticmethod
def _set_transform_in_engine(name: str, location: 'Vector', rotation: 'Vector', scale: 'Vector'):
"""Set transform (location, rotation and scale) of the object in Blender.
Args:
name (str): Name of the object.
location (Vector): Location of the object.
rotation (Vector): Rotation of the object.
scale (Vector): Scale of the object.
"""
actor = bpy.data.objects[name]
actor.location = location
actor.rotation_euler = [math.radians(r) for r in rotation] # convert to radians
actor.scale = scale
@staticmethod
def _set_location_in_engine(name: str, location: 'Vector'):
"""Set location of the object in Blender.
Args:
name (str): Name of the object.
location (Vector): Location of the object.
"""
actor = bpy.data.objects[name]
actor.location = location
@staticmethod
def _set_rotation_in_engine(name: str, rotation: 'Vector'):
"""Set rotation of the object in Blender.
Args:
name (str): Name of the object.
rotation (Vector): Rotation of the object.
"""
actor = bpy.data.objects[name]
actor.rotation_euler = [math.radians(r) for r in rotation] # convert to radians
@staticmethod
def _set_scale_in_engine(name: str, scale: 'Vector'):
"""Set scale of the object in Blender.
Args:
name (str): Name of the object.
rotation (Vector): Scale of the object.
"""
bpy.data.objects[name].scale = scale
@staticmethod
def _set_name_in_engine(name: str, new_name: str):
"""Set name of the object in Blender.
Args:
name (str): Name of the object.
new_name (str): New name of the object.
"""
bpy.data.objects[name].name = new_name
@staticmethod
def _set_transform_keys_in_engine(
obj_name: str,
transform_keys: 'List[Dict]',
) -> None:
"""Set keyframe of the object in Blender.
Args:
obj_name (str): Name of the object.
transform_keys (List[Dict]): Keyframes of transform (location, rotation, scale, and interpolation).
"""
obj = bpy.data.objects[obj_name]
for i, key in enumerate(transform_keys):
## insert keyframes
# https://docs.blender.org/api/current/bpy.types.bpy_struct.html
if key['location']:
obj.location = key['location']
obj.keyframe_insert(data_path='location', frame=key['frame'])
if key['rotation']:
obj.rotation_euler = [math.radians(i) for i in key['rotation']]
obj.keyframe_insert(data_path='rotation_euler', frame=key['frame'])
if key['scale']:
obj.scale = key['scale']
obj.keyframe_insert(data_path='scale', frame=key['frame'])
## set interpolation mode
# https://blender.stackexchange.com/questions/260149/set-keyframe-interpolation-constant-while-setting-a-keyframe-in-blender-python
if obj.animation_data and obj.animation_data.action:
obj_action = bpy.data.actions.get(obj.animation_data.action.name)
if key['location']:
obj_location_fcurve = obj_action.fcurves.find('location')
obj_location_fcurve.keyframe_points[i].interpolation = transform_keys[i]['interpolation']
if key['rotation']:
obj_rotation_fcurve = obj_action.fcurves.find('rotation_euler')
obj_rotation_fcurve.keyframe_points[i].interpolation = transform_keys[i]['interpolation']
if key['scale']:
obj_scale_fcurve = obj_action.fcurves.find('scale')
obj_scale_fcurve.keyframe_points[i].interpolation = transform_keys[i]['interpolation']
@staticmethod
def _set_origin_in_engine(name: str) -> None:
obj = bpy.data.objects[name]
# select this obj in blender
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
# set origin to center of mass
bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='BOUNDS')
@staticmethod
def _set_dimensions_in_engine(name: str, dimensions: 'Vector') -> None:
obj = bpy.data.objects[name]
obj.dimensions = dimensions
##########################
# ------- Delete ------- #
##########################
@staticmethod
def _delete_obj_in_engine(name: str):
"""Delete the object in Blender.
Args:
name (str): Name of the object.
"""
bpy.data.objects.remove(bpy.data.objects[name])
[docs]
@remote_unreal(dec_class=True)
class ObjectUtilsUnreal(ObjectUtilsBase):
"""Object utils class for Unreal."""
##########################
# ----- Engine API ----- #
##########################
# ------- Getter ------- #
@staticmethod
def _get_transform_in_engine(name: str) -> 'Transform':
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
location = actor.get_actor_location()
rotation = actor.get_actor_rotation()
scale = actor.get_actor_scale3d()
# convert from centimeters to meters
location /= 100.0
return location.to_tuple(), rotation.to_tuple(), scale.to_tuple()
@staticmethod
def _get_location_in_engine(name: str) -> 'Vector':
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
location = actor.get_actor_location()
# convert from centimeters to meters
location /= 100.0
return location.to_tuple()
@staticmethod
def _get_rotation_in_engine(name: str) -> 'Vector':
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
rotation = actor.get_actor_rotation()
return rotation.to_tuple()
@staticmethod
def _get_scale_in_engine(name: str) -> 'Vector':
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
scale = actor.get_actor_scale3d()
return scale.to_tuple()
@staticmethod
def _get_dimensions_in_engine(name: str) -> 'Vector':
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
origin, box_extent = actor.get_actor_bounds(only_colliding_components=False)
return (box_extent.x * 2 / 100.0, box_extent.y * 2 / 100.0, box_extent.z * 2 / 100.0)
@staticmethod
def _get_bound_box_in_engine(name: str) -> 'Tuple[Vector, Vector]':
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
origin, box_extent = actor.get_actor_bounds(only_colliding_components=False)
return (
(
(origin.x - box_extent.x) / 100.0,
(origin.y - box_extent.y) / 100.0,
(origin.z - box_extent.z) / 100.0,
),
(
(origin.x + box_extent.x) / 100.0,
(origin.y + box_extent.y) / 100.0,
(origin.z + box_extent.z) / 100.0,
),
)
@staticmethod
def _get_engine_path_in_engine(name: str) -> str:
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
if isinstance(actor, unreal.StaticMeshActor):
return actor.static_mesh_component.static_mesh.get_path_name().split('.')[0]
elif isinstance(actor, unreal.SkeletalMeshActor):
return actor.skeletal_mesh_component.skeletal_mesh.get_path_name().split('.')[0]
@staticmethod
def _get_all_objects_in_engine() -> 'List[str]':
return XRFeitoriaUnrealFactory.utils_actor.get_all_actors_name()
# ------- Setter ------- #
@staticmethod
def _set_transform_in_engine(name: str, location: 'Vector', rotation: 'Vector', scale: 'Vector') -> None:
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
location = unreal.Vector(location[0], location[1], location[2])
rotation = unreal.Rotator(rotation[0], rotation[1], rotation[2])
# convert from meters to centimeters
location *= 100.0
actor.set_actor_transform(unreal.Transform(location, rotation, scale), False, False)
@staticmethod
def _set_location_in_engine(name: str, location: 'Vector') -> None:
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
location = unreal.Vector(location[0], location[1], location[2])
# convert from meters to centimeters
location *= 100.0
actor.set_actor_location(location, False, False)
@staticmethod
def _set_rotation_in_engine(name: str, rotation: 'Vector') -> None:
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
rotation = unreal.Rotator(rotation[0], rotation[1], rotation[2])
actor.set_actor_rotation(rotation, False)
@staticmethod
def _set_scale_in_engine(name, scale: 'Vector') -> None:
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
actor.set_actor_scale3d(scale)
@staticmethod
def _set_name_in_engine(name: str, new_name: str) -> None:
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
actor.set_actor_label(new_name)
@staticmethod
def _delete_obj_in_engine(name):
actor = XRFeitoriaUnrealFactory.utils_actor.get_actor_by_name(name)
XRFeitoriaUnrealFactory.utils_actor.destroy_actor(actor)
@staticmethod
@abstractmethod
def _generate_obj_name_in_engine(obj_type: 'Literal["camera", "actor"]') -> str:
"""Generate a name for the new object.
Args:
obj_type (str): Type of the object. It can be either 'camera' or 'actor'.
Returns:
str: Name of the new object.
"""
actors = XRFeitoriaUnrealFactory.utils_actor.get_class_actors(unreal.Actor)
actors = [
actor
for actor in actors
if obj_type in actor.get_actor_label() and actor.get_actor_label().startswith(xf_obj_name[:3])
]
return xf_obj_name.format(obj_type=obj_type, obj_idx=(len(actors) + 1))
##########################
# ------- Tools -------- #
##########################
# def direction_to_euler(cls, direction_vector: np.ndarray) -> Tuple[float, float, float]:
# """Convert a direction vector to euler angles (yaw, pitch, roll) in degrees.
# Args:
# direction_vector (np.ndarray): [x, y, z] direction vector, in units of meters.
# Returns:
# Tuple[float, float, float]: yaw, pitch, roll in degrees.
# """
# if np.allclose(direction_vector, 0):
# logger.warning('Camera is on the top of the target, cannot look at it.')
# return (0.0, 0.0, 0.0)
# if not isinstance(direction_vector, np.ndarray):
# direction_vector = np.array(direction_vector)
# direction_vector = direction_vector / np.linalg.norm(direction_vector) # Normalize the direction vector
# rotation = R.align_vectors([direction_vector], [cls.axis_zero_direction])[0].as_euler('xyz', degrees=True)
# return rotation.tolist()