Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into wip/spline-rod
Browse files Browse the repository at this point in the history
  • Loading branch information
skim0119 committed Sep 8, 2024
2 parents 67aafda + 96190a1 commit 870bd3c
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 6 deletions.
51 changes: 51 additions & 0 deletions examples/camera_movement.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion src/bsr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,4 +31,5 @@ def get_version() -> str:


version: Final[str] = get_version()
camera_manager = CameraManager()
frame_manager = FrameManager()
5 changes: 0 additions & 5 deletions src/bsr/blender_commands/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
171 changes: 171 additions & 0 deletions src/bsr/camera.py
Original file line number Diff line number Diff line change
@@ -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]]
)

0 comments on commit 870bd3c

Please sign in to comment.