diff --git a/examples/camera_movement.py b/examples/camera_movement.py new file mode 100644 index 0000000..0c6b2e6 --- /dev/null +++ b/examples/camera_movement.py @@ -0,0 +1,51 @@ +import numpy as np + +import bsr + + +def main(filename: str = "camera_movement"): + + frame_rate = 60 + total_time = 5 + + camera_heigh = 1.0 + camera_radius = 1.0 + + bsr.clear_mesh_objects() + + bsr.camera_manager.look_at = np.array([0.0, 0.0, 0.0]) + + for k, angle in enumerate( + np.linspace(0.0, 360.0, frame_rate * total_time + 1) + ): + bsr.camera_manager.location = np.array( + [ + camera_radius * np.cos(np.radians(angle)), + camera_radius * np.sin(np.radians(angle)), + camera_heigh, + ] + ) + bsr.camera_manager.set_keyframe(k) + bsr.frame_manager.update() + + # Set the final keyframe number + bsr.frame_manager.set_frame_end() + + # Set the frame rate + bsr.frame_manager.set_frame_rate(fps=frame_rate) + + # Set the view distance + bsr.set_view_distance(distance=5) + + # Deslect all objects + bsr.deselect_all() + + # Select the camera object + bsr.camera_manager.select() + + # Save as .blend file + bsr.save(filename + ".blend") + + +if __name__ == "__main__": + main() diff --git a/src/bsr/__init__.py b/src/bsr/__init__.py index 7bffd7f..fed7240 100644 --- a/src/bsr/__init__.py +++ b/src/bsr/__init__.py @@ -13,8 +13,8 @@ clear_mesh_objects, deselect_all, scene_update, - select_camera, ) +from .camera import CameraManager from .frame import FrameManager from .geometry.composite.rod import Rod, RodWithBox, RodWithCylinder from .geometry.composite.stack import RodStack, create_rod_collection @@ -31,4 +31,5 @@ def get_version() -> str: version: Final[str] = get_version() +camera_manager = CameraManager() frame_manager = FrameManager() diff --git a/src/bsr/blender_commands/macros.py b/src/bsr/blender_commands/macros.py index 823222b..ed41df6 100644 --- a/src/bsr/blender_commands/macros.py +++ b/src/bsr/blender_commands/macros.py @@ -32,8 +32,3 @@ def clear_materials() -> None: def deselect_all() -> None: # Deselect all objects bpy.ops.object.select_all(action="DESELECT") - - -def select_camera() -> None: - # Select the camera object - bpy.context.view_layer.objects.active = bpy.data.objects["Camera"] diff --git a/src/bsr/camera.py b/src/bsr/camera.py new file mode 100644 index 0000000..641dc31 --- /dev/null +++ b/src/bsr/camera.py @@ -0,0 +1,171 @@ +from typing import Optional + +import bpy +import numpy as np + +from bsr.tools.keyframe_mixin import KeyFrameControlMixin + + +class CameraManager(KeyFrameControlMixin): + """ + This class provides methods for manipulating the camera of the scene. + Only one instance exist, which you can access by: bsr.camera_manager. + """ + + def __init__(self, name: str = "Camera") -> None: + """ + Constructor for camera manager. + """ + self.name = name + self.__look_at_location: Optional[np.ndarray] = None + self.__sky = np.array([0.0, 0.0, 1.0]) + + @property + def camera(self) -> bpy.types.Object: + """ + Return the camera object. + """ + return bpy.data.objects[self.name] + + def select(self) -> None: + """ + Select the camera object. + """ + bpy.context.view_layer.objects.active = self.camera + + def set_keyframe(self, keyframe: int) -> None: + """ + Sets a keyframe at the given frame. + + Parameters + ---------- + keyframe : int + """ + self.camera.keyframe_insert(data_path="location", frame=keyframe) + self.camera.keyframe_insert(data_path="rotation_euler", frame=keyframe) + + def set_film_transparent(self, transparent: bool = True) -> None: + """ + Set the film transparency for rendering. + + Parameters + ---------- + transparent : bool, optional + Whether the film should be transparent. Default is True. + """ + bpy.context.scene.render.film_transparent = transparent + + @property + def is_film_transparent(self) -> bool: + """ + Check if the film is set to transparent. + + Returns + ------- + bool + True if the film is transparent, False otherwise. + """ + film_transparent: bool = bpy.context.scene.render.film_transparent + return film_transparent + + @property + def location(self) -> np.ndarray: + """ + Return the current location of the camera. + """ + return np.array(self.camera.location) + + @location.setter + def location(self, location: np.ndarray) -> None: + """ + Set the location of the camera. If the look at location is set, the camera will be rotated to look at that location. + + Parameters + ---------- + location : np.array + The location of the camera. + """ + assert isinstance( + location, np.ndarray + ), "location must be a numpy array" + assert len(location) == 3, "location must have 3 elements" + self.camera.location = location + if self.__look_at_location is not None: + self.camera.matrix_world = self.compute_matrix_world( + location=self.location, + direction=self.__look_at_location - self.location, + sky=self.__sky, + ) + + @property + def look_at(self) -> Optional[np.ndarray]: + """ + Return the location the camera is looking at. + """ + return self.__look_at_location + + @look_at.setter + def look_at(self, location: np.ndarray) -> None: + """ + Set the direction the camera is looking at. + + Parameters + ---------- + location : np.array + The direction the camera is looking at. + """ + assert isinstance( + location, np.ndarray + ), "location must be a numpy array" + assert len(location) == 3, "location must have 3 elements" + assert ( + np.allclose(self.location, location) == False + ), "camera and look at location must be different" + + self.__look_at_location = location + self.camera.matrix_world = self.compute_matrix_world( + location=self.location, + direction=self.__look_at_location - self.location, + sky=self.__sky, + ) + + @staticmethod + def compute_matrix_world( + location: np.ndarray, direction: np.ndarray, sky: np.ndarray + ) -> np.ndarray: + """ + Compute the world matrix of the camera. + + Parameters + ---------- + location : np.array + The location of the camera. + direction : np.array + The direction the camera is looking at. + sky : np.array + The sky direction of the camera. (unit vector) + + Returns + ------- + np.array + The world matrix of the camera. + """ + assert isinstance( + location, np.ndarray + ), "location must be a numpy array" + assert len(location) == 3, "location must have 3 elements" + assert isinstance( + direction, np.ndarray + ), "direction must be a numpy array" + assert len(direction) == 3, "direction must have 3 elements" + assert isinstance(sky, np.ndarray), "sky must be a numpy array" + assert len(sky) == 3, "sky must have 3 elements" + assert np.linalg.norm(sky) == 1, "sky must be a unit vector" + + direction = direction / np.linalg.norm(direction) + right = np.cross(direction, sky) + up = np.cross(right, direction) + + return np.array( + [[*right, 0.0], [*up, 0.0], [*(-direction), 0.0], [*location, 1.0]] + )