Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] add camera #28

Merged
merged 7 commits into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
from .geometry.composite.stack import RodStack, create_rod_collection
Expand All @@ -30,4 +30,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 @@ -29,8 +29,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]]
)
Loading