mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
camera.py -> cairo_camera.py; delete other cairo camera subclasses
This commit is contained in:
parent
f90c9a2e95
commit
9c40838fcb
11 changed files with 9 additions and 1652 deletions
|
|
@ -38,11 +38,7 @@ Cameras
|
|||
*******
|
||||
|
||||
.. inheritance-diagram::
|
||||
manim.camera.camera
|
||||
manim.camera.mapping_camera
|
||||
manim.camera.moving_camera
|
||||
manim.camera.multi_camera
|
||||
manim.camera.three_d_camera
|
||||
manim.camera.cairo_camera
|
||||
:parts: 1
|
||||
:top-classes: manim.camera.camera.Camera, manim.mobject.mobject.Mobject
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,7 @@ from .animation.transform import *
|
|||
from .animation.transform_matching_parts import *
|
||||
from .animation.updaters.mobject_update_utils import *
|
||||
from .animation.updaters.update import *
|
||||
from .camera.camera import *
|
||||
from .camera.mapping_camera import *
|
||||
from .camera.moving_camera import *
|
||||
from .camera.multi_camera import *
|
||||
from .camera.three_d_camera import *
|
||||
from .camera.cairo_camera import *
|
||||
from .constants import *
|
||||
from .mobject.frame import *
|
||||
from .mobject.geometry.arc import *
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ LINE_JOIN_MAP = {
|
|||
}
|
||||
|
||||
|
||||
class Camera:
|
||||
"""Base camera class.
|
||||
class CairoCamera:
|
||||
"""Cairo rendering implementation.
|
||||
|
||||
This is the object which takes care of what exactly is displayed
|
||||
on screen at any given moment.
|
||||
|
|
@ -1211,135 +1211,3 @@ class Camera:
|
|||
centered_space_coords = centered_space_coords * (1, -1)
|
||||
|
||||
return centered_space_coords
|
||||
|
||||
|
||||
# NOTE: The methods of the following class have not been mentioned outside of their definitions.
|
||||
# Their DocStrings are not as detailed as preferred.
|
||||
class BackgroundColoredVMobjectDisplayer:
|
||||
def __init__(self, camera: Camera):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
camera
|
||||
Camera object to use.
|
||||
"""
|
||||
self.camera = camera
|
||||
self.file_name_to_pixel_array_map = {}
|
||||
self.pixel_array = np.array(camera.pixel_array)
|
||||
self.reset_pixel_array()
|
||||
|
||||
def reset_pixel_array(self):
|
||||
self.pixel_array[:, :] = 0
|
||||
|
||||
def resize_background_array(
|
||||
self,
|
||||
background_array: np.ndarray,
|
||||
new_width: float,
|
||||
new_height: float,
|
||||
mode: str = "RGBA",
|
||||
):
|
||||
"""Resizes the pixel array representing the background.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
background_array
|
||||
The pixel
|
||||
new_width
|
||||
The new width of the background
|
||||
new_height
|
||||
The new height of the background
|
||||
mode
|
||||
The PIL image mode, by default "RGBA"
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The numpy pixel array of the resized background.
|
||||
"""
|
||||
image = Image.fromarray(background_array)
|
||||
image = image.convert(mode)
|
||||
resized_image = image.resize((new_width, new_height))
|
||||
return np.array(resized_image)
|
||||
|
||||
def resize_background_array_to_match(
|
||||
self, background_array: np.ndarray, pixel_array: np.ndarray
|
||||
):
|
||||
"""Resizes the background array to match the passed pixel array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
background_array
|
||||
The prospective pixel array.
|
||||
pixel_array
|
||||
The pixel array whose width and height should be matched.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The resized background array.
|
||||
"""
|
||||
height, width = pixel_array.shape[:2]
|
||||
mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB"
|
||||
return self.resize_background_array(background_array, width, height, mode)
|
||||
|
||||
def get_background_array(self, image: Image.Image | pathlib.Path | str):
|
||||
"""Gets the background array that has the passed file_name.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image
|
||||
The background image or its file name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
The pixel array of the image.
|
||||
"""
|
||||
image_key = str(image)
|
||||
|
||||
if image_key in self.file_name_to_pixel_array_map:
|
||||
return self.file_name_to_pixel_array_map[image_key]
|
||||
if isinstance(image, str):
|
||||
full_path = get_full_raster_image_path(image)
|
||||
image = Image.open(full_path)
|
||||
back_array = np.array(image)
|
||||
|
||||
pixel_array = self.pixel_array
|
||||
if not np.all(pixel_array.shape == back_array.shape):
|
||||
back_array = self.resize_background_array_to_match(back_array, pixel_array)
|
||||
|
||||
self.file_name_to_pixel_array_map[image_key] = back_array
|
||||
return back_array
|
||||
|
||||
def display(self, *cvmobjects: VMobject):
|
||||
"""Displays the colored VMobjects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*cvmobjects
|
||||
The VMobjects
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The pixel array with the `cvmobjects` displayed.
|
||||
"""
|
||||
batch_image_pairs = it.groupby(cvmobjects, lambda cv: cv.get_background_image())
|
||||
curr_array = None
|
||||
for image, batch in batch_image_pairs:
|
||||
background_array = self.get_background_array(image)
|
||||
pixel_array = self.pixel_array
|
||||
self.camera.display_multiple_non_background_colored_vmobjects(
|
||||
batch,
|
||||
pixel_array,
|
||||
)
|
||||
new_array = np.array(
|
||||
(background_array * pixel_array.astype("float") / 255),
|
||||
dtype=self.camera.pixel_array_dtype,
|
||||
)
|
||||
if curr_array is None:
|
||||
curr_array = new_array
|
||||
else:
|
||||
curr_array = np.maximum(curr_array, new_array)
|
||||
self.reset_pixel_array()
|
||||
return curr_array
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
"""A camera that allows mapping between objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MappingCamera", "OldMultiCamera", "SplitScreenCamera"]
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..camera.camera import Camera
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..utils.config_ops import DictAsObject
|
||||
|
||||
# TODO: Add an attribute to mobjects under which they can specify that they should just
|
||||
# map their centers but remain otherwise undistorted (useful for labels, etc.)
|
||||
|
||||
|
||||
class MappingCamera(Camera):
|
||||
"""Camera object that allows mapping
|
||||
between objects.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mapping_func=lambda p: p,
|
||||
min_num_curves=50,
|
||||
allow_object_intrusion=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.mapping_func = mapping_func
|
||||
self.min_num_curves = min_num_curves
|
||||
self.allow_object_intrusion = allow_object_intrusion
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def points_to_pixel_coords(self, mobject, points):
|
||||
return super().points_to_pixel_coords(
|
||||
mobject,
|
||||
np.apply_along_axis(self.mapping_func, 1, points),
|
||||
)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
|
||||
if self.allow_object_intrusion:
|
||||
mobject_copies = mobjects
|
||||
else:
|
||||
mobject_copies = [mobject.copy() for mobject in mobjects]
|
||||
for mobject in mobject_copies:
|
||||
if (
|
||||
isinstance(mobject, VMobject)
|
||||
and 0 < mobject.get_num_curves() < self.min_num_curves
|
||||
):
|
||||
mobject.insert_n_curves(self.min_num_curves)
|
||||
super().capture_mobjects(
|
||||
mobject_copies,
|
||||
include_submobjects=False,
|
||||
excluded_mobjects=None,
|
||||
)
|
||||
|
||||
|
||||
# Note: This allows layering of multiple cameras onto the same portion of the pixel array,
|
||||
# the later cameras overwriting the former
|
||||
#
|
||||
# TODO: Add optional separator borders between cameras (or perhaps peel this off into a
|
||||
# CameraPlusOverlay class)
|
||||
|
||||
# TODO, the classes below should likely be deleted
|
||||
class OldMultiCamera(Camera):
|
||||
def __init__(self, *cameras_with_start_positions, **kwargs):
|
||||
self.shifted_cameras = [
|
||||
DictAsObject(
|
||||
{
|
||||
"camera": camera_with_start_positions[0],
|
||||
"start_x": camera_with_start_positions[1][1],
|
||||
"start_y": camera_with_start_positions[1][0],
|
||||
"end_x": camera_with_start_positions[1][1]
|
||||
+ camera_with_start_positions[0].pixel_width,
|
||||
"end_y": camera_with_start_positions[1][0]
|
||||
+ camera_with_start_positions[0].pixel_height,
|
||||
},
|
||||
)
|
||||
for camera_with_start_positions in cameras_with_start_positions
|
||||
]
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
self.pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
] = shifted_camera.camera.pixel_array
|
||||
|
||||
def set_background(self, pixel_array, **kwargs):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_background(
|
||||
pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def set_pixel_array(self, pixel_array, **kwargs):
|
||||
super().set_pixel_array(pixel_array, **kwargs)
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_pixel_array(
|
||||
pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def init_background(self):
|
||||
super().init_background()
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.init_background()
|
||||
|
||||
|
||||
# A OldMultiCamera which, when called with two full-size cameras, initializes itself
|
||||
# as a split screen, also taking care to resize each individual camera within it
|
||||
|
||||
|
||||
class SplitScreenCamera(OldMultiCamera):
|
||||
def __init__(self, left_camera, right_camera, **kwargs):
|
||||
Camera.__init__(self, **kwargs) # to set attributes such as pixel_width
|
||||
self.left_camera = left_camera
|
||||
self.right_camera = right_camera
|
||||
|
||||
half_width = math.ceil(self.pixel_width / 2)
|
||||
for camera in [self.left_camera, self.right_camera]:
|
||||
camera.reset_pixel_shape(camera.pixel_height, half_width)
|
||||
|
||||
super().__init__(
|
||||
(left_camera, (0, 0)),
|
||||
(right_camera, (0, half_width)),
|
||||
)
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
"""A camera able to move through a scene.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:mod:`.moving_camera_scene`
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MovingCamera"]
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
from ..constants import DOWN, LEFT, RIGHT, UP
|
||||
from ..mobject.frame import ScreenRectangle
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..utils.color import WHITE
|
||||
|
||||
|
||||
class MovingCamera(Camera):
|
||||
"""
|
||||
Stays in line with the height, width and position of it's 'frame', which is a Rectangle
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:class:`.MovingCameraScene`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frame=None,
|
||||
fixed_dimension=0, # width
|
||||
default_frame_stroke_color=WHITE,
|
||||
default_frame_stroke_width=0,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Frame is a Mobject, (should almost certainly be a rectangle)
|
||||
determining which region of space the camera displays
|
||||
"""
|
||||
self.fixed_dimension = fixed_dimension
|
||||
self.default_frame_stroke_color = default_frame_stroke_color
|
||||
self.default_frame_stroke_width = default_frame_stroke_width
|
||||
if frame is None:
|
||||
frame = ScreenRectangle(height=config["frame_height"])
|
||||
frame.set_stroke(
|
||||
self.default_frame_stroke_color,
|
||||
self.default_frame_stroke_width,
|
||||
)
|
||||
self.frame = frame
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# TODO, make these work for a rotated frame
|
||||
@property
|
||||
def frame_height(self):
|
||||
"""Returns the height of the frame.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The height of the frame.
|
||||
"""
|
||||
return self.frame.height
|
||||
|
||||
@property
|
||||
def frame_width(self):
|
||||
"""Returns the width of the frame
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The width of the frame.
|
||||
"""
|
||||
return self.frame.width
|
||||
|
||||
@property
|
||||
def frame_center(self):
|
||||
"""Returns the centerpoint of the frame in cartesian coordinates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The cartesian coordinates of the center of the frame.
|
||||
"""
|
||||
return self.frame.get_center()
|
||||
|
||||
@frame_height.setter
|
||||
def frame_height(self, frame_height: float):
|
||||
"""Sets the height of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_height
|
||||
The new frame_height.
|
||||
"""
|
||||
self.frame.stretch_to_fit_height(frame_height)
|
||||
|
||||
@frame_width.setter
|
||||
def frame_width(self, frame_width: float):
|
||||
"""Sets the width of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_width
|
||||
The new frame_width.
|
||||
"""
|
||||
self.frame.stretch_to_fit_width(frame_width)
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, frame_center: np.ndarray | list | tuple | Mobject):
|
||||
"""Sets the centerpoint of the frame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_center
|
||||
The point to which the frame must be moved.
|
||||
If is of type mobject, the frame will be moved to
|
||||
the center of that mobject.
|
||||
"""
|
||||
self.frame.move_to(frame_center)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
# self.reset_frame_center()
|
||||
# self.realign_frame_shape()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
# Since the frame can be moving around, the cairo
|
||||
# context used for updating should be regenerated
|
||||
# at each frame. So no caching.
|
||||
def get_cached_cairo_context(self, pixel_array):
|
||||
"""
|
||||
Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
return None
|
||||
|
||||
def cache_cairo_context(self, pixel_array, ctx):
|
||||
"""
|
||||
Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
pass
|
||||
|
||||
# def reset_frame_center(self):
|
||||
# self.frame_center = self.frame.get_center()
|
||||
|
||||
# def realign_frame_shape(self):
|
||||
# height, width = self.frame_shape
|
||||
# if self.fixed_dimension == 0:
|
||||
# self.frame_shape = (height, self.frame.width
|
||||
# else:
|
||||
# self.frame_shape = (self.frame.height, width)
|
||||
# self.resize_frame_shape(fixed_dimension=self.fixed_dimension)
|
||||
|
||||
def get_mobjects_indicating_movement(self):
|
||||
"""
|
||||
Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
"""
|
||||
return [self.frame]
|
||||
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: list[Mobject],
|
||||
margin: float = 0,
|
||||
only_mobjects_in_frame: bool = False,
|
||||
animate: bool = True,
|
||||
):
|
||||
"""Zooms on to a given array of mobjects (or a singular mobject)
|
||||
and automatically resizes to frame all the mobjects.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
This method only works when 2D-objects in the XY-plane are considered, it
|
||||
will not work correctly when the camera has been rotated.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobject or array of mobjects that the camera will focus on.
|
||||
|
||||
margin
|
||||
The width of the margin that is added to the frame (optional, 0 by default).
|
||||
|
||||
only_mobjects_in_frame
|
||||
If set to ``True``, only allows focusing on mobjects that are already in frame.
|
||||
|
||||
animate
|
||||
If set to ``False``, applies the changes instead of returning the corresponding animation
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[_AnimationBuilder, ScreenRectangle]
|
||||
_AnimationBuilder that zooms the camera view to a given list of mobjects
|
||||
or ScreenRectangle with position and size updated to zoomed position.
|
||||
|
||||
"""
|
||||
scene_critical_x_left = None
|
||||
scene_critical_x_right = None
|
||||
scene_critical_y_up = None
|
||||
scene_critical_y_down = None
|
||||
|
||||
for m in mobjects:
|
||||
if (m == self.frame) or (
|
||||
only_mobjects_in_frame and not self.is_in_frame(m)
|
||||
):
|
||||
# detected camera frame, should not be used to calculate final position of camera
|
||||
continue
|
||||
|
||||
# initialize scene critical points with first mobjects critical points
|
||||
if scene_critical_x_left is None:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
|
||||
else:
|
||||
if m.get_critical_point(LEFT)[0] < scene_critical_x_left:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
|
||||
if m.get_critical_point(RIGHT)[0] > scene_critical_x_right:
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
|
||||
if m.get_critical_point(UP)[1] > scene_critical_y_up:
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
|
||||
if m.get_critical_point(DOWN)[1] < scene_critical_y_down:
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
|
||||
# calculate center x and y
|
||||
x = (scene_critical_x_left + scene_critical_x_right) / 2
|
||||
y = (scene_critical_y_up + scene_critical_y_down) / 2
|
||||
|
||||
# calculate proposed width and height of zoomed scene
|
||||
new_width = abs(scene_critical_x_left - scene_critical_x_right)
|
||||
new_height = abs(scene_critical_y_up - scene_critical_y_down)
|
||||
|
||||
m_target = self.frame.animate if animate else self.frame
|
||||
# zoom to fit all mobjects along the side that has the largest size
|
||||
if new_width / self.frame.width > new_height / self.frame.height:
|
||||
return m_target.set_x(x).set_y(y).set(width=new_width + margin)
|
||||
else:
|
||||
return m_target.set_x(x).set_y(y).set(height=new_height + margin)
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
"""A camera supporting multiple perspectives."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MultiCamera"]
|
||||
|
||||
|
||||
from manim.mobject.types.image_mobject import ImageMobject
|
||||
|
||||
from ..camera.moving_camera import MovingCamera
|
||||
from ..utils.iterables import list_difference_update
|
||||
|
||||
|
||||
class MultiCamera(MovingCamera):
|
||||
"""Camera Object that allows for multiple perspectives."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image_mobjects_from_cameras: ImageMobject | None = None,
|
||||
allow_cameras_to_capture_their_own_display=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialises the MultiCamera
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_mobjects_from_cameras
|
||||
|
||||
kwargs
|
||||
Any valid keyword arguments of MovingCamera.
|
||||
"""
|
||||
self.image_mobjects_from_cameras = []
|
||||
if image_mobjects_from_cameras is not None:
|
||||
for imfc in image_mobjects_from_cameras:
|
||||
self.add_image_mobject_from_camera(imfc)
|
||||
self.allow_cameras_to_capture_their_own_display = (
|
||||
allow_cameras_to_capture_their_own_display
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_image_mobject_from_camera(self, image_mobject_from_camera: ImageMobject):
|
||||
"""Adds an ImageMobject that's been obtained from the camera
|
||||
into the list ``self.image_mobject_from_cameras``
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_mobject_from_camera
|
||||
The ImageMobject to add to self.image_mobject_from_cameras
|
||||
"""
|
||||
# A silly method to have right now, but maybe there are things
|
||||
# we want to guarantee about any imfc's added later.
|
||||
imfc = image_mobject_from_camera
|
||||
assert isinstance(imfc.camera, MovingCamera)
|
||||
self.image_mobjects_from_cameras.append(imfc)
|
||||
|
||||
def update_sub_cameras(self):
|
||||
"""Reshape sub_camera pixel_arrays"""
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
pixel_height, pixel_width = self.pixel_array.shape[:2]
|
||||
imfc.camera.frame_shape = (
|
||||
imfc.camera.frame.height,
|
||||
imfc.camera.frame.width,
|
||||
)
|
||||
imfc.camera.reset_pixel_shape(
|
||||
int(pixel_height * imfc.height / self.frame_height),
|
||||
int(pixel_width * imfc.width / self.frame_width),
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
"""Resets the MultiCamera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
MultiCamera
|
||||
The reset MultiCamera
|
||||
"""
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
imfc.camera.reset()
|
||||
super().reset()
|
||||
return self
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
self.update_sub_cameras()
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
to_add = list(mobjects)
|
||||
if not self.allow_cameras_to_capture_their_own_display:
|
||||
to_add = list_difference_update(to_add, imfc.get_family())
|
||||
imfc.camera.capture_mobjects(to_add, **kwargs)
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_mobjects_indicating_movement(self):
|
||||
"""Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
"""
|
||||
return [self.frame] + [
|
||||
imfc.camera.frame for imfc in self.image_mobjects_from_cameras
|
||||
]
|
||||
|
|
@ -1,441 +0,0 @@
|
|||
"""A camera that can be positioned and oriented in three-dimensional space."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["ThreeDCamera"]
|
||||
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.three_d.three_d_utils import (
|
||||
get_3d_vmob_end_corner,
|
||||
get_3d_vmob_end_corner_unit_normal,
|
||||
get_3d_vmob_start_corner,
|
||||
get_3d_vmob_start_corner_unit_normal,
|
||||
)
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
from ..constants import *
|
||||
from ..mobject.types.point_cloud_mobject import Point
|
||||
from ..utils.color import get_shaded_rgb
|
||||
from ..utils.family import extract_mobject_family_members
|
||||
from ..utils.space_ops import rotation_about_z, rotation_matrix
|
||||
|
||||
|
||||
class ThreeDCamera(Camera):
|
||||
def __init__(
|
||||
self,
|
||||
focal_distance=20.0,
|
||||
shading_factor=0.2,
|
||||
default_distance=5.0,
|
||||
light_source_start_point=9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
should_apply_shading=True,
|
||||
exponential_projection=False,
|
||||
phi=0,
|
||||
theta=-90 * DEGREES,
|
||||
gamma=0,
|
||||
zoom=1,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initializes the ThreeDCamera
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*kwargs
|
||||
Any keyword argument of Camera.
|
||||
"""
|
||||
self._frame_center = Point(kwargs.get("frame_center", ORIGIN), stroke_width=0)
|
||||
super().__init__(**kwargs)
|
||||
self.focal_distance = focal_distance
|
||||
self.phi = phi
|
||||
self.theta = theta
|
||||
self.gamma = gamma
|
||||
self.zoom = zoom
|
||||
self.shading_factor = shading_factor
|
||||
self.default_distance = default_distance
|
||||
self.light_source_start_point = light_source_start_point
|
||||
self.light_source = Point(self.light_source_start_point)
|
||||
self.should_apply_shading = should_apply_shading
|
||||
self.exponential_projection = exponential_projection
|
||||
self.max_allowable_norm = 3 * config["frame_width"]
|
||||
self.phi_tracker = ValueTracker(self.phi)
|
||||
self.theta_tracker = ValueTracker(self.theta)
|
||||
self.focal_distance_tracker = ValueTracker(self.focal_distance)
|
||||
self.gamma_tracker = ValueTracker(self.gamma)
|
||||
self.zoom_tracker = ValueTracker(self.zoom)
|
||||
self.fixed_orientation_mobjects = {}
|
||||
self.fixed_in_frame_mobjects = set()
|
||||
self.reset_rotation_matrix()
|
||||
|
||||
@property
|
||||
def frame_center(self):
|
||||
return self._frame_center.points[0]
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, point):
|
||||
self._frame_center.move_to(point)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
self.reset_rotation_matrix()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_value_trackers(self):
|
||||
"""Returns list of ValueTrackers of phi, theta, focal_distance and gamma
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
list of ValueTracker objects
|
||||
"""
|
||||
return [
|
||||
self.phi_tracker,
|
||||
self.theta_tracker,
|
||||
self.focal_distance_tracker,
|
||||
self.gamma_tracker,
|
||||
self.zoom_tracker,
|
||||
]
|
||||
|
||||
def modified_rgbas(self, vmobject, rgbas):
|
||||
if not self.should_apply_shading:
|
||||
return rgbas
|
||||
if vmobject.shade_in_3d and (vmobject.get_num_points() > 0):
|
||||
light_source_point = self.light_source.points[0]
|
||||
if len(rgbas) < 2:
|
||||
shaded_rgbas = rgbas.repeat(2, axis=0)
|
||||
else:
|
||||
shaded_rgbas = np.array(rgbas[:2])
|
||||
shaded_rgbas[0, :3] = get_shaded_rgb(
|
||||
shaded_rgbas[0, :3],
|
||||
get_3d_vmob_start_corner(vmobject),
|
||||
get_3d_vmob_start_corner_unit_normal(vmobject),
|
||||
light_source_point,
|
||||
)
|
||||
shaded_rgbas[1, :3] = get_shaded_rgb(
|
||||
shaded_rgbas[1, :3],
|
||||
get_3d_vmob_end_corner(vmobject),
|
||||
get_3d_vmob_end_corner_unit_normal(vmobject),
|
||||
light_source_point,
|
||||
)
|
||||
return shaded_rgbas
|
||||
return rgbas
|
||||
|
||||
def get_stroke_rgbas(
|
||||
self,
|
||||
vmobject,
|
||||
background=False,
|
||||
): # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_stroke_rgbas(background))
|
||||
|
||||
def get_fill_rgbas(self, vmobject): # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_fill_rgbas())
|
||||
|
||||
def get_mobjects_to_display(self, *args, **kwargs): # NOTE : DocStrings From parent
|
||||
mobjects = super().get_mobjects_to_display(*args, **kwargs)
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
def z_key(mob):
|
||||
if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d):
|
||||
return np.inf
|
||||
# Assign a number to a three dimensional mobjects
|
||||
# based on how close it is to the camera
|
||||
return np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
|
||||
|
||||
return sorted(mobjects, key=z_key)
|
||||
|
||||
def get_phi(self):
|
||||
"""Returns the Polar angle (the angle off Z_AXIS) phi.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Polar angle in radians.
|
||||
"""
|
||||
return self.phi_tracker.get_value()
|
||||
|
||||
def get_theta(self):
|
||||
"""Returns the Azimuthal i.e the angle that spins the camera around the Z_AXIS.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Azimuthal angle in radians.
|
||||
"""
|
||||
return self.theta_tracker.get_value()
|
||||
|
||||
def get_focal_distance(self):
|
||||
"""Returns focal_distance of the Camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The focal_distance of the Camera in MUnits.
|
||||
"""
|
||||
return self.focal_distance_tracker.get_value()
|
||||
|
||||
def get_gamma(self):
|
||||
"""Returns the rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The angle of rotation of the camera about the vector
|
||||
from the ORIGIN to the Camera in radians
|
||||
"""
|
||||
return self.gamma_tracker.get_value()
|
||||
|
||||
def get_zoom(self):
|
||||
"""Returns the zoom amount of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The zoom amount of the camera.
|
||||
"""
|
||||
return self.zoom_tracker.get_value()
|
||||
|
||||
def set_phi(self, value: float):
|
||||
"""Sets the polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new value of the polar angle in radians.
|
||||
"""
|
||||
self.phi_tracker.set_value(value)
|
||||
|
||||
def set_theta(self, value: float):
|
||||
"""Sets the azimuthal angle i.e the angle that spins the camera around Z_AXIS in radians.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new value of the azimuthal angle in radians.
|
||||
"""
|
||||
self.theta_tracker.set_value(value)
|
||||
|
||||
def set_focal_distance(self, value: float):
|
||||
"""Sets the focal_distance of the Camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The focal_distance of the Camera.
|
||||
"""
|
||||
self.focal_distance_tracker.set_value(value)
|
||||
|
||||
def set_gamma(self, value: float):
|
||||
"""Sets the angle of rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new angle of rotation of the camera.
|
||||
"""
|
||||
self.gamma_tracker.set_value(value)
|
||||
|
||||
def set_zoom(self, value: float):
|
||||
"""Sets the zoom amount of the camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The zoom amount of the camera.
|
||||
"""
|
||||
self.zoom_tracker.set_value(value)
|
||||
|
||||
def reset_rotation_matrix(self):
|
||||
"""Sets the value of self.rotation_matrix to
|
||||
the matrix corresponding to the current position of the camera
|
||||
"""
|
||||
self.rotation_matrix = self.generate_rotation_matrix()
|
||||
|
||||
def get_rotation_matrix(self):
|
||||
"""Returns the matrix corresponding to the current position of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The matrix corresponding to the current position of the camera.
|
||||
"""
|
||||
return self.rotation_matrix
|
||||
|
||||
def generate_rotation_matrix(self):
|
||||
"""Generates a rotation matrix based off the current position of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The matrix corresponding to the current position of the camera.
|
||||
"""
|
||||
phi = self.get_phi()
|
||||
theta = self.get_theta()
|
||||
gamma = self.get_gamma()
|
||||
matrices = [
|
||||
rotation_about_z(-theta - 90 * DEGREES),
|
||||
rotation_matrix(-phi, RIGHT),
|
||||
rotation_about_z(gamma),
|
||||
]
|
||||
result = np.identity(3)
|
||||
for matrix in matrices:
|
||||
result = np.dot(matrix, result)
|
||||
return result
|
||||
|
||||
def project_points(self, points: np.ndarray | list):
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed array of points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points
|
||||
The list of points to project.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The points after projecting.
|
||||
"""
|
||||
frame_center = self.frame_center
|
||||
focal_distance = self.get_focal_distance()
|
||||
zoom = self.get_zoom()
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
points = points - frame_center
|
||||
points = np.dot(points, rot_matrix.T)
|
||||
zs = points[:, 2]
|
||||
for i in 0, 1:
|
||||
if self.exponential_projection:
|
||||
# Proper projection would involve multiplying
|
||||
# x and y by d / (d-z). But for points with high
|
||||
# z value that causes weird artifacts, and applying
|
||||
# the exponential helps smooth it out.
|
||||
factor = np.exp(zs / focal_distance)
|
||||
lt0 = zs < 0
|
||||
factor[lt0] = focal_distance / (focal_distance - zs[lt0])
|
||||
else:
|
||||
factor = focal_distance / (focal_distance - zs)
|
||||
factor[(focal_distance - zs) < 0] = 10**6
|
||||
points[:, i] *= factor * zoom
|
||||
return points
|
||||
|
||||
def project_point(self, point: list | np.ndarray):
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point
|
||||
The point to project.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The point after projection.
|
||||
"""
|
||||
return self.project_points(point.reshape((1, 3)))[0, :]
|
||||
|
||||
def transform_points_pre_display(
|
||||
self,
|
||||
mobject,
|
||||
points,
|
||||
): # TODO: Write Docstrings for this Method.
|
||||
points = super().transform_points_pre_display(mobject, points)
|
||||
fixed_orientation = mobject in self.fixed_orientation_mobjects
|
||||
fixed_in_frame = mobject in self.fixed_in_frame_mobjects
|
||||
|
||||
if fixed_in_frame:
|
||||
return points
|
||||
if fixed_orientation:
|
||||
center_func = self.fixed_orientation_mobjects[mobject]
|
||||
center = center_func()
|
||||
new_center = self.project_point(center)
|
||||
return points + (new_center - center)
|
||||
else:
|
||||
return self.project_points(points)
|
||||
|
||||
def add_fixed_orientation_mobjects(
|
||||
self,
|
||||
*mobjects: Mobject,
|
||||
use_static_center_func: bool = False,
|
||||
center_func: Callable[[], np.ndarray] | None = None,
|
||||
):
|
||||
"""This method allows the mobject to have a fixed orientation,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, facing the camera, it
|
||||
will continue to face the camera even as the camera moves.
|
||||
Highly useful when adding labels to graphs and the like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*mobjects
|
||||
The mobject whose orientation must be fixed.
|
||||
use_static_center_func
|
||||
Whether or not to use the function that takes the mobject's
|
||||
center as centerpoint, by default False
|
||||
center_func
|
||||
The function which returns the centerpoint
|
||||
with respect to which the mobject will be oriented, by default None
|
||||
"""
|
||||
# This prevents the computation of mobject.get_center
|
||||
# every single time a projection happens
|
||||
def get_static_center_func(mobject):
|
||||
point = mobject.get_center()
|
||||
return lambda: point
|
||||
|
||||
for mobject in mobjects:
|
||||
if center_func:
|
||||
func = center_func
|
||||
elif use_static_center_func:
|
||||
func = get_static_center_func(mobject)
|
||||
else:
|
||||
func = mobject.get_center
|
||||
for submob in mobject.get_family():
|
||||
self.fixed_orientation_mobjects[submob] = func
|
||||
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
"""This method allows the mobject to have a fixed position,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, at the top of the frame, it
|
||||
will continue to be displayed at the top of the frame.
|
||||
|
||||
Highly useful when displaying Titles or formulae or the like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**mobjects
|
||||
The mobject to fix in frame.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
self.fixed_in_frame_mobjects.add(mobject)
|
||||
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject):
|
||||
"""If a mobject was fixed in its orientation by passing it through
|
||||
:meth:`.add_fixed_orientation_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer have a fixed orientation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobjects whose orientation need not be fixed any longer.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
if mobject in self.fixed_orientation_mobjects:
|
||||
del self.fixed_orientation_mobjects[mobject]
|
||||
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
"""If a mobject was fixed in frame by passing it through
|
||||
:meth:`.add_fixed_in_frame_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer be fixed in frame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobjects which need not be fixed in frame any longer.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
if mobject in self.fixed_in_frame_mobjects:
|
||||
self.fixed_in_frame_mobjects.remove(mobject)
|
||||
|
|
@ -768,7 +768,7 @@ class Mobject:
|
|||
|
||||
def get_image(self, camera=None):
|
||||
if camera is None:
|
||||
from ..camera.camera import Camera
|
||||
from ..camera.cairo_camera import CairoCamera as Camera
|
||||
|
||||
camera = Camera()
|
||||
camera.capture_mobject(self)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import numpy as np
|
|||
from manim.utils.hashing import get_hash_from_play_call
|
||||
|
||||
from .. import config, logger
|
||||
from ..camera.camera import Camera
|
||||
from ..camera.cairo_camera import CairoCamera as Camera
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..scene.scene_file_writer import SceneFileWriter
|
||||
from ..utils.exceptions import EndSceneEarlyException
|
||||
|
|
|
|||
|
|
@ -17,35 +17,17 @@ else:
|
|||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import OpenGL.GL as gl
|
||||
from PIL import Image
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
from manim import config, logger
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.utils.caching import handle_caching_play
|
||||
from manim.utils.color import BLACK, color_to_rgba
|
||||
from manim.utils.exceptions import EndSceneEarlyException
|
||||
|
||||
from ..constants import *
|
||||
from ..scene.scene_file_writer import SceneFileWriter
|
||||
from ..utils import opengl
|
||||
from ..utils.config_ops import _Data
|
||||
from ..utils.simple_functions import clip, fdiv
|
||||
from ..utils.space_ops import (
|
||||
angle_of_vector,
|
||||
normalize,
|
||||
quaternion_from_angle_axis,
|
||||
quaternion_mult,
|
||||
rotation_matrix_transpose,
|
||||
rotation_matrix_transpose_from_quaternion,
|
||||
)
|
||||
from .shader import Mesh, Shader
|
||||
from .vectorized_mobject_rendering import (
|
||||
render_opengl_vectorized_mobject_fill,
|
||||
render_opengl_vectorized_mobject_stroke,
|
||||
)
|
||||
from ..utils.simple_functions import fdiv
|
||||
from ..utils.space_ops import normalize
|
||||
|
||||
|
||||
class OpenGLCameraFrame(OpenGLMobject):
|
||||
|
|
@ -538,554 +520,3 @@ class OpenGLCamera:
|
|||
if tid_and_texture:
|
||||
tid_and_texture[1].release()
|
||||
return self
|
||||
|
||||
|
||||
class OpenGLCameraLegacy(OpenGLMobject):
|
||||
euler_angles = _Data()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frame_shape=None,
|
||||
center_point=None,
|
||||
# Theta, phi, gamma
|
||||
euler_angles=[0, 0, 0],
|
||||
focal_distance=2,
|
||||
light_source_position=[-10, 10, 10],
|
||||
orthographic=False,
|
||||
minimum_polar_angle=-PI / 2,
|
||||
maximum_polar_angle=PI / 2,
|
||||
model_matrix=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.use_z_index = True
|
||||
self.frame_rate = 60
|
||||
self.orthographic = orthographic
|
||||
self.minimum_polar_angle = minimum_polar_angle
|
||||
self.maximum_polar_angle = maximum_polar_angle
|
||||
if self.orthographic:
|
||||
self.projection_matrix = opengl.orthographic_projection_matrix()
|
||||
self.unformatted_projection_matrix = opengl.orthographic_projection_matrix(
|
||||
format=False,
|
||||
)
|
||||
else:
|
||||
self.projection_matrix = opengl.perspective_projection_matrix()
|
||||
self.unformatted_projection_matrix = opengl.perspective_projection_matrix(
|
||||
format=False,
|
||||
)
|
||||
|
||||
if frame_shape is None:
|
||||
self.frame_shape = (config["frame_width"], config["frame_height"])
|
||||
else:
|
||||
self.frame_shape = frame_shape
|
||||
|
||||
if center_point is None:
|
||||
self.center_point = ORIGIN
|
||||
else:
|
||||
self.center_point = center_point
|
||||
|
||||
if model_matrix is None:
|
||||
model_matrix = opengl.translation_matrix(0, 0, 11)
|
||||
|
||||
self.focal_distance = focal_distance
|
||||
|
||||
if light_source_position is None:
|
||||
self.light_source_position = [-10, 10, 10]
|
||||
else:
|
||||
self.light_source_position = light_source_position
|
||||
self.light_source = OpenGLPoint(self.light_source_position)
|
||||
|
||||
self.default_model_matrix = model_matrix
|
||||
super().__init__(model_matrix=model_matrix, should_render=False, **kwargs)
|
||||
|
||||
if euler_angles is None:
|
||||
euler_angles = [0, 0, 0]
|
||||
euler_angles = np.array(euler_angles, dtype=float)
|
||||
|
||||
self.euler_angles = euler_angles
|
||||
self.refresh_rotation_matrix()
|
||||
|
||||
def get_position(self):
|
||||
return self.model_matrix[:, 3][:3]
|
||||
|
||||
def set_position(self, position):
|
||||
self.model_matrix[:, 3][:3] = position
|
||||
return self
|
||||
|
||||
@cached_property
|
||||
def formatted_view_matrix(self):
|
||||
return opengl.matrix_to_shader_input(np.linalg.inv(self.model_matrix))
|
||||
|
||||
@cached_property
|
||||
def unformatted_view_matrix(self):
|
||||
return np.linalg.inv(self.model_matrix)
|
||||
|
||||
def init_points(self):
|
||||
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
|
||||
self.set_width(self.frame_shape[0], stretch=True)
|
||||
self.set_height(self.frame_shape[1], stretch=True)
|
||||
self.move_to(self.center_point)
|
||||
|
||||
def to_default_state(self):
|
||||
self.center()
|
||||
self.set_height(config["frame_height"])
|
||||
self.set_width(config["frame_width"])
|
||||
self.set_euler_angles(0, 0, 0)
|
||||
self.model_matrix = self.default_model_matrix
|
||||
return self
|
||||
|
||||
def refresh_rotation_matrix(self):
|
||||
# Rotate based on camera orientation
|
||||
theta, phi, gamma = self.euler_angles
|
||||
quat = quaternion_mult(
|
||||
quaternion_from_angle_axis(theta, OUT, axis_normalized=True),
|
||||
quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True),
|
||||
quaternion_from_angle_axis(gamma, OUT, axis_normalized=True),
|
||||
)
|
||||
self.inverse_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat)
|
||||
|
||||
def rotate(self, angle, axis=OUT, **kwargs):
|
||||
curr_rot_T = self.inverse_rotation_matrix
|
||||
added_rot_T = rotation_matrix_transpose(angle, axis)
|
||||
new_rot_T = np.dot(curr_rot_T, added_rot_T)
|
||||
Fz = new_rot_T[2]
|
||||
phi = np.arccos(Fz[2])
|
||||
theta = angle_of_vector(Fz[:2]) + PI / 2
|
||||
partial_rot_T = np.dot(
|
||||
rotation_matrix_transpose(phi, RIGHT),
|
||||
rotation_matrix_transpose(theta, OUT),
|
||||
)
|
||||
gamma = angle_of_vector(np.dot(partial_rot_T, new_rot_T.T)[:, 0])
|
||||
self.set_euler_angles(theta, phi, gamma)
|
||||
return self
|
||||
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None):
|
||||
if theta is not None:
|
||||
self.euler_angles[0] = theta
|
||||
if phi is not None:
|
||||
self.euler_angles[1] = phi
|
||||
if gamma is not None:
|
||||
self.euler_angles[2] = gamma
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def set_theta(self, theta):
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
def set_phi(self, phi):
|
||||
return self.set_euler_angles(phi=phi)
|
||||
|
||||
def set_gamma(self, gamma):
|
||||
return self.set_euler_angles(gamma=gamma)
|
||||
|
||||
def increment_theta(self, dtheta):
|
||||
self.euler_angles[0] += dtheta
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def increment_phi(self, dphi):
|
||||
phi = self.euler_angles[1]
|
||||
new_phi = clip(phi + dphi, -PI / 2, PI / 2)
|
||||
self.euler_angles[1] = new_phi
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def increment_gamma(self, dgamma):
|
||||
self.euler_angles[2] += dgamma
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def get_shape(self):
|
||||
return (self.get_width(), self.get_height())
|
||||
|
||||
def get_center(self):
|
||||
# Assumes first point is at the center
|
||||
return self.points[0]
|
||||
|
||||
def get_width(self):
|
||||
points = self.points
|
||||
return points[2, 0] - points[1, 0]
|
||||
|
||||
def get_height(self):
|
||||
points = self.points
|
||||
return points[4, 1] - points[3, 1]
|
||||
|
||||
def get_focal_distance(self):
|
||||
return self.focal_distance * self.get_height()
|
||||
|
||||
def interpolate(self, *args, **kwargs):
|
||||
super().interpolate(*args, **kwargs)
|
||||
self.refresh_rotation_matrix()
|
||||
|
||||
|
||||
points_per_curve = 3
|
||||
|
||||
|
||||
class OpenGLRenderer:
|
||||
def __init__(self, file_writer_class=SceneFileWriter, skip_animations=False):
|
||||
# Measured in pixel widths, used for vector graphics
|
||||
self.anti_alias_width = 1.5
|
||||
self._file_writer_class = file_writer_class
|
||||
|
||||
self._original_skipping_status = skip_animations
|
||||
self.skip_animations = skip_animations
|
||||
self.animation_start_time = 0
|
||||
self.animation_elapsed_time = 0
|
||||
self.time = 0
|
||||
self.animations_hashes = []
|
||||
self.num_plays = 0
|
||||
|
||||
self.camera = OpenGLCamera()
|
||||
self.pressed_keys = set()
|
||||
|
||||
# Initialize texture map.
|
||||
self.path_to_texture_id = {}
|
||||
|
||||
self.background_color = config["background_color"]
|
||||
|
||||
def init_scene(self, scene):
|
||||
self.partial_movie_files = []
|
||||
self.file_writer: Any = self._file_writer_class(
|
||||
self,
|
||||
scene.__class__.__name__,
|
||||
)
|
||||
self.scene = scene
|
||||
self.background_color = config["background_color"]
|
||||
if not hasattr(self, "window"):
|
||||
if self.should_create_window():
|
||||
from .opengl_renderer_window import Window
|
||||
|
||||
self.window = Window(self)
|
||||
self.context = self.window.ctx
|
||||
self.frame_buffer_object = self.context.detect_framebuffer()
|
||||
else:
|
||||
self.window = None
|
||||
try:
|
||||
self.context = moderngl.create_context(standalone=True)
|
||||
except Exception:
|
||||
self.context = moderngl.create_context(
|
||||
standalone=True,
|
||||
backend="egl",
|
||||
)
|
||||
self.frame_buffer_object = self.get_frame_buffer_object(self.context, 0)
|
||||
self.frame_buffer_object.use()
|
||||
self.context.enable(moderngl.BLEND)
|
||||
self.context.wireframe = config["enable_wireframe"]
|
||||
self.context.blend_func = (
|
||||
moderngl.SRC_ALPHA,
|
||||
moderngl.ONE_MINUS_SRC_ALPHA,
|
||||
moderngl.ONE,
|
||||
moderngl.ONE,
|
||||
)
|
||||
|
||||
def should_create_window(self):
|
||||
if config["force_window"]:
|
||||
logger.warning(
|
||||
"'--force_window' is enabled, this is intended for debugging purposes "
|
||||
"and may impact performance if used when outputting files",
|
||||
)
|
||||
return True
|
||||
return (
|
||||
config["preview"]
|
||||
and not config["save_last_frame"]
|
||||
and not config["format"]
|
||||
and not config["write_to_movie"]
|
||||
and not config["dry_run"]
|
||||
)
|
||||
|
||||
def get_pixel_shape(self):
|
||||
if hasattr(self, "frame_buffer_object"):
|
||||
return self.frame_buffer_object.viewport[2:4]
|
||||
else:
|
||||
return None
|
||||
|
||||
def refresh_perspective_uniforms(self, camera):
|
||||
pw, ph = self.get_pixel_shape()
|
||||
fw, fh = camera.get_shape()
|
||||
# TODO, this should probably be a mobject uniform, with
|
||||
# the camera taking care of the conversion factor
|
||||
anti_alias_width = self.anti_alias_width / (ph / fh)
|
||||
# Orient light
|
||||
rotation = camera.inverse_rotation_matrix
|
||||
light_pos = camera.light_source.get_location()
|
||||
light_pos = np.dot(rotation, light_pos)
|
||||
|
||||
self.perspective_uniforms = {
|
||||
"frame_shape": camera.get_shape(),
|
||||
"anti_alias_width": anti_alias_width,
|
||||
"camera_center": tuple(camera.get_center()),
|
||||
"camera_rotation": tuple(np.array(rotation).T.flatten()),
|
||||
"light_source_position": tuple(light_pos),
|
||||
"focal_distance": camera.get_focal_distance(),
|
||||
}
|
||||
|
||||
def render_mobject(self, mobject):
|
||||
if isinstance(mobject, OpenGLVMobject):
|
||||
if config["use_projection_fill_shaders"]:
|
||||
render_opengl_vectorized_mobject_fill(self, mobject)
|
||||
|
||||
if config["use_projection_stroke_shaders"]:
|
||||
render_opengl_vectorized_mobject_stroke(self, mobject)
|
||||
|
||||
shader_wrapper_list = mobject.get_shader_wrapper_list()
|
||||
|
||||
# Convert ShaderWrappers to Meshes.
|
||||
for shader_wrapper in shader_wrapper_list:
|
||||
shader = Shader(self.context, shader_wrapper.shader_folder)
|
||||
|
||||
# Set textures.
|
||||
for name, path in shader_wrapper.texture_paths.items():
|
||||
tid = self.get_texture_id(path)
|
||||
shader.shader_program[name].value = tid
|
||||
|
||||
# Set uniforms.
|
||||
for name, value in it.chain(
|
||||
shader_wrapper.uniforms.items(),
|
||||
self.perspective_uniforms.items(),
|
||||
):
|
||||
try:
|
||||
shader.set_uniform(name, value)
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
shader.set_uniform(
|
||||
"u_view_matrix", self.scene.camera.formatted_view_matrix
|
||||
)
|
||||
shader.set_uniform(
|
||||
"u_projection_matrix",
|
||||
self.scene.camera.projection_matrix,
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Set depth test.
|
||||
if shader_wrapper.depth_test:
|
||||
self.context.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.context.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
# Render.
|
||||
mesh = Mesh(
|
||||
shader,
|
||||
shader_wrapper.vert_data,
|
||||
indices=shader_wrapper.vert_indices,
|
||||
use_depth_test=shader_wrapper.depth_test,
|
||||
primitive=mobject.render_primitive,
|
||||
)
|
||||
mesh.set_uniforms(self)
|
||||
mesh.render()
|
||||
|
||||
def get_texture_id(self, path):
|
||||
if repr(path) not in self.path_to_texture_id:
|
||||
tid = len(self.path_to_texture_id)
|
||||
texture = self.context.texture(
|
||||
size=path.size,
|
||||
components=len(path.getbands()),
|
||||
data=path.tobytes(),
|
||||
)
|
||||
texture.repeat_x = False
|
||||
texture.repeat_y = False
|
||||
texture.filter = (moderngl.NEAREST, moderngl.NEAREST)
|
||||
texture.swizzle = "RRR1" if path.mode == "L" else "RGBA"
|
||||
texture.use(location=tid)
|
||||
self.path_to_texture_id[repr(path)] = tid
|
||||
|
||||
return self.path_to_texture_id[repr(path)]
|
||||
|
||||
def update_skipping_status(self):
|
||||
"""
|
||||
This method is used internally to check if the current
|
||||
animation needs to be skipped or not. It also checks if
|
||||
the number of animations that were played correspond to
|
||||
the number of animations that need to be played, and
|
||||
raises an EndSceneEarlyException if they don't correspond.
|
||||
"""
|
||||
# there is always at least one section -> no out of bounds here
|
||||
if self.file_writer.sections[-1].skip_animations:
|
||||
self.skip_animations = True
|
||||
if (
|
||||
config["from_animation_number"]
|
||||
and self.num_plays < config["from_animation_number"]
|
||||
):
|
||||
self.skip_animations = True
|
||||
if (
|
||||
config["upto_animation_number"]
|
||||
and self.num_plays > config["upto_animation_number"]
|
||||
):
|
||||
self.skip_animations = True
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
@handle_caching_play
|
||||
def play(self, scene, *args, **kwargs):
|
||||
# TODO: Handle data locking / unlocking.
|
||||
self.animation_start_time = time.time()
|
||||
self.file_writer.begin_animation(not self.skip_animations)
|
||||
|
||||
scene.compile_animation_data(*args, **kwargs)
|
||||
scene.begin_animations()
|
||||
if scene.is_current_animation_frozen_frame():
|
||||
self.update_frame(scene)
|
||||
|
||||
if not self.skip_animations:
|
||||
for _ in range(int(config.frame_rate * scene.duration)):
|
||||
self.file_writer.write_frame(self)
|
||||
|
||||
if self.window is not None:
|
||||
self.window.swap_buffers()
|
||||
while time.time() - self.animation_start_time < scene.duration:
|
||||
pass
|
||||
self.animation_elapsed_time = scene.duration
|
||||
|
||||
else:
|
||||
scene.play_internal()
|
||||
|
||||
self.file_writer.end_animation(not self.skip_animations)
|
||||
self.time += scene.duration
|
||||
self.num_plays += 1
|
||||
|
||||
def clear_screen(self):
|
||||
self.frame_buffer_object.clear(*self.background_color)
|
||||
self.window.swap_buffers()
|
||||
|
||||
def render(self, scene, frame_offset, moving_mobjects):
|
||||
self.update_frame(scene)
|
||||
|
||||
if self.skip_animations:
|
||||
return
|
||||
|
||||
self.file_writer.write_frame(self)
|
||||
|
||||
if self.window is not None:
|
||||
self.window.swap_buffers()
|
||||
while self.animation_elapsed_time < frame_offset:
|
||||
self.update_frame(scene)
|
||||
self.window.swap_buffers()
|
||||
|
||||
def update_frame(self, scene):
|
||||
self.frame_buffer_object.clear(*self.background_color)
|
||||
self.refresh_perspective_uniforms(scene.camera)
|
||||
|
||||
for mobject in scene.mobjects:
|
||||
self.render_mobject(mobject)
|
||||
|
||||
for obj in scene.meshes:
|
||||
for mesh in obj.get_meshes():
|
||||
mesh.set_uniforms(self)
|
||||
mesh.render()
|
||||
|
||||
self.animation_elapsed_time = time.time() - self.animation_start_time
|
||||
|
||||
def scene_finished(self, scene):
|
||||
# When num_plays is 0, no images have been output, so output a single
|
||||
# image in this case
|
||||
if self.num_plays > 0:
|
||||
self.file_writer.finish()
|
||||
elif self.num_plays == 0 and config.write_to_movie:
|
||||
config.write_to_movie = False
|
||||
|
||||
if self.should_save_last_frame():
|
||||
config.save_last_frame = True
|
||||
self.update_frame(scene)
|
||||
self.file_writer.save_final_image(self.get_image())
|
||||
|
||||
def should_save_last_frame(self):
|
||||
if config["save_last_frame"]:
|
||||
return True
|
||||
if self.scene.interactive_mode:
|
||||
return False
|
||||
return self.num_plays == 0
|
||||
|
||||
def get_image(self) -> Image.Image:
|
||||
"""Returns an image from the current frame. The first argument passed to image represents
|
||||
the mode RGB with the alpha channel A. The data we read is from the currently bound frame
|
||||
buffer. We pass in 'raw' as the name of the decoder, 0 and -1 args are specifically
|
||||
used for the decoder tand represent the stride and orientation. 0 means there is no
|
||||
padding expected between bytes and -1 represents the orientation and means the first
|
||||
line of the image is the bottom line on the screen.
|
||||
|
||||
Returns
|
||||
-------
|
||||
PIL.Image
|
||||
The PIL image of the array.
|
||||
"""
|
||||
raw_buffer_data = self.get_raw_frame_buffer_object_data()
|
||||
image = Image.frombytes(
|
||||
"RGBA",
|
||||
self.get_pixel_shape(),
|
||||
raw_buffer_data,
|
||||
"raw",
|
||||
"RGBA",
|
||||
0,
|
||||
-1,
|
||||
)
|
||||
return image
|
||||
|
||||
def save_static_frame_data(self, scene, static_mobjects):
|
||||
pass
|
||||
|
||||
def get_frame_buffer_object(self, context, samples=0):
|
||||
pixel_width = config["pixel_width"]
|
||||
pixel_height = config["pixel_height"]
|
||||
num_channels = 4
|
||||
return context.framebuffer(
|
||||
color_attachments=context.texture(
|
||||
(pixel_width, pixel_height),
|
||||
components=num_channels,
|
||||
samples=samples,
|
||||
),
|
||||
depth_attachment=context.depth_renderbuffer(
|
||||
(pixel_width, pixel_height),
|
||||
samples=samples,
|
||||
),
|
||||
)
|
||||
|
||||
def get_raw_frame_buffer_object_data(self, dtype="f1"):
|
||||
# Copy blocks from the fbo_msaa to the drawn fbo using Blit
|
||||
# pw, ph = self.get_pixel_shape()
|
||||
# gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo)
|
||||
# gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.fbo.glo)
|
||||
# gl.glBlitFramebuffer(
|
||||
# 0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
|
||||
# )
|
||||
num_channels = 4
|
||||
ret = self.frame_buffer_object.read(
|
||||
viewport=self.frame_buffer_object.viewport,
|
||||
components=num_channels,
|
||||
dtype=dtype,
|
||||
)
|
||||
return ret
|
||||
|
||||
def get_frame(self):
|
||||
# get current pixel values as numpy data in order to test output
|
||||
raw = self.get_raw_frame_buffer_object_data(dtype="f1")
|
||||
pixel_shape = self.get_pixel_shape()
|
||||
result_dimensions = (pixel_shape[1], pixel_shape[0], 4)
|
||||
np_buf = np.frombuffer(raw, dtype="uint8").reshape(result_dimensions)
|
||||
np_buf = np.flipud(np_buf)
|
||||
return np_buf
|
||||
|
||||
# Returns offset from the bottom left corner in pixels.
|
||||
# top_left flag should be set to True when using a GUI framework
|
||||
# where the (0,0) is at the top left: e.g. PySide6
|
||||
def pixel_coords_to_space_coords(self, px, py, relative=False, top_left=False):
|
||||
pixel_shape = self.get_pixel_shape()
|
||||
if pixel_shape is None:
|
||||
return np.array([0, 0, 0])
|
||||
pw, ph = pixel_shape
|
||||
fw, fh = config["frame_width"], config["frame_height"]
|
||||
fc = self.camera.get_center()
|
||||
if relative:
|
||||
return 2 * np.array([px / pw, py / ph, 0])
|
||||
else:
|
||||
# Only scale wrt one axis
|
||||
scale = fh / ph
|
||||
return fc + scale * np.array(
|
||||
[(px - pw / 2), (-1 if top_left else 1) * (py - ph / 2), 0]
|
||||
)
|
||||
|
||||
@property
|
||||
def background_color(self):
|
||||
return self._background_color
|
||||
|
||||
@background_color.setter
|
||||
def background_color(self, value):
|
||||
self._background_color = color_to_rgba(value, 1.0)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from typing import Any
|
|||
import numpy as np
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from manim.camera.camera import Camera
|
||||
from manim.camera.cairo_camera import CairoCamera as Camera
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
||||
from .. import config, logger
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue