3b1b-manim/manimlib/camera/camera.py

247 lines
8.1 KiB
Python
Raw Normal View History

2022-03-22 11:31:52 -07:00
from __future__ import annotations
2022-04-12 19:19:59 +08:00
import moderngl
import numpy as np
2022-04-12 19:19:59 +08:00
import OpenGL.GL as gl
from PIL import Image
2023-01-25 10:19:44 -08:00
from manimlib.camera.camera_frame import CameraFrame
2022-04-12 19:19:59 +08:00
from manimlib.constants import BLACK
2022-05-14 17:47:31 -07:00
from manimlib.constants import DEFAULT_FPS
2022-04-12 19:19:59 +08:00
from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH
2023-01-25 10:19:44 -08:00
from manimlib.constants import FRAME_WIDTH
from manimlib.mobject.mobject import Mobject
2020-06-02 16:18:44 -07:00
from manimlib.mobject.mobject import Point
2022-04-12 19:19:59 +08:00
from manimlib.utils.color import color_to_rgba
from typing import TYPE_CHECKING
if TYPE_CHECKING:
2023-01-27 10:12:53 -08:00
from typing import Optional
from manimlib.typing import ManimColor, Vect3
from manimlib.window import Window
2022-12-14 16:41:19 -08:00
class Camera(object):
2022-12-14 16:41:19 -08:00
def __init__(
self,
2023-01-27 10:12:53 -08:00
window: Optional[Window] = None,
background_image: Optional[str] = None,
2022-12-27 14:53:55 -08:00
frame_config: dict = dict(),
2022-12-14 16:41:19 -08:00
pixel_width: int = DEFAULT_PIXEL_WIDTH,
pixel_height: int = DEFAULT_PIXEL_HEIGHT,
fps: int = DEFAULT_FPS,
# Note: frame height and width will be resized to match the pixel aspect ratio
background_color: ManimColor = BLACK,
background_opacity: float = 1.0,
# Points in vectorized mobjects with norm greater
# than this value will be rescaled.
2022-12-14 16:41:19 -08:00
max_allowable_norm: float = FRAME_WIDTH,
image_mode: str = "RGBA",
n_channels: int = 4,
pixel_array_dtype: type = np.uint8,
2022-12-27 14:53:55 -08:00
light_source_position: Vect3 = np.array([-10, 10, 10]),
2020-06-08 14:09:31 -07:00
# Although vector graphics handle antialiasing fine
# without multisampling, for 3d scenes one might want
# to set samples to be greater than 0.
2022-12-14 16:41:19 -08:00
samples: int = 0,
):
self.background_image = background_image
self.window = window
self.default_pixel_shape = (pixel_width, pixel_height)
2022-12-14 16:41:19 -08:00
self.fps = fps
self.max_allowable_norm = max_allowable_norm
self.image_mode = image_mode
self.n_channels = n_channels
self.pixel_array_dtype = pixel_array_dtype
self.light_source_position = light_source_position
self.samples = samples
self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max
2022-04-12 19:19:59 +08:00
self.background_rgba: list[float] = list(color_to_rgba(
2022-12-14 16:41:19 -08:00
background_color, background_opacity
2022-04-12 19:19:59 +08:00
))
self.uniforms = dict()
2022-12-14 16:41:19 -08:00
self.init_frame(**frame_config)
2023-01-27 10:12:53 -08:00
self.init_context()
self.init_fbo()
self.init_light_source()
2022-12-14 16:41:19 -08:00
def init_frame(self, **config) -> None:
self.frame = CameraFrame(**config)
2023-01-27 10:12:53 -08:00
def init_context(self) -> None:
if self.window is None:
2023-01-28 15:40:58 -08:00
self.ctx: moderngl.Context = moderngl.create_standalone_context()
2020-02-11 19:51:19 -08:00
else:
2023-01-28 15:40:58 -08:00
self.ctx: moderngl.Context = self.window.ctx
2020-06-08 14:09:31 -07:00
2023-01-18 15:36:00 -08:00
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
self.ctx.enable(moderngl.BLEND)
2023-01-18 15:36:00 -08:00
2023-01-27 10:12:53 -08:00
def init_fbo(self) -> None:
# This is the buffer used when writing to a video/image file
self.fbo_for_files = self.get_fbo(self.samples)
# This is the frame buffer we'll draw into when emitting frames
2023-01-24 12:04:43 -08:00
self.draw_fbo = self.get_fbo(samples=0)
2020-02-11 19:51:19 -08:00
2023-01-27 10:12:53 -08:00
if self.window is None:
self.window_fbo = None
self.fbo = self.fbo_for_files
else:
self.window_fbo = self.ctx.detect_framebuffer()
self.fbo = self.window_fbo
self.fbo.use()
def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position)
def use_window_fbo(self, use: bool = True):
assert(self.window is not None)
if use:
self.fbo = self.window_fbo
else:
self.fbo = self.fbo_for_files
# Methods associated with the frame buffer
def get_fbo(
self,
samples: int = 0
) -> moderngl.Framebuffer:
2023-01-24 12:04:43 -08:00
return self.ctx.framebuffer(
color_attachments=self.ctx.texture(
self.default_pixel_shape,
2020-06-08 14:09:31 -07:00
components=self.n_channels,
samples=samples,
),
2023-01-24 12:04:43 -08:00
depth_attachment=self.ctx.depth_renderbuffer(
self.default_pixel_shape,
2020-06-08 14:09:31 -07:00
samples=samples
)
2020-02-13 10:49:43 -08:00
)
def clear(self) -> None:
2020-06-08 14:09:31 -07:00
self.fbo.clear(*self.background_rgba)
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
# Copy blocks from fbo into draw_fbo using Blit
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo)
src_viewport = self.fbo.viewport
gl.glBlitFramebuffer(
*src_viewport,
*self.draw_fbo.viewport,
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
)
return self.draw_fbo.read(
viewport=self.draw_fbo.viewport,
components=self.n_channels,
dtype=dtype,
)
def get_image(self) -> Image.Image:
return Image.frombytes(
'RGBA',
self.get_pixel_shape(),
self.get_raw_fbo_data(),
'raw', 'RGBA', 0, -1
2017-09-26 17:41:45 -07:00
)
def get_pixel_array(self) -> np.ndarray:
raw = self.get_raw_fbo_data(dtype='f4')
flat_arr = np.frombuffer(raw, dtype='f4')
2023-01-24 12:04:43 -08:00
arr = flat_arr.reshape([*reversed(self.draw_fbo.size), self.n_channels])
arr = arr[::-1]
# Convert from float
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
# Needed?
def get_texture(self) -> moderngl.Texture:
texture = self.ctx.texture(
size=self.fbo.size,
components=4,
data=self.get_raw_fbo_data(),
dtype='f4'
)
return texture
# Getting camera attributes
2023-01-24 12:04:43 -08:00
def get_pixel_size(self) -> float:
return self.frame.get_shape()[0] / self.get_pixel_shape()[0]
def get_pixel_shape(self) -> tuple[int, int]:
return self.draw_fbo.size
def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]
def get_pixel_height(self) -> int:
return self.get_pixel_shape()[1]
2023-01-24 12:04:43 -08:00
def get_aspect_ratio(self):
pw, ph = self.get_pixel_shape()
return pw / ph
def get_frame_height(self) -> float:
return self.frame.get_height()
2018-03-09 10:32:19 -08:00
def get_frame_width(self) -> float:
return self.frame.get_width()
def get_frame_shape(self) -> tuple[float, float]:
2020-02-11 19:51:19 -08:00
return (self.get_frame_width(), self.get_frame_height())
def get_frame_center(self) -> np.ndarray:
return self.frame.get_center()
2016-11-23 17:50:25 -08:00
def get_location(self) -> tuple[float, float, float]:
2021-11-08 21:46:35 -08:00
return self.frame.get_implied_camera_location()
def resize_frame_shape(self, fixed_dimension: bool = False) -> None:
2020-06-08 14:09:31 -07:00
"""
Changes frame_shape to match the aspect ratio
of the pixels, where fixed_dimension determines
whether frame_height or frame_width
remains fixed while the other changes accordingly.
"""
frame_height = self.get_frame_height()
frame_width = self.get_frame_width()
2023-01-24 12:04:43 -08:00
aspect_ratio = self.get_aspect_ratio()
if not fixed_dimension:
2020-06-08 14:09:31 -07:00
frame_height = frame_width / aspect_ratio
else:
frame_width = aspect_ratio * frame_height
2023-01-24 12:04:43 -08:00
self.frame.set_height(frame_height, stretch=true)
self.frame.set_width(frame_width, stretch=true)
2020-06-08 14:09:31 -07:00
# Rendering
2022-12-27 14:53:55 -08:00
def capture(self, *mobjects: Mobject) -> None:
2023-01-26 20:38:38 -08:00
self.clear()
self.refresh_uniforms()
self.fbo.use()
for mobject in mobjects:
mobject.render(self.ctx, self.uniforms)
def refresh_uniforms(self) -> None:
frame = self.frame
2023-01-23 14:41:17 -08:00
view_matrix = frame.get_view_matrix()
2023-01-16 19:34:20 -08:00
light_pos = self.light_source.get_location()
2023-01-18 13:44:41 -08:00
cam_pos = self.frame.get_implied_camera_location()
self.uniforms.update(
2023-01-23 17:10:18 -08:00
frame_shape=frame.get_shape(),
pixel_size=self.get_pixel_size(),
2023-01-23 14:41:17 -08:00
view=tuple(view_matrix.T.flatten()),
camera_position=tuple(cam_pos),
light_position=tuple(light_pos),
focal_distance=frame.get_focal_distance(),
)
2020-06-04 15:41:20 -07:00
# Mostly just defined so old scenes don't break
2020-06-04 15:41:20 -07:00
class ThreeDCamera(Camera):
def __init__(self, samples: int = 4, **kwargs):
super().__init__(samples=samples, **kwargs)