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 ..object.object_utils import ObjectUtilsBlender
from ..renderer.renderer_blender import RendererBlender
from ..rpc import remote_blender
from .sequence_base import SequenceBase
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 RenderPass, TransformKeys # isort:skip
except (ImportError, ModuleNotFoundError):
pass
[docs]
@remote_blender(dec_class=True, suffix='_in_engine')
class SequenceBlender(SequenceBase):
"""Sequence class for Blender."""
_actor = ActorBlender
_camera = CameraBlender
_renderer = RendererBlender
_object_utils = ObjectUtilsBlender
# -------- spawn methods -------- #
[docs]
@classmethod
def import_actor_with_keys(
cls,
file_path: PathLike,
transform_keys: 'TransformKeys',
actor_name: str = None,
stencil_value: int = 1,
) -> 'ActorBlender':
"""Import an actor from a file to the sequence and add transform keyframes to
the it.
Args:
name (str): Name of the actor in Blender.
path (PathLike): File path used for importing the actor.
transform_keys (TransformKeys):Keyframes of transform (location, rotation, and scale).
stencil_value (int in [0, 255], optional): Stencil value of the actor. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
Returns:
ActorBlender: New imported actor.
"""
actor = cls.import_actor(
actor_name=actor_name,
file_path=file_path,
stencil_value=stencil_value,
)
cls._object_utils.set_transform_keys(name=actor.name, transform_keys=transform_keys)
return actor
[docs]
@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) ########
#####################################
@staticmethod
def _new_seq_in_engine(
level: str = None,
seq_name: str = 'Sequence',
seq_fps: int = 60,
seq_length: int = 1,
replace: bool = False,
) -> None:
"""Create a new sequence in Blender. (Create a collection named 'seq_name' and
link it to the scene named 'level').
Args:
seq_name (str): _description_
link_levels (Optional[Union[List[str],str]], optional): _description_. Defaults to [].
seq_fps (Optional[float], optional): _description_. Defaults to None.
seq_length (Optional[int], optional): _description_. Defaults to None.
replace (bool, optional): _description_. Defaults to False.
"""
# get level scene
level_scene = (
XRFeitoriaBlenderFactory.get_scene(level) if level else XRFeitoriaBlenderFactory.get_active_scene()
)
# new collection named seq_name
new_seq_collection = XRFeitoriaBlenderFactory.new_collection(name=seq_name, replace=replace)
# link sequence(new_seq_collection) to level(level_scene)
XRFeitoriaBlenderFactory.link_collection_to_scene(collection=new_seq_collection, scene=level_scene)
XRFeitoriaBlenderFactory.set_level_properties(scene=level_scene, active_seq=new_seq_collection)
# set sequence properties
XRFeitoriaBlenderFactory.set_sequence_properties(
collection=new_seq_collection,
level=level_scene,
fps=seq_fps,
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
level_scene.frame_current = 0
level_scene.render.fps = seq_fps
# set scene to active
XRFeitoriaBlenderFactory.set_scene_active(level_scene)
# set collection to active
XRFeitoriaBlenderFactory.set_collection_active(new_seq_collection)
# tag the collection as sequence collection
XRFeitoriaBlenderFactory.tag_sequence_collection(collection=new_seq_collection)
@staticmethod
def _open_seq_in_engine(seq_name: str) -> None:
"""Open an exist sequence in Blender. (Link the collection of the sequence to
its level scene.)
Args:
seq_name (str): Name of the sequence.
"""
# XRFeitoriaBlenderFactory.open_sequence(seq_name)
level_scene = XRFeitoriaBlenderFactory.get_active_scene()
active_sequence = XRFeitoriaBlenderFactory.get_collection(seq_name)
XRFeitoriaBlenderFactory.set_level_properties(scene=level_scene, active_seq=active_sequence)
@staticmethod
def _close_seq_in_engine() -> None:
"""Close the opened sequence in Blender.
(Unlink the collection of the sequence from the active scene.)
"""
# XRFeitoriaBlenderFactory.close_sequence()
level_scene = XRFeitoriaBlenderFactory.get_active_scene()
XRFeitoriaBlenderFactory.set_level_properties(scene=level_scene, active_seq=None)
# -------- spawn methods -------- #
@staticmethod
def _import_actor_in_engine(
file_path: str,
transform_keys: 'Union[List[Dict], Dict]',
actor_name: str = 'Actor',
stencil_value: int = 1,
) -> None:
"""Import.
Args:
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]
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
actor = bpy.data.objects[actor_name]
actor.pass_index = stencil_value
for child in actor.children_recursive:
child.pass_index = stencil_value
@staticmethod
def _spawn_camera_in_engine(
transform_keys: 'Union[List[Dict], Dict]',
fov: float = 90.0,
aspect_ratio: float = 16.0 / 9.0,
camera_name: str = 'Camera',
) -> None:
"""Spawn a camera in the engine.
Args:
transform_keys (Union[List[Dict], Dict]): Keyframes of transform (location, rotation, and scale).
fov (float, optional): Field of view of the camera lens, in degrees. Defaults to 90.0.
camera_name (str, optional): Name of the camera. Defaults to 'Camera'.
"""
if not isinstance(transform_keys, list):
transform_keys = [transform_keys]
CameraBlender._spawn_in_engine(camera_name=camera_name, fov=fov)
ObjectUtilsBlender._set_transform_keys_in_engine(obj_name=camera_name, transform_keys=transform_keys)
@staticmethod
def _spawn_shape_in_engine(
type: str,
transform_keys: 'Union[List[Dict], Dict]',
shape_name: str = 'Shape',
stencil_value: int = 1,
size: float = 1.0,
segments: int = 32,
ring_count: int = 16,
radius: float = 1.0,
subdivisions: int = 2,
vertices: int = 32,
depth: float = 2.0,
radius1: float = 0.0,
radius2: float = 2.0,
) -> None:
"""Spawn a shape in the engine.
Args:
type (str, enum in ['cube', 'plane', 'cone', 'cylinder', 'sphere', 'ico_sphere']): Type of the shape.
transform_keys (Union[List[Dict], Dict]): Keyframes of transform (location, rotation, and scale).
shape_name (str, optional): Name of the shape. Defaults to 'Shape'.
stencil_value (int, optional): Stencil value of the shape. Defaults to 1.
size (float in [0, inf], optional): Size. Defaults to 1.0. (unit: meter)
segments (int in [3, 100000], optional): Segments. Defaults to 32.
ring_count (int in [3, 100000], optional): Ring count. Defaults to 16.
radius (float in [0, inf], optional): Radius. Defaults to 1.0. (unit: meter)
subdivisions (int in [1, 10], optional): Subdivisions. Defaults to 2.
vertices (int in [3, 10000000], optional): Vertices. Defaults to 32.
depth (float in [0, inf], optional): Depth. Defaults to 2.0. (unit: meter)
radius1 (float in [0, inf], optional): Radius1. Defaults to 0.0. (unit: meter)
radius2 (float in [0, inf], optional): Radius2. Defaults to 2.0. (unit: meter)
"""
if not isinstance(transform_keys, list):
transform_keys = [transform_keys]
ShapeBlenderWrapper._spawn_shape_in_engine(
name=shape_name,
type=type,
size=size,
segments=segments,
ring_count=ring_count,
radius=radius1,
subdivisions=subdivisions,
vertices=vertices,
depth=depth,
radius1=radius1,
radius2=radius2,
)
ObjectUtilsBlender._set_transform_keys_in_engine(obj_name=shape_name, transform_keys=transform_keys)
# XXX: set stencil value. may use actor property
actor = bpy.data.objects[shape_name]
actor.pass_index = stencil_value
for child in actor.children_recursive:
child.pass_index = stencil_value
# -------- use methods -------- #
@staticmethod
def _use_camera_in_engine(
transform_keys: 'Union[List[Dict], Dict]',
fov: float = 90.0,
aspect_ratio: float = 16.0 / 9.0,
camera_name: str = 'Camera',
) -> None:
"""Use level camera to render in the sequence in the engine.
Args:
camera_name (str): Name of the camera.
"""
import math
if not isinstance(transform_keys, list):
transform_keys = [transform_keys]
# get actor by name
camera = bpy.data.objects[camera_name]
# set sequence properties
collection = XRFeitoriaBlenderFactory.get_active_collection()
level_camera_data = collection.sequence_properties.level_cameras.add()
level_camera_data.camera = camera
level_camera_data.location = camera.location
level_camera_data.rotation = camera.rotation_euler
level_camera_data.scale = camera.scale
level_camera_data.level_fov = camera.data.angle
level_camera_data.sequence_fov = math.radians(fov)
# set level camera's properties
CameraBlender._set_camera_fov_in_engine(name=camera_name, fov=fov)
ObjectUtilsBlender._set_transform_keys_in_engine(obj_name=camera_name, transform_keys=transform_keys)
level_camera_data.sequence_animation = camera.animation_data.action if camera.animation_data else None
# set camera activity
scene = XRFeitoriaBlenderFactory.get_active_scene()
XRFeitoriaBlenderFactory.set_camera_activity(camera_name=camera_name, scene=scene, active=True)
@staticmethod
def _use_actor_in_engine(
actor_name: str,
transform_keys: 'Union[List[Dict], Dict]',
stencil_value: int,
anim_asset_path: 'Optional[str]' = None,
):
if not isinstance(transform_keys, list):
transform_keys = [transform_keys]
# get actor by name
actor = bpy.data.objects[actor_name]
# if anim_asset_path is None, use the current action of the actor if it has one
# else, use the action of the anim_asset_path
if anim_asset_path is None:
if actor.animation_data and actor.animation_data.action:
action = actor.animation_data.action
else:
action = None
else:
action = bpy.data.actions[anim_asset_path]
# set sequence properties
collection = XRFeitoriaBlenderFactory.get_active_collection()
level_actor_data = collection.sequence_properties.level_actors.add()
level_actor_data.actor = actor
level_actor_data.level_stencil_value = actor.pass_index
level_actor_data.sequence_stencil_value = stencil_value
level_actor_data.level_animation = actor.animation_data.action if actor.animation_data else None
level_actor_data.sequence_animation = action
level_actor_data.location = actor.location
level_actor_data.rotation = actor.rotation_euler
level_actor_data.scale = actor.scale
# set level actor's properties
actor.pass_index = stencil_value
for child in actor.children_recursive:
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