Merge pull request #1977 from 3b1b/video-work

Various clean ups associated with 3d scenes
This commit is contained in:
Grant Sanderson 2023-01-30 16:27:53 -08:00 committed by GitHub
commit 5490b3be19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 267 additions and 346 deletions

View file

@ -554,9 +554,7 @@ class TexAndNumbersExample(Scene):
)
class SurfaceExample(Scene):
samples = 4
class SurfaceExample(ThreeDScene):
def construct(self):
surface_text = Text("For 3d scenes, try using surfaces")
surface_text.fix_in_frame()
@ -588,13 +586,6 @@ class SurfaceExample(Scene):
mob.mesh = SurfaceMesh(mob)
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
# Set perspective
frame = self.camera.frame
frame.set_euler_angles(
theta=-30 * DEGREES,
phi=70 * DEGREES,
)
surface = surfaces[0]
self.play(
@ -616,12 +607,12 @@ class SurfaceExample(Scene):
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
frame.animate.increment_phi(-10 * DEGREES),
frame.animate.increment_theta(-20 * DEGREES),
self.frame.animate.increment_phi(-10 * DEGREES),
self.frame.animate.increment_theta(-20 * DEGREES),
run_time=3
)
# Add ambient rotation
frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
self.frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
# Play around with where the light is
light_text = Text("You can move around the light source")

View file

@ -136,7 +136,6 @@ class DrawBorderThenFill(Animation):
if index == 1 and self.sm_to_index[hash(submob)] == 0:
# First time crossing over
submob.set_data(outline.data)
submob.needs_new_triangulation = False
self.sm_to_index[hash(submob)] = 1
if index == 0:

View file

@ -72,9 +72,9 @@ class Camera(object):
def init_context(self) -> None:
if self.window is None:
self.ctx = moderngl.create_standalone_context()
self.ctx: moderngl.Context = moderngl.create_standalone_context()
else:
self.ctx = self.window.ctx
self.ctx: moderngl.Context = self.window.ctx
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
self.ctx.enable(moderngl.BLEND)
@ -169,10 +169,10 @@ class Camera(object):
# Getting camera attributes
def get_pixel_size(self) -> float:
return self.frame.get_shape()[0] / self.get_pixel_shape()[0]
return self.frame.get_width() / self.get_pixel_shape()[0]
def get_pixel_shape(self) -> tuple[int, int]:
return self.draw_fbo.size
return self.fbo.size
def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]
@ -231,12 +231,12 @@ class Camera(object):
cam_pos = self.frame.get_implied_camera_location()
self.uniforms.update(
frame_shape=frame.get_shape(),
pixel_size=self.get_pixel_size(),
view=tuple(view_matrix.T.flatten()),
focal_distance=frame.get_focal_distance() / frame.get_scale(),
frame_scale=frame.get_scale(),
pixel_size=self.get_pixel_size(),
camera_position=tuple(cam_pos),
light_position=tuple(light_pos),
focal_distance=frame.get_focal_distance(),
)

View file

@ -4,9 +4,10 @@ import math
import numpy as np
from scipy.spatial.transform import Rotation
from pyrr import Matrix44
from manimlib.constants import DEGREES, RADIANS
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import FRAME_SHAPE
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.mobject.mobject import Mobject
from manimlib.utils.space_ops import normalize
@ -20,28 +21,23 @@ if TYPE_CHECKING:
class CameraFrame(Mobject):
def __init__(
self,
frame_shape: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT),
frame_shape: tuple[float, float] = FRAME_SHAPE,
center_point: Vect3 = ORIGIN,
focal_dist_to_height: float = 2.0,
# Field of view in the y direction
fovy: float = 45 * DEGREES,
**kwargs,
):
self.frame_shape = frame_shape
self.center_point = center_point
self.focal_dist_to_height = focal_dist_to_height
self.view_matrix = np.identity(4)
super().__init__(**kwargs)
def init_uniforms(self) -> None:
super().init_uniforms()
# As a quaternion
self.uniforms["orientation"] = Rotation.identity().as_quat()
self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height
self.view_matrix = np.identity(4)
self.default_orientation = Rotation.identity()
def init_points(self) -> None:
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)
self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
self.set_width(frame_shape[0], stretch=True)
self.set_height(frame_shape[1], stretch=True)
self.move_to(center_point)
self.uniforms["orientation"] = Rotation.identity().as_quat()
self.uniforms["fovy"] = fovy
def set_orientation(self, rotation: Rotation):
self.uniforms["orientation"][:] = rotation.as_quat()
@ -50,15 +46,21 @@ class CameraFrame(Mobject):
def get_orientation(self):
return Rotation.from_quat(self.uniforms["orientation"])
def to_default_state(self):
self.center()
self.set_height(FRAME_HEIGHT)
self.set_width(FRAME_WIDTH)
self.set_orientation(Rotation.identity())
def make_orientation_default(self):
self.default_orientation = self.get_orientation()
return self
def get_euler_angles(self):
return self.get_orientation().as_euler("zxz")[::-1]
def to_default_state(self):
self.set_shape(*FRAME_SHAPE)
self.center()
self.set_orientation(self.default_orientation)
return self
def get_euler_angles(self) -> np.ndarray:
orientation = self.get_orientation()
if all(orientation.as_quat() == [0, 0, 0, 1]):
return np.zeros(3)
return orientation.as_euler("zxz")[::-1]
def get_theta(self):
return self.get_euler_angles()[0]
@ -69,6 +71,9 @@ class CameraFrame(Mobject):
def get_gamma(self):
return self.get_euler_angles()[2]
def get_scale(self):
return self.get_height() / FRAME_SHAPE[1]
def get_inverse_camera_rotation_matrix(self):
return self.get_orientation().as_matrix().T
@ -77,13 +82,14 @@ class CameraFrame(Mobject):
Returns a 4x4 for the affine transformation mapping a point
into the camera's internal coordinate system
"""
result = self.view_matrix
result[:] = np.identity(4)
result[:3, 3] = -self.get_center()
rotation = np.identity(4)
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
result[:] = np.dot(rotation, result)
return result
shift = Matrix44.from_translation(-self.get_center()).T
rotation = Matrix44.from_quaternion(self.uniforms["orientation"]).T
scale = Matrix44(np.identity(3) / self.get_scale())
self.view_matrix[:] = shift * rotation * scale
return self.view_matrix
def get_inv_view_matrix(self):
return np.linalg.inv(self.get_view_matrix())
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
rot = Rotation.from_rotvec(angle * normalize(axis))
@ -101,7 +107,11 @@ class CameraFrame(Mobject):
for i, var in enumerate([theta, phi, gamma]):
if var is not None:
eulers[i] = var * units
self.set_orientation(Rotation.from_euler("zxz", eulers[::-1]))
if all(eulers == 0):
rot = Rotation.identity()
else:
rot = Rotation.from_euler("zxz", eulers[::-1])
self.set_orientation(rot)
return self
def reorient(
@ -139,16 +149,20 @@ class CameraFrame(Mobject):
return self
def set_focal_distance(self, focal_distance: float):
self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height()
self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
return self
def set_field_of_view(self, field_of_view: float):
self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2)
self.uniforms["fovy"] = field_of_view
return self
def get_shape(self):
return (self.get_width(), self.get_height())
def get_aspect_ratio(self):
width, height = self.get_shape()
return width / height
def get_center(self) -> np.ndarray:
# Assumes first point is at the center
return self.get_points()[0]
@ -162,12 +176,22 @@ class CameraFrame(Mobject):
return points[4, 1] - points[3, 1]
def get_focal_distance(self) -> float:
return self.uniforms["focal_dist_to_height"] * self.get_height()
return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"])
def get_field_of_view(self) -> float:
return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2)
return self.uniforms["fovy"]
def get_implied_camera_location(self) -> np.ndarray:
to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance()
return self.get_center() + dist * to_camera
def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
view = self.get_view_matrix()
point4d = [*point, 0 if relative else 1]
return np.dot(point4d, view.T)[:3]
def from_fixed_frame_point(self, point: Vect3, relative: bool = False):
inv_view = self.get_inv_view_matrix()
point4d = [*point, 0 if relative else 1]
return np.dot(point4d, inv_view.T)[:3]

View file

@ -11,6 +11,7 @@ if TYPE_CHECKING:
ASPECT_RATIO: float = 16.0 / 9.0
FRAME_HEIGHT: float = 8.0
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
FRAME_X_RADIUS: float = FRAME_WIDTH / 2

View file

@ -9,7 +9,6 @@ import itertools as it
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
from manimlib.constants import DEGREES, PI
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
from manimlib.mobject.functions import ParametricCurve

View file

@ -448,12 +448,8 @@ class Annulus(VMobject):
)
self.radius = outer_radius
# Make sure to add enough components that triangulation doesn't fail
kw = dict(
n_components=int(max(8, np.ceil(TAU / math.acos(inner_radius / outer_radius))))
)
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0, **kw)
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0, **kw)
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0)
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0)
self.add_subpath(outer_path)
self.add_subpath(inner_path)
self.shift(center)

View file

@ -48,7 +48,7 @@ from manimlib.utils.space_ops import rotation_matrix_transpose
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Union, Tuple
from typing import Callable, Iterable, Union, Tuple, Optional
import numpy.typing as npt
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict
from moderngl.context import Context
@ -408,8 +408,13 @@ class Mobject(object):
self.assemble_family()
return self
def remove(self, *to_remove: Mobject, reassemble: bool = True):
for parent in self.get_family():
def remove(
self,
*to_remove: Mobject,
reassemble: bool = True,
recurse: bool = True
):
for parent in self.get_family(recurse):
for child in to_remove:
if child in parent.submobjects:
parent.submobjects.remove(child)
@ -419,6 +424,9 @@ class Mobject(object):
parent.assemble_family()
return self
def clear(self):
self.remove(*self.submobjects, recurse=False)
def add_to_back(self, *mobjects: Mobject):
self.set_submobjects(list_update(mobjects, self.submobjects))
return self
@ -1160,6 +1168,21 @@ class Mobject(object):
self.set_depth(min_depth, **kwargs)
return self
def set_shape(
self,
width: Optional[float] = None,
height: Optional[float] = None,
depth: Optional[float] = None,
**kwargs
):
if width is not None:
self.set_width(width, stretch=True, **kwargs)
if height is not None:
self.set_height(height, stretch=True, **kwargs)
if depth is not None:
self.set_depth(depth, stretch=True, **kwargs)
return self
def set_coord(self, value: float, dim: int, direction: Vect3 = ORIGIN):
curr = self.get_coord(dim, direction)
shift_vect = np.zeros(self.dim)
@ -1801,35 +1824,41 @@ class Mobject(object):
def affects_shader_info_id(func: Callable):
@wraps(func)
def wrapper(self):
for mob in self.get_family():
func(mob)
mob.refresh_shader_wrapper_id()
return self
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
self.refresh_shader_wrapper_id()
return result
return wrapper
@affects_shader_info_id
def fix_in_frame(self, recurse: bool = True):
def set_uniform(self, recurse: bool = True, **new_uniforms):
for mob in self.get_family(recurse):
mob.uniforms["is_fixed_in_frame"] = 1.0
mob.uniforms.update(new_uniforms)
return self
@affects_shader_info_id
def unfix_from_frame(self):
self.uniforms["is_fixed_in_frame"] = 0.0
def fix_in_frame(self, recurse: bool = True):
self.set_uniform(recurse, is_fixed_in_frame=1.0)
return self
@affects_shader_info_id
def unfix_from_frame(self, recurse: bool = True):
self.set_uniform(recurse, is_fixed_in_frame=0.0)
return self
def is_fixed_in_frame(self) -> bool:
return bool(self.uniforms["is_fixed_in_frame"])
@affects_shader_info_id
def apply_depth_test(self):
self.depth_test = True
def apply_depth_test(self, recurse: bool = True):
for mob in self.get_family(recurse):
mob.depth_test = True
return self
@affects_shader_info_id
def deactivate_depth_test(self):
self.depth_test = False
def deactivate_depth_test(self, recurse: bool = True):
for mob in self.get_family(recurse):
mob.depth_test = False
return self
# Shader code manipulation

View file

@ -383,7 +383,6 @@ class Bubble(SVGMobject):
self.flip()
self.content = Mobject()
self.refresh_triangulation()
def get_tip(self):
# TODO, find a better way

View file

@ -30,10 +30,7 @@ class SingleStringTex(SVGMobject):
fill_opacity: float = 1.0,
stroke_width: float = 0,
svg_default: dict = dict(fill_color=WHITE),
path_string_config: dict = dict(
should_subdivide_sharp_curves=True,
should_remove_null_curves=True,
),
path_string_config: dict = dict(),
font_size: int = 48,
alignment: str = R"\centering",
math_mode: bool = True,

View file

@ -295,15 +295,11 @@ class VMobjectFromSVGPath(VMobject):
def __init__(
self,
path_obj: se.Path,
should_subdivide_sharp_curves: bool = False,
should_remove_null_curves: bool = True,
**kwargs
):
# Get rid of arcs
path_obj.approximate_arcs_with_quads()
self.path_obj = path_obj
self.should_subdivide_sharp_curves = should_subdivide_sharp_curves
self.should_remove_null_curves = should_remove_null_curves
super().__init__(**kwargs)
def init_points(self) -> None:
@ -313,14 +309,6 @@ class VMobjectFromSVGPath(VMobject):
path_string = self.path_obj.d()
if path_string not in PATH_TO_POINTS:
self.handle_commands()
if self.should_subdivide_sharp_curves:
# For a healthy triangulation later
self.subdivide_sharp_curves()
if self.should_remove_null_curves:
# Get rid of any null curves
self.set_points(self.get_points_without_null_curves())
# So triangulation doesn't get messed up
self.subdivide_intersections()
# Save for future use
PATH_TO_POINTS[path_string] = self.get_points().copy()
else:

View file

@ -144,24 +144,14 @@ class VMobject(Mobject):
def __getitem__(self, value: int | slice) -> VMobject:
return super().__getitem__(value)
def __iter__(self) -> Iterable[VMobject]:
return super().__iter__()
def add(self, *vmobjects: VMobject):
if not all((isinstance(m, VMobject) for m in vmobjects)):
raise Exception("All submobjects must be of type VMobject")
super().add(*vmobjects)
def add_background_rectangle(
self,
color: ManimColor | None = None,
opacity: float = 0.75,
**kwargs
):
normal = self.family_members_with_points()[0].get_unit_normal()
super().add_background_rectangle(color, opacity, **kwargs)
rect = self.background_rectangle
if np.dot(rect.get_unit_normal(), normal) < 0:
rect.reverse_points()
return self
# Colors
def init_colors(self):
self.set_fill(
@ -329,6 +319,10 @@ class VMobject(Mobject):
self.set_stroke(opacity=opacity, recurse=recurse)
return self
def set_anti_alias_width(self, anti_alias_width: float, recurse: bool = True):
self.set_uniform(recurse, anti_alias_width=anti_alias_width)
return self
def fade(self, darkness: float = 0.5, recurse: bool = True):
mobs = self.get_family() if recurse else [self]
for mob in mobs:
@ -399,6 +393,9 @@ class VMobject(Mobject):
return self.get_fill_color()
return self.get_stroke_color()
def get_anti_alias_width(self):
return self.uniforms["anti_alias_width"]
def has_stroke(self) -> bool:
return any(self.data['stroke_width']) and any(self.data['stroke_rgba'][:, 3])
@ -426,10 +423,34 @@ class VMobject(Mobject):
def get_joint_type(self) -> float:
return self.uniforms["joint_type"]
def apply_depth_test(
self,
anti_alias_width: float = 0,
fill_border_width: float = 0,
recurse: bool=True
):
super().apply_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
self.set_fill(border_width=fill_border_width)
return self
def deactivate_depth_test(
self,
anti_alias_width: float = 1.0,
fill_border_width: float = 0.5,
recurse: bool=True
):
super().apply_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
self.set_fill(border_width=fill_border_width)
return self
@Mobject.affects_family_data
def use_winding_fill(self, value: bool = True, recurse: bool = True):
for submob in self.get_family(recurse):
submob._use_winding_fill = value
if not value and submob.has_points():
submob.subdivide_intersections()
return self
# Points
@ -870,8 +891,11 @@ class VMobject(Mobject):
# If both have fill, and they have the same shape, just
# give them the same triangulation so that it's not recalculated
# needlessly throughout an animation
if not self._use_winding_fill and self.has_fill() \
and vmobject.has_fill() and self.has_same_shape_as(vmobject):
match_tris = not self._use_winding_fill and \
self.has_fill() and \
vmobject.has_fill() and \
self.has_same_shape_as(vmobject)
if match_tris:
vmobject.triangulation = self.triangulation
return self
@ -892,8 +916,7 @@ class VMobject(Mobject):
def get_nth_subpath(path_list, n):
if n >= len(path_list):
# Create a null path at the very end
return [path_list[-1][-1]] * 3
return [path_list[-1][-1]]
return path_list[n]
for n in range(n_subpaths):
@ -1277,23 +1300,6 @@ class VMobject(Mobject):
submob.get_joint_products()
has_fill = submob.has_fill()
has_stroke = submob.has_stroke()
if has_fill:
data = submob.data[fill_names]
data["base_point"][:] = data["point"][0]
fill_datas.append(data)
if self._use_winding_fill:
# Add dummy
fill_datas.append(data[-1:])
else:
fill_indices.append(submob.get_triangulation())
# Add fill border
if not has_stroke:
names = list(stroke_names)
names[names.index('stroke_rgba')] = 'fill_rgba'
names[names.index('stroke_width')] = 'fill_border_width'
border_stroke_data = submob.data[names]
fill_border_datas.append(border_stroke_data)
fill_border_datas.append(border_stroke_data[-1:])
if has_stroke:
lst = back_stroke_datas if submob.stroke_behind else stroke_datas
lst.append(submob.data[stroke_names])
@ -1301,6 +1307,24 @@ class VMobject(Mobject):
# with a dummy vertex added at the end. This is to ensure
# it can be safely stacked onto other stroke data arrays.
lst.append(submob.data[stroke_names][-1:])
if has_fill:
data = submob.data[fill_names]
data["base_point"][:] = data["point"][0]
fill_datas.append(data)
if self._use_winding_fill:
# Add dummy, as above
fill_datas.append(data[-1:])
else:
fill_indices.append(submob.get_triangulation())
if not has_stroke and has_fill:
# Add fill border
names = list(stroke_names)
names[names.index('stroke_rgba')] = 'fill_rgba'
names[names.index('stroke_width')] = 'fill_border_width'
border_stroke_data = submob.data[names]
fill_border_datas.append(border_stroke_data)
fill_border_datas.append(border_stroke_data[-1:])
shader_wrappers = [
self.back_stroke_shader_wrapper.read_in(

View file

@ -126,7 +126,7 @@ class InteractiveScene(Scene):
def update_selection_rectangle(self, rect: Rectangle):
p1 = rect.fixed_corner
p2 = self.mouse_point.get_center()
p2 = self.frame.to_fixed_frame_point(self.mouse_point.get_center())
rect.set_points_as_corners([
p1, np.array([p2[0], p1[1], 0]),
p2, np.array([p1[0], p2[1], 0]),
@ -377,7 +377,9 @@ class InteractiveScene(Scene):
def enable_selection(self):
self.is_selecting = True
self.add(self.selection_rectangle)
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
self.selection_rectangle.fixed_corner = self.frame.to_fixed_frame_point(
self.mouse_point.get_center()
)
def gather_new_selection(self):
self.is_selecting = False
@ -568,7 +570,8 @@ class InteractiveScene(Scene):
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
super().on_mouse_motion(point, d_point)
self.crosshair.move_to(point)
ff_point = self.frame.to_fixed_frame_point(point)
self.crosshair.move_to(ff_point)
if self.is_grabbing:
self.handle_grabbing(point)
elif self.window.is_key_pressed(ord(RESIZE_KEY)):

View file

@ -1,144 +0,0 @@
from manimlib.animation.animation import Animation
from manimlib.animation.transform import MoveToTarget
from manimlib.animation.transform import Transform
from manimlib.animation.update import UpdateFromFunc
from manimlib.constants import DOWN, RIGHT
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
from manimlib.mobject.probability import SampleSpace
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.scene.scene import Scene
class SampleSpaceScene(Scene):
def get_sample_space(self, **config):
self.sample_space = SampleSpace(**config)
return self.sample_space
def add_sample_space(self, **config):
self.add(self.get_sample_space(**config))
def get_division_change_animations(
self, sample_space, parts, p_list,
dimension=1,
new_label_kwargs=None,
**kwargs
):
if new_label_kwargs is None:
new_label_kwargs = {}
anims = []
p_list = sample_space.complete_p_list(p_list)
space_copy = sample_space.copy()
vect = DOWN if dimension == 1 else RIGHT
parts.generate_target()
for part, p in zip(parts.target, p_list):
part.replace(space_copy, stretch=True)
part.stretch(p, dimension)
parts.target.arrange(vect, buff=0)
parts.target.move_to(space_copy)
anims.append(MoveToTarget(parts))
if hasattr(parts, "labels") and parts.labels is not None:
label_kwargs = parts.label_kwargs
label_kwargs.update(new_label_kwargs)
new_braces, new_labels = sample_space.get_subdivision_braces_and_labels(
parts.target, **label_kwargs
)
anims += [
Transform(parts.braces, new_braces),
Transform(parts.labels, new_labels),
]
return anims
def get_horizontal_division_change_animations(self, p_list, **kwargs):
assert(hasattr(self.sample_space, "horizontal_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.horizontal_parts, p_list,
dimension=1,
**kwargs
)
def get_vertical_division_change_animations(self, p_list, **kwargs):
assert(hasattr(self.sample_space, "vertical_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.vertical_parts, p_list,
dimension=0,
**kwargs
)
def get_conditional_change_anims(
self, sub_sample_space_index, value, post_rects=None,
**kwargs
):
parts = self.sample_space.horizontal_parts
sub_sample_space = parts[sub_sample_space_index]
anims = self.get_division_change_animations(
sub_sample_space, sub_sample_space.vertical_parts, value,
dimension=0,
**kwargs
)
if post_rects is not None:
anims += self.get_posterior_rectangle_change_anims(post_rects)
return anims
def get_top_conditional_change_anims(self, *args, **kwargs):
return self.get_conditional_change_anims(0, *args, **kwargs)
def get_bottom_conditional_change_anims(self, *args, **kwargs):
return self.get_conditional_change_anims(1, *args, **kwargs)
def get_prior_rectangles(self):
return VGroup(*[
self.sample_space.horizontal_parts[i].vertical_parts[0]
for i in range(2)
])
def get_posterior_rectangles(self, buff=MED_LARGE_BUFF):
prior_rects = self.get_prior_rectangles()
areas = [
rect.get_width() * rect.get_height()
for rect in prior_rects
]
total_area = sum(areas)
total_height = prior_rects.get_height()
post_rects = prior_rects.copy()
for rect, area in zip(post_rects, areas):
rect.stretch_to_fit_height(total_height * area / total_area)
rect.stretch_to_fit_width(
area / rect.get_height()
)
post_rects.arrange(DOWN, buff=0)
post_rects.next_to(
self.sample_space, RIGHT, buff
)
return post_rects
def get_posterior_rectangle_braces_and_labels(
self, post_rects, labels, direction=RIGHT, **kwargs
):
return self.sample_space.get_subdivision_braces_and_labels(
post_rects, labels, direction, **kwargs
)
def update_posterior_braces(self, post_rects):
braces = post_rects.braces
labels = post_rects.labels
for rect, brace, label in zip(post_rects, braces, labels):
brace.stretch_to_fit_height(rect.get_height())
brace.next_to(rect, RIGHT, SMALL_BUFF)
label.next_to(brace, RIGHT, SMALL_BUFF)
def get_posterior_rectangle_change_anims(self, post_rects):
def update_rects(rects):
new_rects = self.get_posterior_rectangles()
Transform(rects, new_rects).update(1)
if hasattr(rects, "braces"):
self.update_posterior_braces(rects)
return rects
anims = [UpdateFromFunc(post_rects, update_rects)]
if hasattr(post_rects, "braces"):
anims += list(map(Animation, [
post_rects.labels, post_rects.braces
]))
return anims

View file

@ -18,12 +18,14 @@ from tqdm.auto import tqdm as ProgressDisplay
from manimlib.animation.animation import prepare_animation
from manimlib.animation.fading import VFadeInThenOut
from manimlib.camera.camera import Camera
from manimlib.camera.camera_frame import CameraFrame
from manimlib.config import get_module
from manimlib.constants import ARROW_SYMBOLS
from manimlib.constants import DEFAULT_WAIT_TIME
from manimlib.constants import COMMAND_MODIFIER
from manimlib.constants import SHIFT_MODIFIER
from manimlib.constants import RED
from manimlib.constants import FRAME_HEIGHT
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_type import EventType
from manimlib.logger import log
@ -42,6 +44,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable
from manimlib.typing import Vect3
from PIL.Image import Image
@ -50,19 +53,21 @@ if TYPE_CHECKING:
PAN_3D_KEY = 'd'
FRAME_SHIFT_KEY = 'f'
ZOOM_KEY = 'z'
RESET_FRAME_KEY = 'r'
QUIT_KEY = 'q'
class Scene(object):
random_seed: int = 0
pan_sensitivity: float = 3.0
pan_sensitivity: float = 0.5
scroll_sensitivity: float = 20
max_num_saved_states: int = 50
default_camera_config: dict = dict()
default_window_config: dict = dict()
default_file_writer_config: dict = dict()
samples = 0
# Euler angles, in degrees
default_frame_orientation = (0, 0)
def __init__(
self,
@ -108,6 +113,10 @@ class Scene(object):
# Core state of the scene
self.camera: Camera = Camera(**self.camera_config)
self.frame: CameraFrame = self.camera.frame
self.frame.reorient(*self.default_frame_orientation)
self.frame.make_orientation_default()
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
self.mobjects: list[Mobject] = [self.camera.frame]
self.id_to_mobject_map: dict[int, Mobject] = dict()
@ -793,9 +802,10 @@ class Scene(object):
def on_mouse_motion(
self,
point: np.ndarray,
d_point: np.ndarray
point: Vect3,
d_point: Vect3
) -> None:
assert(self.window is not None)
self.mouse_point.move_to(point)
event_data = {"point": point, "d_point": d_point}
@ -806,25 +816,23 @@ class Scene(object):
frame = self.camera.frame
# Handle perspective changes
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
frame.increment_theta(-self.pan_sensitivity * d_point[0])
frame.increment_phi(self.pan_sensitivity * d_point[1])
ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
ff_d_point *= self.pan_sensitivity
frame.increment_theta(-ff_d_point[0])
frame.increment_phi(ff_d_point[1])
# Handle frame movements
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
shift = -d_point
shift[0] *= frame.get_width() / 2
shift[1] *= frame.get_height() / 2
transform = frame.get_inverse_camera_rotation_matrix()
shift = np.dot(np.transpose(transform), shift)
frame.shift(shift)
frame.shift(-d_point)
def on_mouse_drag(
self,
point: np.ndarray,
d_point: np.ndarray,
point: Vect3,
d_point: Vect3,
buttons: int,
modifiers: int
) -> None:
self.mouse_drag_point.move_to(point)
self.frame.shift(-d_point)
event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data)
@ -833,7 +841,7 @@ class Scene(object):
def on_mouse_press(
self,
point: np.ndarray,
point: Vect3,
button: int,
mods: int
) -> None:
@ -845,7 +853,7 @@ class Scene(object):
def on_mouse_release(
self,
point: np.ndarray,
point: Vect3,
button: int,
mods: int
) -> None:
@ -856,22 +864,21 @@ class Scene(object):
def on_mouse_scroll(
self,
point: np.ndarray,
offset: np.ndarray
point: Vect3,
offset: Vect3,
x_pixel_offset: float,
y_pixel_offset: float
) -> None:
event_data = {"point": point, "offset": offset}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
frame = self.camera.frame
if self.window.is_key_pressed(ord(ZOOM_KEY)):
factor = 1 + np.arctan(10 * offset[1])
frame.scale(1 / factor, about_point=point)
else:
transform = frame.get_inverse_camera_rotation_matrix()
shift = np.dot(np.transpose(transform), offset)
frame.shift(-20.0 * shift)
rel_offset = y_pixel_offset / self.camera.get_pixel_height()
self.frame.scale(
1 - self.scroll_sensitivity * rel_offset,
about_point=point
)
def on_key_release(
self,
@ -971,3 +978,14 @@ class SceneState():
class EndScene(Exception):
pass
class ThreeDScene(Scene):
samples = 4
default_frame_orientation = (-30, 70)
def add(self, *mobjects, set_depth_test: bool = True):
for mob in mobjects:
if set_depth_test and not mob.is_fixed_in_frame():
mob.apply_depth_test()
super().add(*mobjects)

View file

@ -248,8 +248,8 @@ class ShaderWrapper(object):
def generate_vao(self, refresh: bool = True):
self.release()
# Data buffer
vbo = self.vbo = self.get_vertex_buffer_object(refresh)
ibo = self.ibo = self.get_index_buffer_object(refresh)
vbo = self.get_vertex_buffer_object(refresh)
ibo = self.get_index_buffer_object(refresh)
# Vertex array object
self.vao = self.ctx.vertex_array(

View file

@ -24,14 +24,11 @@ vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
vec3 to_camera = normalize(camera_position - point);
vec3 to_light = normalize(light_position - point);
// Note, this effectively treats surfaces as two-sided
// if(dot(to_camera, unit_normal) < 0) unit_normal *= -1;
float light_to_normal = dot(to_light, unit_normal);
// When unit normal points towards light, brighten
float bright_factor = max(light_to_normal, 0) * reflectiveness;
// For glossy surface, add extra shine if light beam go towards camera
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
vec3 light_reflection = reflect(-to_light, unit_normal);
float light_to_cam = dot(light_reflection, to_camera);
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
bright_factor += shine;

View file

@ -1,20 +1,17 @@
uniform float is_fixed_in_frame;
uniform mat4 view;
uniform vec2 frame_shape;
uniform float focal_distance;
const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0);
const float DEFAULT_FRAME_HEIGHT = 8.0;
const float DEFAULT_FRAME_WIDTH = DEFAULT_FRAME_HEIGHT * 16.0 / 9.0;
vec4 get_gl_Position(vec3 point){
bool is_fixed = bool(is_fixed_in_frame);
vec4 result = vec4(point, 1.0);
if(!is_fixed){
if(!bool(is_fixed_in_frame)){
result = view * result;
}
vec2 shape = is_fixed ? DEFAULT_FRAME_SHAPE : frame_shape;
result.x *= 2.0 / shape.x;
result.y *= 2.0 / shape.y;
result.x *= 2.0 / DEFAULT_FRAME_WIDTH;
result.y *= 2.0 / DEFAULT_FRAME_HEIGHT;
result.z /= focal_distance;
result.w = 1.0 - result.z;
// Flip and scale to prevent premature clipping

View file

@ -15,8 +15,6 @@ uniform vec3 color6;
uniform vec3 color7;
uniform vec3 color8;
uniform vec2 frame_shape;
in vec3 xyz_coords;
out vec4 frag_color;

View file

@ -26,8 +26,6 @@ uniform float saturation_factor;
uniform float black_for_cycles;
uniform float is_parameter_space;
uniform vec2 frame_shape;
in vec3 xyz_coords;
out vec4 frag_color;

View file

@ -26,7 +26,7 @@ float dist_to_curve(){
// Evaluate F(x, y) = y - x^2
// divide by its gradient's magnitude
float Fxy = y0 - x0 * x0;
float approx_dist = abs(Fxy) / sqrt(1.0 + 4 * x0 * x0);
float approx_dist = abs(Fxy) * inversesqrt(1.0 + 4 * x0 * x0);
if(approx_dist < QUICK_DIST_WIDTH) return approx_dist;
// Otherwise, solve for the minimal distance.

View file

@ -1,6 +1,7 @@
#version 330
uniform vec2 frame_shape;
uniform float frame_scale;
uniform float is_fixed_in_frame;
in vec3 point;
in vec4 stroke_rgba;
@ -20,7 +21,10 @@ const float STROKE_WIDTH_CONVERSION = 0.01;
void main(){
verts = point;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width;
if(!bool(is_fixed_in_frame)){
v_stroke_width *= frame_scale;
}
v_joint_product = joint_product;
v_color = stroke_rgba;
v_vert_index = gl_VertexID;

View file

@ -7,8 +7,8 @@ import moderngl
from PIL import Image
import numpy as np
from manimlib.constants import DEFAULT_PIXEL_HEIGHT
from manimlib.constants import DEFAULT_PIXEL_WIDTH
from manimlib.config import parse_cli
from manimlib.config import get_configuration
from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_shader_dir
from manimlib.utils.file_ops import find_file
@ -103,7 +103,7 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str:
@lru_cache()
def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]:
def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]:
"""
Because VMobjects with fill are rendered in a funny way, using
alpha blending to effectively compute the winding number around
@ -114,15 +114,14 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
which can display that texture as a simple quad onto a screen,
along with the rgb value which is meant to be discarded.
"""
cam_config = get_customization()['camera_resolutions']
res_name = cam_config['default_resolution']
size = tuple(map(int, cam_config[res_name].split("x")))
cam_config = get_configuration(parse_cli())['camera_config']
size = (cam_config['pixel_width'], cam_config['pixel_height'])
# Important to make sure dtype is floating point (not fixed point)
# so that alpha values can be negative and are not clipped
texture = ctx.texture(size=size, components=4, dtype='f2')
depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used
texture_fbo = ctx.framebuffer(texture, depth_buffer)
depth_texture = ctx.depth_texture(size=size)
texture_fbo = ctx.framebuffer(texture, depth_texture)
# We'll paint onto a canvas with initially negative rgbs, and
# discard any pixels remaining close to this value. This is
@ -150,6 +149,7 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
#version 330
uniform sampler2D Texture;
uniform sampler2D DepthTexture;
uniform vec3 null_rgb;
in vec2 v_textcoord;
@ -163,13 +163,16 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
// Un-blend from the null value
color.rgb -= (1 - color.a) * null_rgb;
// Counteract scaling in fill frag
color.a *= 1.01;
//TODO, set gl_FragDepth;
gl_FragDepth = texture(DepthTexture, v_textcoord)[0];
}
''',
)
simple_program['Texture'].value = get_texture_id(texture)
simple_program['DepthTexture'].value = get_texture_id(depth_texture)
simple_program['null_rgb'].value = null_rgb
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])

View file

@ -7,6 +7,7 @@ from moderngl_window.context.pyglet.window import Window as PygletWindow
from moderngl_window.timers.clock import Timer
from screeninfo import get_monitors
from manimlib.constants import FRAME_SHAPE
from manimlib.utils.customization import get_customization
from typing import TYPE_CHECKING
@ -77,17 +78,15 @@ class Window(PygletWindow):
py: int,
relative: bool = False
) -> np.ndarray:
pw, ph = self.size
fw, fh = self.scene.camera.get_frame_shape()
fc = self.scene.camera.get_frame_center()
if relative:
return np.array([px / pw, py / ph, 0])
else:
return np.array([
fc[0] + px * fw / pw - fw / 2,
fc[1] + py * fh / ph - fh / 2,
0
])
pixel_shape = np.array(self.size)
fixed_frame_shape = np.array(FRAME_SHAPE)
frame = self.scene.frame
coords = np.zeros(3)
coords[:2] = (fixed_frame_shape / pixel_shape) * np.array([px, py])
if not relative:
coords[:2] -= 0.5 * fixed_frame_shape
return frame.from_fixed_frame_point(coords, relative)
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
super().on_mouse_motion(x, y, dx, dy)
@ -115,7 +114,7 @@ class Window(PygletWindow):
super().on_mouse_scroll(x, y, x_offset, y_offset)
point = self.pixel_coords_to_space_coords(x, y)
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
self.scene.on_mouse_scroll(point, offset)
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
def on_key_press(self, symbol: int, modifiers: int) -> None:
self.pressed_keys.add(symbol) # Modifiers?

View file

@ -13,6 +13,7 @@ pydub
pygments
PyOpenGL
pyperclip
pyrr
pyyaml
rich
scipy