Merge pull request #3 from 3b1b/shaders

Update
This commit is contained in:
鹤翔万里 2021-01-16 08:46:48 +08:00 committed by GitHub
commit 83a9217063
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 273 additions and 231 deletions

View file

@ -27,6 +27,7 @@ tex:
# intermediate_filetype: "xdv"
universal_import_line: "from manimlib.imports import *"
style:
font: "Consolas"
background_color: "#333333"
camera_qualities:
low:

View file

@ -310,22 +310,33 @@ class SurfaceExample(Scene):
}
def construct(self):
surface_text = Text("For 3d scenes, try using surfaces")
surface_text.fix_in_frame()
surface_text.to_edge(UP)
self.add(surface_text)
self.wait(0.1)
torus1 = Torus(r1=1, r2=1)
torus2 = Torus(r1=3, r2=1)
sphere = Sphere(radius=3, resolution=torus1.resolution)
# You can texture a surface with up to two images, which will
# be interpreted as the side towards the light, and away from
# the light. These can be either urls, or paths to a local file
# in whatever you've set as the iamge directory in
# in whatever you've set as the image directory in
# the custom_defaults.yml file
# day_texture = "EarthTextureMap"
# night_texture = "NightEarthTextureMap"
day_texture = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Whole_world_-_land_and_oceans.jpg/1280px-Whole_world_-_land_and_oceans.jpg"
night_texture = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/The_earth_at_night.jpg/1280px-The_earth_at_night.jpg"
surfaces = [
TexturedSurface(surface, day_texture, night_texture)
for surface in [sphere, torus1, torus2]
]
for mob in surfaces:
mob.shift(IN)
mob.mesh = SurfaceMesh(mob)
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
@ -365,12 +376,23 @@ class SurfaceExample(Scene):
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")
light_text.move_to(surface_text)
light_text.fix_in_frame()
self.play(FadeTransform(surface_text, light_text))
light = self.camera.light_source
self.add(light)
light.save_state()
self.play(light.move_to, 3 * IN, run_time=5)
self.play(light.shift, 10 * OUT, run_time=5)
self.wait(4)
drag_text = Text("Try clicking and dragging while pressing d")
drag_text.move_to(light_text)
drag_text.fix_in_frame()
self.play(FadeTransform(light_text, drag_text))
self.wait()
# See https://github.com/3b1b/videos for many, many more

View file

@ -95,7 +95,7 @@ class CameraFrame(Mobject):
return self.set_rotation(phi=phi)
def set_gamma(self, gamma):
return self.set_rotation(phi=phi)
return self.set_rotation(gamma=gamma)
def increment_theta(self, dtheta):
return self.set_rotation(theta=self.euler_angles[0] + dtheta)

View file

@ -77,6 +77,7 @@ from manimlib.scene.vector_space_scene import *
from manimlib.utils.bezier import *
from manimlib.utils.color import *
from manimlib.utils.config_ops import *
from manimlib.utils.customization import *
from manimlib.utils.debug import *
from manimlib.utils.directories import *
from manimlib.utils.images import *

View file

@ -1,7 +1,5 @@
from functools import reduce
import copy
import itertools as it
import operator as op
import random
import sys
import moderngl
@ -31,9 +29,6 @@ from manimlib.shader_wrapper import ShaderWrapper
from manimlib.shader_wrapper import get_colormap_code
# TODO: Explain array_attrs
# TODO: Incorporate shader defaults
class Mobject(object):
"""
Mathematical Object
@ -146,9 +141,12 @@ class Mobject(object):
about_point = self.get_bounding_box_point(about_edge)
for mob in self.get_family():
arrs = [mob.get_points()]
arrs = []
if mob.has_points():
arrs.append(mob.get_points())
if works_on_bounding_box:
arrs.append(mob.get_bounding_box())
for arr in arrs:
if about_point is None:
arr[:] = func(arr)
@ -298,6 +296,84 @@ class Mobject(object):
self.set_submobjects(list_update(self.submobjects, mobject_attrs))
return self
# Submobject organization
def arrange(self, direction=RIGHT, center=True, **kwargs):
for m1, m2 in zip(self.submobjects, self.submobjects[1:]):
m2.next_to(m1, direction, **kwargs)
if center:
self.center()
return self
def arrange_in_grid(self, n_rows=None, n_cols=None,
buff=None,
h_buff=None,
v_buff=None,
buff_ratio=None,
h_buff_ratio=0.5,
v_buff_ratio=0.5,
aligned_edge=ORIGIN,
fill_rows_first=True):
submobs = self.submobjects
if n_rows is None and n_cols is None:
n_rows = int(np.sqrt(len(submobs)))
if n_rows is None:
n_rows = len(submobs) // n_cols
if n_cols is None:
n_cols = len(submobs) // n_rows
if buff is not None:
h_buff = buff
v_buff = buff
else:
if buff_ratio is not None:
v_buff_ratio = buff_ratio
h_buff_ratio = buff_ratio
if h_buff is None:
h_buff = h_buff_ratio * self[0].get_width()
if v_buff is None:
v_buff = v_buff_ratio * self[0].get_height()
x_unit = h_buff + max([sm.get_width() for sm in submobs])
y_unit = v_buff + max([sm.get_height() for sm in submobs])
for index, sm in enumerate(submobs):
if fill_rows_first:
x, y = index % n_cols, index // n_cols
else:
x, y = index // n_rows, index % n_rows
sm.move_to(ORIGIN, aligned_edge)
sm.shift(x * x_unit * RIGHT + y * y_unit * DOWN)
self.center()
return self
def get_grid(self, n_rows, n_cols, height=None, **kwargs):
"""
Returns a new mobject containing multiple copies of this one
arranged in a grid
"""
grid = self.get_group_class()(
*(self.copy() for n in range(n_rows * n_cols))
)
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
if height is not None:
grid.set_height(height)
return grid
def sort(self, point_to_num_func=lambda p: p[0], submob_func=None):
if submob_func is not None:
self.submobjects.sort(key=submob_func)
else:
self.submobjects.sort(key=lambda m: point_to_num_func(m.get_center()))
return self
def shuffle(self, recurse=False):
if recurse:
for submob in self.submobjects:
submob.shuffle(recurse=True)
random.shuffle(self.submobjects)
return self
# Copying
def copy(self):
@ -373,16 +449,16 @@ class Mobject(object):
self.has_updaters = False
self.updating_suspended = False
def update(self, dt=0, recursive=True):
def update(self, dt=0, recurse=True):
if not self.has_updaters or self.updating_suspended:
return self
for updater in self.time_based_updaters:
updater(self, dt)
for updater in self.non_time_updaters:
updater(self)
if recursive:
if recurse:
for submob in self.submobjects:
submob.update(dt, recursive)
submob.update(dt, recurse)
return self
def get_time_based_updaters(self):
@ -419,13 +495,13 @@ class Mobject(object):
updater_list.remove(update_function)
return self
def clear_updaters(self, recursive=True):
def clear_updaters(self, recurse=True):
self.time_based_updaters = []
self.non_time_updaters = []
if recursive:
if recurse:
for submob in self.submobjects:
submob.clear_updaters()
self.suspend_updating(recursive)
self.suspend_updating(recurse)
return self
def match_updaters(self, mobject):
@ -434,22 +510,22 @@ class Mobject(object):
self.add_updater(updater)
return self
def suspend_updating(self, recursive=True):
def suspend_updating(self, recurse=True):
self.updating_suspended = True
if recursive:
if recurse:
for submob in self.submobjects:
submob.suspend_updating(recursive)
submob.suspend_updating(recurse)
return self
def resume_updating(self, recursive=True, call_updater=True):
def resume_updating(self, recurse=True, call_updater=True):
self.updating_suspended = False
if recursive:
if recurse:
for submob in self.submobjects:
submob.resume_updating(recursive)
submob.resume_updating(recurse)
for parent in self.parents:
parent.resume_updating(recursive=False, call_updater=False)
parent.resume_updating(recurse=False, call_updater=False)
if call_updater:
self.update(dt=0, recursive=recursive)
self.update(dt=0, recurse=recurse)
return self
def refresh_has_updater_status(self):
@ -744,30 +820,6 @@ class Mobject(object):
self.shift(start - curr_start)
return self
# Background rectangle
def add_background_rectangle(self, color=BLACK, opacity=0.75, **kwargs):
# TODO, this does not behave well when the mobject has points,
# since it gets displayed on top
from manimlib.mobject.shape_matchers import BackgroundRectangle
self.background_rectangle = BackgroundRectangle(
self, color=color,
fill_opacity=opacity,
**kwargs
)
self.add_to_back(self.background_rectangle)
return self
def add_background_rectangle_to_submobjects(self, **kwargs):
for submobject in self.submobjects:
submobject.add_background_rectangle(**kwargs)
return self
def add_background_rectangle_to_family_members_with_points(self, **kwargs):
for mob in self.family_members_with_points():
mob.add_background_rectangle(**kwargs)
return self
# Color functions
def set_rgba_array(self, color=None, opacity=None, name="rgbas", recurse=True):
@ -857,6 +909,30 @@ class Mobject(object):
mob.uniforms["shadow"] = shadow
return self
# Background rectangle
def add_background_rectangle(self, color=None, opacity=0.75, **kwargs):
# TODO, this does not behave well when the mobject has points,
# since it gets displayed on top
from manimlib.mobject.shape_matchers import BackgroundRectangle
self.background_rectangle = BackgroundRectangle(
self, color=color,
fill_opacity=opacity,
**kwargs
)
self.add_to_back(self.background_rectangle)
return self
def add_background_rectangle_to_submobjects(self, **kwargs):
for submobject in self.submobjects:
submobject.add_background_rectangle(**kwargs)
return self
def add_background_rectangle_to_family_members_with_points(self, **kwargs):
for mob in self.family_members_with_points():
mob.add_background_rectangle(**kwargs)
return self
# Getters
def get_bounding_box_point(self, direction):
@ -1037,84 +1113,6 @@ class Mobject(object):
def get_group_class(self):
return Group
# Submobject organization
def arrange(self, direction=RIGHT, center=True, **kwargs):
for m1, m2 in zip(self.submobjects, self.submobjects[1:]):
m2.next_to(m1, direction, **kwargs)
if center:
self.center()
return self
def arrange_in_grid(self, n_rows=None, n_cols=None,
buff=None,
h_buff=None,
v_buff=None,
buff_ratio=None,
h_buff_ratio=0.5,
v_buff_ratio=0.5,
aligned_edge=ORIGIN,
fill_rows_first=True):
submobs = self.submobjects
if n_rows is None and n_cols is None:
n_rows = int(np.sqrt(len(submobs)))
if n_rows is None:
n_rows = len(submobs) // n_cols
if n_cols is None:
n_cols = len(submobs) // n_rows
if buff is not None:
h_buff = buff
v_buff = buff
else:
if buff_ratio is not None:
v_buff_ratio = buff_ratio
h_buff_ratio = buff_ratio
if h_buff is None:
h_buff = h_buff_ratio * self[0].get_width()
if v_buff is None:
v_buff = v_buff_ratio * self[0].get_height()
x_unit = h_buff + max([sm.get_width() for sm in submobs])
y_unit = v_buff + max([sm.get_height() for sm in submobs])
for index, sm in enumerate(submobs):
if fill_rows_first:
x, y = index % n_cols, index // n_cols
else:
x, y = index // n_rows, index % n_rows
sm.move_to(ORIGIN, aligned_edge)
sm.shift(x * x_unit * RIGHT + y * y_unit * DOWN)
self.center()
return self
def get_grid(self, n_rows, n_cols, height=None, **kwargs):
"""
Returns a new mobject containing multiple copies of this one
arranged in a grid
"""
grid = self.get_group_class()(
*(self.copy() for n in range(n_rows * n_cols))
)
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
if height is not None:
grid.set_height(height)
return grid
def sort(self, point_to_num_func=lambda p: p[0], submob_func=None):
if submob_func is not None:
self.submobjects.sort(key=submob_func)
else:
self.submobjects.sort(key=lambda m: point_to_num_func(m.get_center()))
return self
def shuffle(self, recurse=False):
if recurse:
for submob in self.submobjects:
submob.shuffle(recurse=True)
random.shuffle(self.submobjects)
return self
# Alignment
def align_data_and_family(self, mobject):

View file

@ -4,6 +4,7 @@ from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.color import Color
from manimlib.utils.customization import get_customization
from manimlib.utils.config_ops import digest_config
@ -23,15 +24,16 @@ class SurroundingRectangle(Rectangle):
class BackgroundRectangle(SurroundingRectangle):
CONFIG = {
"color": BLACK,
"stroke_width": 0,
"stroke_opacity": 0,
"fill_opacity": 0.75,
"buff": 0
}
def __init__(self, mobject, **kwargs):
SurroundingRectangle.__init__(self, mobject, **kwargs)
def __init__(self, mobject, color=None, **kwargs):
if color is None:
color = get_customization()['style']['background_color']
SurroundingRectangle.__init__(self, mobject, color=color, **kwargs)
self.original_fill_opacity = self.fill_opacity
def pointwise_become_partial(self, mobject, a, b):

View file

@ -6,6 +6,7 @@ import cairo
from manimlib.constants import *
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.utils.config_ops import digest_config
from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_text_dir
@ -135,7 +136,7 @@ class Text(SVGMobject):
settings = self.font + self.slant + self.weight
settings += str(self.t2f) + str(self.t2s) + str(self.t2w)
settings += str(self.lsh) + str(self.size)
id_str = self.text+settings
id_str = self.text + settings
hasher = hashlib.sha256()
hasher.update(id_str.encode())
return hasher.hexdigest()[:16]
@ -192,11 +193,11 @@ class Text(SVGMobject):
lsh = self.lsh * 10
if self.font == '':
print(NOT_SETTING_FONT_MSG)
self.font = get_customization()['style']['font']
dir_name = get_text_dir()
hash_name = self.text2hash()
file_name = os.path.join(dir_name, hash_name) +'.svg'
file_name = os.path.join(dir_name, hash_name) + '.svg'
if os.path.exists(file_name):
return file_name

View file

@ -90,13 +90,6 @@ class VMobject(Mobject):
"unit_normal": np.zeros((1, 3))
})
def set_points(self, points):
old_points = self.get_points()
super().set_points(points)
if not np.all(points == old_points):
self.refresh_triangulation()
return self
# Colors
def init_colors(self):
self.set_fill(
@ -458,19 +451,6 @@ class VMobject(Mobject):
self.resize_data(len(self.get_points() - 1))
self.append_points(new_points)
# TODO, how to be smart about tangents here?
def apply_function(self, function):
Mobject.apply_function(self, function)
if self.make_smooth_after_applying_functions:
self.make_smooth()
self.refresh_triangulation()
return self
def flip(self, *args, **kwargs):
super().flip(*args, **kwargs)
self.refresh_unit_normal()
self.refresh_triangulation()
#
def consider_points_equals(self, p0, p1):
return get_norm(p1 - p0) < self.tolerance_for_point_equality
@ -761,6 +741,98 @@ class VMobject(Mobject):
vmob.pointwise_become_partial(self, a, b)
return vmob
# Related to triangulation
def refresh_triangulation(self):
for mob in self.get_family():
mob.needs_new_triangulation = True
return self
def get_triangulation(self, normal_vector=None):
# Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader.
# First triangles come directly from the points
if normal_vector is None:
normal_vector = self.get_unit_normal()
if not self.needs_new_triangulation:
return self.triangulation
points = self.get_points()
if len(points) <= 1:
self.triangulation = np.zeros(0, dtype='i4')
self.needs_new_triangulation = False
return self.triangulation
# Rotate points such that unit normal vector is OUT
# TODO, 99% of the time this does nothing. Do a check for that?
points = np.dot(points, z_to_vector(normal_vector))
indices = np.arange(len(points), dtype=int)
b0s = points[0::3]
b1s = points[1::3]
b2s = points[2::3]
v01s = b1s - b0s
v12s = b2s - b1s
crosses = cross2d(v01s, v12s)
convexities = np.sign(crosses)
atol = self.tolerance_for_point_equality
end_of_loop = np.zeros(len(b0s), dtype=bool)
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
end_of_loop[-1] = True
concave_parts = convexities < 0
# These are the vertices to which we'll apply a polygon triangulation
inner_vert_indices = np.hstack([
indices[0::3],
indices[1::3][concave_parts],
indices[2::3][end_of_loop],
])
inner_vert_indices.sort()
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
tri_indices = np.hstack([indices, inner_tri_indices])
self.triangulation = tri_indices
self.needs_new_triangulation = False
return tri_indices
def triggers_refreshed_triangulation(func):
def wrapper(self, *args, **kwargs):
old_points = self.get_points()
func(self, *args, **kwargs)
if not np.all(self.get_points() == old_points):
self.refresh_triangulation()
self.refresh_unit_normal()
return wrapper
@triggers_refreshed_triangulation
def set_points(self, points):
super().set_points(points)
@triggers_refreshed_triangulation
def set_data(self, data):
super().set_data(data)
# TODO, how to be smart about tangents here?
@triggers_refreshed_triangulation
def apply_function(self, function):
super().apply_function(function)
if self.make_smooth_after_applying_functions:
self.make_smooth()
return self
@triggers_refreshed_triangulation
def flip(self, *args, **kwargs):
super().flip(*args, **kwargs)
# For shaders
def init_shader_data(self):
self.fill_data = np.zeros(0, dtype=self.fill_dtype)
@ -849,67 +921,6 @@ class VMobject(Mobject):
return self.stroke_data
def refresh_triangulation(self):
for mob in self.get_family():
mob.needs_new_triangulation = True
return self
def get_triangulation(self, normal_vector=None):
# Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader.
# First triangles come directly from the points
if normal_vector is None:
normal_vector = self.get_unit_normal()
if not self.needs_new_triangulation:
return self.triangulation
points = self.get_points()
if len(points) <= 1:
self.triangulation = np.zeros(0, dtype='i4')
self.needs_new_triangulation = False
return self.triangulation
# Rotate points such that unit normal vector is OUT
# TODO, 99% of the time this does nothing. Do a check for that?
points = np.dot(points, z_to_vector(normal_vector))
indices = np.arange(len(points), dtype=int)
b0s = points[0::3]
b1s = points[1::3]
b2s = points[2::3]
v01s = b1s - b0s
v12s = b2s - b1s
crosses = cross2d(v01s, v12s)
convexities = np.sign(crosses)
atol = self.tolerance_for_point_equality
end_of_loop = np.zeros(len(b0s), dtype=bool)
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
end_of_loop[-1] = True
concave_parts = convexities < 0
# These are the vertices to which we'll apply a polygon triangulation
inner_vert_indices = np.hstack([
indices[0::3],
indices[1::3][concave_parts],
indices[2::3][end_of_loop],
])
inner_vert_indices.sort()
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
tri_indices = np.hstack([indices, inner_tri_indices])
self.triangulation = tri_indices
self.needs_new_triangulation = False
return tri_indices
def get_fill_shader_data(self):
points = self.get_points()
if len(self.fill_data) != len(points):

View file

@ -0,0 +1,23 @@
import os
import tempfile
from manimlib.config import get_custom_defaults
from manimlib.config import get_manim_dir
CUSTOMIZATION = {}
def get_customization():
if not CUSTOMIZATION:
CUSTOMIZATION.update(get_custom_defaults())
directories = CUSTOMIZATION["directories"]
# Unless user has specified otherwise, use the system default temp
# directory for storing tex files, mobject_data, etc.
if not directories["temporary_storage"]:
directories["temporary_storage"] = tempfile.gettempdir()
# Assumes all shaders are written into manimlib/shaders
directories["shaders"] = os.path.join(
get_manim_dir(), "manimlib", "shaders"
)
return CUSTOMIZATION

View file

@ -1,28 +1,11 @@
import os
import tempfile
from manimlib.utils.file_ops import guarantee_existence
from manimlib.config import get_custom_defaults
from manimlib.config import get_manim_dir
PRE_COMPUTED_DIRS = {}
from manimlib.utils.customization import get_customization
def get_directories():
if not PRE_COMPUTED_DIRS:
custom_defaults = get_custom_defaults()
PRE_COMPUTED_DIRS.update(custom_defaults["directories"])
# Unless user has specified otherwise, use the system default temp
# directory for storing tex files, mobject_data, etc.
if not PRE_COMPUTED_DIRS["temporary_storage"]:
PRE_COMPUTED_DIRS["temporary_storage"] = tempfile.gettempdir()
# Assumes all shaders are written into manimlib/shaders
PRE_COMPUTED_DIRS["shaders"] = os.path.join(
get_manim_dir(), "manimlib", "shaders"
)
return PRE_COMPUTED_DIRS
return get_customization()["directories"]
def get_temp_dir():