From 8b454fbe9335a7011e947093230b07a74ba9c653 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 18 Oct 2021 07:12:05 -0700 Subject: [PATCH 01/37] Slight tweaks to how saturation_factor works on newton-fractal --- manimlib/shaders/newton_fractal/frag.glsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/shaders/newton_fractal/frag.glsl b/manimlib/shaders/newton_fractal/frag.glsl index 8315f9ec..223f8556 100644 --- a/manimlib/shaders/newton_fractal/frag.glsl +++ b/manimlib/shaders/newton_fractal/frag.glsl @@ -75,7 +75,7 @@ vec2 seek_root(vec2 z, vec2[MAX_DEGREE + 1] coefs, int max_steps, out float n_it } z = z - step; } - n_iters -= clamp((threshold - curr_len) / (last_len - curr_len), 0.0, 1.0); + n_iters -= log(curr_len) / log(threshold); return z; } @@ -118,7 +118,7 @@ void main() { color = colors[i]; } } - color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 5 * saturation_factor); + color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 2 * saturation_factor); if(black_for_cycles > 0 && min_dist > CLOSE_ENOUGH){ color = vec4(0.0, 0.0, 0.0, 1.0); From 82fa6ab125c3dff4c775f68de3de1b3adf9e585e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 24 Oct 2021 09:28:39 -0700 Subject: [PATCH 02/37] Temporary hack to fix a bug I don't understand --- manimlib/camera/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 8144dd48..e585d110 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -445,6 +445,8 @@ class Camera(object): def get_texture_id(self, path): if path not in self.path_to_texture: + if self.n_textures == 15: # I have no clue why this is needed + self.n_textures += 1 tid = self.n_textures self.n_textures += 1 im = Image.open(path).convert("RGBA") From deb1311e482ff0c73488110f7de3c8d0cf571c70 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 24 Oct 2021 09:28:52 -0700 Subject: [PATCH 03/37] Fix VideoIcon --- manimlib/mobject/svg/drawings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 4b0637da..69cf6c1d 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -200,12 +200,11 @@ class Laptop(VGroup): class VideoIcon(SVGMobject): CONFIG = { - "file_name": "video_icon", "width": FRAME_WIDTH / 12., } def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) + super().__init__(file_name="video_icon", **kwargs) self.center() self.set_width(self.width) self.set_stroke(color=WHITE, width=0) From 8ab95ebe9db578d381c3c056e9395b0e483b5da3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 1 Nov 2021 13:04:53 -0700 Subject: [PATCH 04/37] Change where unit_normal data gets updated --- manimlib/mobject/types/vectorized_mobject.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 69bfd0b9..48d2902c 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -74,7 +74,6 @@ class VMobject(Mobject): self.needs_new_triangulation = True self.triangulation = np.zeros(0, dtype='i4') super().__init__(**kwargs) - self.refresh_unit_normal() def get_group_class(self): return VGroup @@ -629,17 +628,19 @@ class VMobject(Mobject): area_vect = self.get_area_vector() area = get_norm(area_vect) if area > 0: - return area_vect / area + normal = area_vect / area else: points = self.get_points() - return get_unit_normal( + normal = get_unit_normal( points[1] - points[0], points[2] - points[1], ) + self.data["unit_normal"][:] = normal + return normal def refresh_unit_normal(self): for mob in self.get_family(): - mob.data["unit_normal"][:] = mob.get_unit_normal(recompute=True) + mob.get_unit_normal(recompute=True) return self # Alignment @@ -797,7 +798,7 @@ class VMobject(Mobject): # 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() + normal_vector = self.get_unit_normal(recompute=True) if not self.needs_new_triangulation: return self.triangulation From 185782a2e7ea5b0c96ff164d0afeab64477aa18b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 1 Nov 2021 13:05:13 -0700 Subject: [PATCH 05/37] Remove stray brace --- manimlib/shaders/quadratic_bezier_fill/frag.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index b2a1c82a..3e9433b2 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -3,7 +3,7 @@ #INSERT camera_uniform_declarations.glsl in vec4 color; -in float fill_all; // Either 0 or 1e +in float fill_all; // Either 0 or 1 in float uv_anti_alias_width; in vec3 xyz_coords; From 81c3ae30372e288dc772633dbd17def6e603753e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 8 Nov 2021 21:43:57 -0800 Subject: [PATCH 06/37] Have separate notions of gloss and reflectiveness --- manimlib/mobject/mobject.py | 36 +++++++++---- .../inserts/camera_uniform_declarations.glsl | 2 +- manimlib/shaders/inserts/finalize_color.glsl | 51 ++++++++++++------- ...et_rotated_surface_unit_normal_vector.glsl | 2 +- .../inserts/position_point_into_frame.glsl | 4 +- manimlib/shaders/mandelbrot_fractal/frag.glsl | 4 ++ manimlib/shaders/newton_fractal/frag.glsl | 4 ++ .../shaders/quadratic_bezier_fill/geom.glsl | 4 ++ .../shaders/quadratic_bezier_stroke/geom.glsl | 4 ++ manimlib/shaders/surface/frag.glsl | 4 ++ manimlib/shaders/textured_surface/frag.glsl | 4 ++ manimlib/shaders/true_dot/frag.glsl | 30 +++++++---- 12 files changed, 108 insertions(+), 41 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index b5e89fc0..35e3a0f5 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -43,10 +43,13 @@ class Mobject(object): "opacity": 1, "dim": 3, # TODO, get rid of this # Lighting parameters - # Positive gloss up to 1 makes it reflect the light. - "gloss": 0.0, - # Positive shadow up to 1 makes a side opposite the light darker + # ... + # Larger reflectiveness makes things brighter when facing the light + "reflectiveness": 0.0, + # Larger shadow makes faces opposite the light darker "shadow": 0.0, + # Makes parts bright where light gets reflected toward the camera + "gloss": 0.0, # For shaders "shader_folder": "", "render_primitive": moderngl.TRIANGLE_STRIP, @@ -82,11 +85,11 @@ class Mobject(object): def __str__(self): return self.__class__.__name__ - def __add__(self, other : 'Mobject') -> 'Mobject': + def __add__(self, other: 'Mobject') -> 'Mobject': assert(isinstance(other, Mobject)) return self.get_group_class()(self, other) - def __mul__(self, other : 'int') -> 'Mobject': + def __mul__(self, other: 'int') -> 'Mobject': assert(isinstance(other, int)) return self.replicate(other) @@ -102,6 +105,7 @@ class Mobject(object): "is_fixed_in_frame": float(self.is_fixed_in_frame), "gloss": self.gloss, "shadow": self.shadow, + "reflectiveness": self.reflectiveness, } def init_colors(self): @@ -394,6 +398,7 @@ class Mobject(object): self.submobjects.sort(key=submob_func) else: self.submobjects.sort(key=lambda m: point_to_num_func(m.get_center())) + self.assemble_family() return self def shuffle(self, recurse=False): @@ -401,6 +406,7 @@ class Mobject(object): for submob in self.submobjects: submob.shuffle(recurse=True) random.shuffle(self.submobjects) + self.assemble_family() return self # Copying @@ -974,12 +980,12 @@ class Mobject(object): def fade(self, darkness=0.5, recurse=True): self.set_opacity(1.0 - darkness, recurse=recurse) - def get_gloss(self): - return self.uniforms["gloss"] + def get_reflectiveness(self): + return self.uniforms["reflectiveness"] - def set_gloss(self, gloss, recurse=True): + def set_reflectiveness(self, reflectiveness, recurse=True): for mob in self.get_family(recurse): - mob.uniforms["gloss"] = gloss + mob.uniforms["reflectiveness"] = reflectiveness return self def get_shadow(self): @@ -990,6 +996,14 @@ class Mobject(object): mob.uniforms["shadow"] = shadow return self + def get_gloss(self): + return self.uniforms["gloss"] + + def set_gloss(self, gloss, recurse=True): + for mob in self.get_family(recurse): + mob.uniforms["gloss"] = gloss + return self + # Background rectangle def add_background_rectangle(self, color=None, opacity=0.75, **kwargs): @@ -1617,8 +1631,8 @@ class Group(Mobject): raise Exception("All submobjects must be of type Mobject") Mobject.__init__(self, **kwargs) self.add(*mobjects) - - def __add__(self, other : 'Mobject' or 'Group'): + + def __add__(self, other: 'Mobject' or 'Group'): assert(isinstance(other, Mobject)) return self.add(other) diff --git a/manimlib/shaders/inserts/camera_uniform_declarations.glsl b/manimlib/shaders/inserts/camera_uniform_declarations.glsl index b40b1b90..c67a01e1 100644 --- a/manimlib/shaders/inserts/camera_uniform_declarations.glsl +++ b/manimlib/shaders/inserts/camera_uniform_declarations.glsl @@ -1,6 +1,6 @@ uniform vec2 frame_shape; uniform float anti_alias_width; -uniform vec3 camera_center; +uniform vec3 camera_offset; uniform mat3 camera_rotation; uniform float is_fixed_in_frame; uniform float focal_distance; \ No newline at end of file diff --git a/manimlib/shaders/inserts/finalize_color.glsl b/manimlib/shaders/inserts/finalize_color.glsl index e7b64eee..de45e86c 100644 --- a/manimlib/shaders/inserts/finalize_color.glsl +++ b/manimlib/shaders/inserts/finalize_color.glsl @@ -13,39 +13,56 @@ vec4 add_light(vec4 color, vec3 point, vec3 unit_normal, vec3 light_coords, + vec3 cam_coords, + float reflectiveness, float gloss, float shadow){ - if(gloss == 0.0 && shadow == 0.0) return color; + if(reflectiveness == 0.0 && gloss == 0.0 && shadow == 0.0) return color; - float camera_distance = focal_distance; + vec4 result = color; // Assume everything has already been rotated such that camera is in the z-direction - vec3 to_camera = vec3(0, 0, camera_distance) - point; - vec3 to_light = light_coords - point; + // cam_coords = vec3(0, 0, focal_distance); + vec3 to_camera = normalize(cam_coords - point); + vec3 to_light = normalize(light_coords - point); - // TODO, do we actually want this? It effectively treats surfaces as two-sided - if(dot(to_camera,unit_normal) < 0){ - unit_normal *= -1; - } + // 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); - float dot_prod = dot(normalize(light_reflection), normalize(to_camera)); - float shine = gloss * exp(-3 * pow(1 - dot_prod, 2)); - float dp2 = dot(normalize(to_light), unit_normal); - float darkening = mix(1, max(dp2, 0), shadow); - return vec4( - darkening * mix(color.rgb, vec3(1.0), shine), - color.a - ); + float light_to_cam = dot(light_reflection, to_camera); + float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2)); + bright_factor += shine; + + result.rgb = mix(result.rgb, vec3(1.0), bright_factor); + if (light_to_normal < 0){ + // Darken + result.rgb = mix(result.rgb, vec3(0.0), -light_to_normal * shadow); + } + // float darkening = mix(1, max(light_to_normal, 0), shadow); + // return vec4( + // darkening * mix(color.rgb, vec3(1.0), shine), + // color.a + // ); + return result; } vec4 finalize_color(vec4 color, vec3 point, vec3 unit_normal, vec3 light_coords, + vec3 cam_coords, + float reflectiveness, float gloss, float shadow){ ///// INSERT COLOR FUNCTION HERE ///// // The line above may be replaced by arbitrary code snippets, as per // the method Mobject.set_color_by_code - return add_light(color, point, unit_normal, light_coords, gloss, shadow); + return add_light( + color, point, unit_normal, light_coords, cam_coords, + reflectiveness, gloss, shadow + ); } \ No newline at end of file diff --git a/manimlib/shaders/inserts/get_rotated_surface_unit_normal_vector.glsl b/manimlib/shaders/inserts/get_rotated_surface_unit_normal_vector.glsl index a9d637fe..012ec682 100644 --- a/manimlib/shaders/inserts/get_rotated_surface_unit_normal_vector.glsl +++ b/manimlib/shaders/inserts/get_rotated_surface_unit_normal_vector.glsl @@ -1,5 +1,5 @@ // Assumes the following uniforms exist in the surrounding context: -// uniform vec3 camera_center; +// uniform vec3 camera_offset; // uniform mat3 camera_rotation; vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){ diff --git a/manimlib/shaders/inserts/position_point_into_frame.glsl b/manimlib/shaders/inserts/position_point_into_frame.glsl index 4cef7e38..2e029579 100644 --- a/manimlib/shaders/inserts/position_point_into_frame.glsl +++ b/manimlib/shaders/inserts/position_point_into_frame.glsl @@ -1,6 +1,6 @@ // Assumes the following uniforms exist in the surrounding context: // uniform float is_fixed_in_frame; -// uniform vec3 camera_center; +// uniform vec3 camera_offset; // uniform mat3 camera_rotation; vec3 rotate_point_into_frame(vec3 point){ @@ -15,5 +15,5 @@ vec3 position_point_into_frame(vec3 point){ if(bool(is_fixed_in_frame)){ return point; } - return rotate_point_into_frame(point - camera_center); + return rotate_point_into_frame(point - camera_offset); } diff --git a/manimlib/shaders/mandelbrot_fractal/frag.glsl b/manimlib/shaders/mandelbrot_fractal/frag.glsl index 7d7e593f..0a2d40cc 100644 --- a/manimlib/shaders/mandelbrot_fractal/frag.glsl +++ b/manimlib/shaders/mandelbrot_fractal/frag.glsl @@ -1,6 +1,8 @@ #version 330 uniform vec3 light_source_position; +uniform vec3 camera_position; +uniform float reflectiveness; uniform float gloss; uniform float shadow; uniform float focal_distance; @@ -71,6 +73,8 @@ void main() { xyz_coords, vec3(0.0, 0.0, 1.0), light_source_position, + camera_position, + reflectiveness, gloss, shadow ); diff --git a/manimlib/shaders/newton_fractal/frag.glsl b/manimlib/shaders/newton_fractal/frag.glsl index 223f8556..74d4f4e9 100644 --- a/manimlib/shaders/newton_fractal/frag.glsl +++ b/manimlib/shaders/newton_fractal/frag.glsl @@ -1,6 +1,8 @@ #version 330 uniform vec3 light_source_position; +uniform vec3 camera_position; +uniform float reflectiveness; uniform float gloss; uniform float shadow; uniform float focal_distance; @@ -151,6 +153,8 @@ void main() { xyz_coords, vec3(0.0, 0.0, 1.0), light_source_position, + camera_position, + reflectiveness, gloss, shadow ); diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index 4fd9245f..7bc48d3e 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -11,6 +11,8 @@ uniform float focal_distance; uniform float is_fixed_in_frame; // Needed for finalize_color uniform vec3 light_source_position; +uniform vec3 camera_position; +uniform float reflectiveness; uniform float gloss; uniform float shadow; @@ -44,6 +46,8 @@ void emit_vertex_wrapper(vec3 point, int index){ point, v_global_unit_normal[index], light_source_position, + camera_position, + reflectiveness, gloss, shadow ); diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 8baea0f9..08d35891 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -13,7 +13,9 @@ uniform float flat_stroke; //Needed for lighting uniform vec3 light_source_position; +uniform vec3 camera_position; uniform float joint_type; +uniform float reflectiveness; uniform float gloss; uniform float shadow; @@ -259,6 +261,8 @@ void main() { xyz_coords, v_global_unit_normal[index_map[i]], light_source_position, + camera_position, + reflectiveness, gloss, shadow ); diff --git a/manimlib/shaders/surface/frag.glsl b/manimlib/shaders/surface/frag.glsl index 707621a1..204a37dd 100644 --- a/manimlib/shaders/surface/frag.glsl +++ b/manimlib/shaders/surface/frag.glsl @@ -1,6 +1,8 @@ #version 330 uniform vec3 light_source_position; +uniform vec3 camera_position; +uniform float reflectiveness; uniform float gloss; uniform float shadow; uniform float focal_distance; @@ -19,6 +21,8 @@ void main() { xyz_coords, normalize(v_normal), light_source_position, + camera_position, + reflectiveness, gloss, shadow ); diff --git a/manimlib/shaders/textured_surface/frag.glsl b/manimlib/shaders/textured_surface/frag.glsl index 616b06e2..dc31ba36 100644 --- a/manimlib/shaders/textured_surface/frag.glsl +++ b/manimlib/shaders/textured_surface/frag.glsl @@ -4,6 +4,8 @@ uniform sampler2D LightTexture; uniform sampler2D DarkTexture; uniform float num_textures; uniform vec3 light_source_position; +uniform vec3 camera_position; +uniform float reflectiveness; uniform float gloss; uniform float shadow; uniform float focal_distance; @@ -36,6 +38,8 @@ void main() { xyz_coords, normalize(v_normal), light_source_position, + camera_position, + reflectiveness, gloss, shadow ); diff --git a/manimlib/shaders/true_dot/frag.glsl b/manimlib/shaders/true_dot/frag.glsl index 0be9a2ab..eca8b0b1 100644 --- a/manimlib/shaders/true_dot/frag.glsl +++ b/manimlib/shaders/true_dot/frag.glsl @@ -1,10 +1,13 @@ #version 330 uniform vec3 light_source_position; +uniform vec3 camera_position; +uniform float reflectiveness; uniform float gloss; uniform float shadow; uniform float anti_alias_width; uniform float focal_distance; +uniform float glow_factor; in vec4 color; in float radius; @@ -22,14 +25,23 @@ void main() { if (signed_dist > 0.5 * anti_alias_width){ discard; } - vec3 normal = vec3(diff / radius, sqrt(1 - (dist * dist) / (radius * radius))); - frag_color = finalize_color( - color, - vec3(point.xy, 0.0), - normal, - light_source_position, - gloss, - shadow - ); + frag_color = color; + if(gloss > 0 || shadow > 0){ + vec3 normal = vec3(diff / radius, sqrt(1 - (dist * dist) / (radius * radius))); + frag_color = finalize_color( + frag_color, + vec3(point.xy, 0.0), + normal, + light_source_position, + camera_position, + reflectiveness, + gloss, + shadow + ); + } + if(glow_factor > 0){ + frag_color.a *= pow(1 - dist / radius, glow_factor); + } + frag_color.a *= smoothstep(0.5, -0.5, signed_dist / anti_alias_width); } \ No newline at end of file From 4339f97c56db8bfbb851b40c8a05a32863b35961 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 8 Nov 2021 21:46:35 -0800 Subject: [PATCH 07/37] Small refactor and added functionality --- manimlib/camera/camera.py | 55 +++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index e585d110..2bd725ea 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -1,4 +1,5 @@ import moderngl +import math from colour import Color import OpenGL.GL as gl @@ -139,6 +140,16 @@ class CameraFrame(Mobject): def get_focal_distance(self): return self.focal_distance * self.get_height() + def get_implied_camera_location(self): + theta, phi, gamma = self.get_euler_angles() + dist = self.get_focal_distance() + x, y, z = self.get_center() + return ( + x + dist * math.sin(theta) * math.sin(phi), + y - dist * math.cos(theta) * math.sin(phi), + z + dist * math.cos(phi) + ) + def interpolate(self, *args, **kwargs): super().interpolate(*args, **kwargs) self.refresh_rotation_matrix() @@ -194,20 +205,30 @@ class Camera(object): fbo = self.get_fbo(ctx, 0) else: fbo = ctx.detect_framebuffer() + self.ctx = ctx + self.fbo = fbo + self.set_ctx_blending() # For multisample antialiasing fbo_msaa = self.get_fbo(ctx, self.samples) fbo_msaa.use() + self.fbo_msaa = fbo_msaa - ctx.enable(moderngl.BLEND) - ctx.blend_func = ( + def set_ctx_blending(self, enable=True): + if enable: + self.ctx.enable(moderngl.BLEND) + else: + self.ctx.disable(moderngl.BLEND) + self.ctx.blend_func = ( moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA, - moderngl.ONE, moderngl.ONE + # moderngl.ONE, moderngl.ONE ) - self.ctx = ctx - self.fbo = fbo - self.fbo_msaa = fbo_msaa + def set_ctx_depth_test(self, enable=True): + if enable: + self.ctx.enable(moderngl.DEPTH_TEST) + else: + self.ctx.disable(moderngl.DEPTH_TEST) def init_light_source(self): self.light_source = Point(self.light_source_position) @@ -297,6 +318,9 @@ class Camera(object): def get_frame_center(self): return self.frame.get_center() + def get_location(self): + return self.frame.get_implied_camera_location() + def resize_frame_shape(self, fixed_dimension=0): """ Changes frame_shape to match the aspect ratio @@ -327,17 +351,11 @@ class Camera(object): shader_wrapper = render_group["shader_wrapper"] shader_program = render_group["prog"] self.set_shader_uniforms(shader_program, shader_wrapper) - self.update_depth_test(shader_wrapper) + self.set_ctx_depth_test(shader_wrapper.depth_test) render_group["vao"].render(int(shader_wrapper.render_primitive)) if render_group["single_use"]: self.release_render_group(render_group) - def update_depth_test(self, shader_wrapper): - if shader_wrapper.depth_test: - self.ctx.enable(moderngl.DEPTH_TEST) - else: - self.ctx.disable(moderngl.DEPTH_TEST) - def get_render_group_list(self, mobject): try: return self.static_mobject_to_render_group_list[id(mobject)] @@ -427,14 +445,18 @@ class Camera(object): anti_alias_width = self.anti_alias_width / (ph / fh) # Orient light rotation = frame.get_inverse_camera_rotation_matrix() - light_pos = self.light_source.get_location() - light_pos = np.dot(rotation, light_pos) + offset = frame.get_center() + light_pos = np.dot( + rotation, self.light_source.get_location() + offset + ) + cam_pos = self.frame.get_implied_camera_location() # TODO self.perspective_uniforms = { "frame_shape": frame.get_shape(), "anti_alias_width": anti_alias_width, - "camera_center": tuple(frame.get_center()), + "camera_offset": tuple(offset), "camera_rotation": tuple(np.array(rotation).T.flatten()), + "camera_position": tuple(cam_pos), "light_source_position": tuple(light_pos), "focal_distance": frame.get_focal_distance(), } @@ -470,4 +492,5 @@ class Camera(object): class ThreeDCamera(Camera): CONFIG = { "samples": 4, + "anti_alias_width": 0, } From 317a5d6226475b6b54a78db7116c373ef84ea923 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 8 Nov 2021 21:47:02 -0800 Subject: [PATCH 08/37] Make it possible to set full screen preview as a default --- manimlib/config.py | 2 +- manimlib/default_config.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/config.py b/manimlib/config.py index 8992ce5b..76f5477c 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -282,7 +282,7 @@ def get_configuration(args): mon_index = custom_config["window_monitor"] monitor = monitors[min(mon_index, len(monitors) - 1)] window_width = monitor.width - if not args.full_screen: + if not (args.full_screen or custom_config["full_screen"]): window_width //= 2 window_height = window_width * 9 // 16 config["window_config"] = { diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index c67dd502..c948a8e5 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -34,6 +34,7 @@ style: # the window on the monitor, e.g. "960,540" window_position: UR window_monitor: 0 +full_screen: False # If break_into_partial_movies is set to True, then many small # files will be written corresponding to each Scene.play and # Scene.wait call, and these files will then be combined From c73d507c76af5c8602d4118bc7538ba04c03ebae Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 8 Nov 2021 21:47:26 -0800 Subject: [PATCH 09/37] Fix SurfaceMesh to be evenly spaced --- manimlib/mobject/three_dimensions.py | 34 ++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index 277e552c..949e643b 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -6,6 +6,7 @@ from manimlib.mobject.types.surface import SGroup from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.geometry import Square +from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import z_to_vector @@ -14,9 +15,9 @@ from manimlib.utils.space_ops import compass_directions class SurfaceMesh(VGroup): CONFIG = { - "resolution": (21, 21), + "resolution": (21, 11), "stroke_width": 1, - "normal_nudge": 1e-3, + "normal_nudge": 1e-2, "depth_test": True, "flat_stroke": False, } @@ -32,8 +33,11 @@ class SurfaceMesh(VGroup): full_nu, full_nv = uv_surface.resolution part_nu, part_nv = self.resolution - u_indices = np.linspace(0, full_nu, part_nu).astype(int) - v_indices = np.linspace(0, full_nv, part_nv).astype(int) + # 'indices' are treated as floats. Later, there will be + # an interpolation between the floor and ceiling of these + # indices + u_indices = np.linspace(0, full_nu, part_nu) + v_indices = np.linspace(0, full_nv, part_nv) points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points() normals = uv_surface.get_unit_normals() @@ -42,12 +46,28 @@ class SurfaceMesh(VGroup): for ui in u_indices: path = VMobject() - full_ui = full_nv * ui - path.set_points_smoothly(nudged_points[full_ui:full_ui + full_nv]) + # full_ui = full_nv * ui + # path.set_points_smoothly( + # nudged_points[full_ui:full_ui + full_nv] + # ) + low_ui = full_nv * int(math.floor(ui)) + high_ui = full_nv * int(math.ceil(ui)) + path.set_points_smoothly(interpolate( + nudged_points[low_ui:low_ui + full_nv], + nudged_points[high_ui:high_ui + full_nv], + ui % 1 + )) self.add(path) for vi in v_indices: path = VMobject() - path.set_points_smoothly(nudged_points[vi::full_nv]) + # path.set_points_smoothly( + # nudged_points[vi::full_nv] + # ) + path.set_points_smoothly(interpolate( + nudged_points[int(math.floor(vi))::full_nv], + nudged_points[int(math.ceil(vi))::full_nv], + vi % 1 + )) self.add(path) From 2c7689ed9e81229ce87c648f97f26267956c0bc9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 8 Nov 2021 21:47:48 -0800 Subject: [PATCH 10/37] Enable glow_factor on dots --- manimlib/mobject/types/dot_cloud.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index de5ce517..3b0afd18 100644 --- a/manimlib/mobject/types/dot_cloud.py +++ b/manimlib/mobject/types/dot_cloud.py @@ -17,6 +17,7 @@ class DotCloud(PMobject): "color": GREY_C, "opacity": 1, "radius": DEFAULT_DOT_RADIUS, + "glow_factor": 0, "shader_folder": "true_dot", "render_primitive": moderngl.POINTS, "shader_dtype": [ @@ -36,6 +37,10 @@ class DotCloud(PMobject): self.data["radii"] = np.zeros((1, 1)) self.set_radius(self.radius) + def init_uniforms(self): + super().init_uniforms() + self.uniforms["glow_factor"] = self.glow_factor + def to_grid(self, n_rows, n_cols, n_layers=1, buff_ratio=None, h_buff_ratio=1.0, @@ -85,6 +90,12 @@ class DotCloud(PMobject): def get_radius(self): return self.get_radii().max() + def set_glow_factor(self, glow_factor): + self.uniforms["glow_factor"] = glow_factor + + def get_glow_factor(self): + return self.uniforms["glow_factor"] + def compute_bounding_box(self): bb = super().compute_bounding_box() radius = self.get_radius() From e20690b7c1ba5a1bfc7cd7890e86745601a8ff3a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 8 Nov 2021 21:48:42 -0800 Subject: [PATCH 11/37] Don't necessarily remove anti_alias on ThreeDScene --- manimlib/scene/three_d_scene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/scene/three_d_scene.py b/manimlib/scene/three_d_scene.py index 728d1d37..2d29fff8 100644 --- a/manimlib/scene/three_d_scene.py +++ b/manimlib/scene/three_d_scene.py @@ -5,7 +5,6 @@ class ThreeDScene(Scene): CONFIG = { "camera_config": { "samples": 4, - "anti_alias_width": 0, } } From 8f3ff9116567b97ff0c5670470d58ae217c67e5a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 9 Nov 2021 09:15:00 -0800 Subject: [PATCH 12/37] Add reflectiveness to style and default to fill for VMobject.get_color --- manimlib/mobject/types/vectorized_mobject.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 9661d9a6..a6586d87 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -149,6 +149,7 @@ class VMobject(Mobject): stroke_rgba=None, stroke_width=None, stroke_background=True, + reflectiveness=None, gloss=None, shadow=None, recurse=True): @@ -176,6 +177,8 @@ class VMobject(Mobject): background=stroke_background, ) + if reflectiveness is not None: + self.set_reflectiveness(reflectiveness, recurse=recurse) if gloss is not None: self.set_gloss(gloss, recurse=recurse) if shadow is not None: @@ -188,6 +191,7 @@ class VMobject(Mobject): "stroke_rgba": self.data['stroke_rgba'], "stroke_width": self.data['stroke_width'], "stroke_background": self.draw_stroke_behind_fill, + "reflectiveness": self.get_reflectiveness(), "gloss": self.get_gloss(), "shadow": self.get_shadow(), } @@ -276,9 +280,9 @@ class VMobject(Mobject): return self.get_stroke_opacities()[0] def get_color(self): - if self.has_stroke(): - return self.get_stroke_color() - return self.get_fill_color() + if self.has_fill(): + return self.get_fill_color() + return self.get_stroke_color() def has_stroke(self): return self.get_stroke_widths().any() and self.get_stroke_opacities().any() From 5930e6a176cbf9ec8fd5fed12cf17a10098eb269 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 9 Nov 2021 09:15:15 -0800 Subject: [PATCH 13/37] Refresh unit normal when reversing points --- manimlib/mobject/mobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 35e3a0f5..fa555397 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -157,6 +157,7 @@ class Mobject(object): for mob in self.get_family(): for key in mob.data: mob.data[key] = mob.data[key][::-1] + self.refresh_unit_normal() return self def apply_points_function(self, func, about_point=None, about_edge=ORIGIN, works_on_bounding_box=False): From 7070777408a4f68ad9376ae1ec252f4c0c3df116 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 12 Nov 2021 15:47:23 -0800 Subject: [PATCH 14/37] Tiny formatting change --- manimlib/scene/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 118404be..bfc011d4 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -135,7 +135,7 @@ class Scene(object): for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"): local_ns[term] = getattr(self, term) log.info("Tips: Now the embed iPython terminal is open. But you can't interact with" - " the window directly. To do so, you need to type `touch()` or `self.interact()`") + " the window directly. To do so, you need to type `touch()` or `self.interact()`") shell(local_ns=local_ns, stack_depth=2) # End scene when exiting an embed. raise EndSceneEarlyException() From d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 14 Nov 2021 12:31:56 -0800 Subject: [PATCH 15/37] Add option to insert embed line from the command line (mildly hacky) --- manimlib/config.py | 60 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index 76f5477c..dce5e657 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -5,6 +5,7 @@ import importlib import os import sys import yaml +from contextlib import contextmanager from screeninfo import get_monitors from manimlib.utils.config_ops import merge_dicts_recursively @@ -12,6 +13,9 @@ from manimlib.utils.init_config import init_customization from manimlib.logger import log +__config_file__ = "custom_config.yml" + + def parse_cli(): try: parser = argparse.ArgumentParser() @@ -112,6 +116,12 @@ def parse_cli(): "in two comma separated values, e.g. \"3,6\", it will end" "the rendering at the second value", ) + parser.add_argument( + "-e", "--embed", + help="Takes a line number as an argument, and results" + "in the scene being called as if the line `self.embed()`" + "was inserted into the scene code at that line number." + ) parser.add_argument( "-r", "--resolution", help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"", @@ -162,14 +172,31 @@ def get_manim_dir(): def get_module(file_name): if file_name is None: return None - else: - module_name = file_name.replace(os.sep, ".").replace(".py", "") - spec = importlib.util.spec_from_file_location(module_name, file_name) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module + module_name = file_name.replace(os.sep, ".").replace(".py", "") + spec = importlib.util.spec_from_file_location(module_name, file_name) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@contextmanager +def insert_embed_line(file_name, lineno): + with open(file_name, 'r') as fp: + lines = fp.readlines() + line = lines[lineno - 1] + n_spaces = len(line) - len(line.lstrip()) + lines.insert(lineno - 1, " " * n_spaces + "self.embed()\n") + + # Where should this really live? + alt_file = file_name.replace(".py", "_alt.py") + with open(alt_file, 'w') as fp: + fp.writelines(lines) + + try: + yield alt_file + finally: + os.remove(alt_file) -__config_file__ = "custom_config.yml" def get_custom_config(): global __config_file__ @@ -197,9 +224,11 @@ def get_custom_config(): def check_temporary_storage(config): if config["directories"]["temporary_storage"] == "" and sys.platform == "win32": - log.warning("You may be using Windows platform and have not specified the path of" + log.warning( + "You may be using Windows platform and have not specified the path of" " `temporary_storage`, which may cause OSError. So it is recommended" - " to specify the `temporary_storage` in the config file (.yml)") + " to specify the `temporary_storage` in the config file (.yml)" + ) def get_configuration(args): @@ -229,8 +258,10 @@ def get_configuration(args): elif not os.path.exists(__config_file__): log.info(f"Using the default configuration file, which you can modify in `{global_defaults_file}`") - log.info("If you want to create a local configuration file, you can create a file named" - f" `{__config_file__}`, or run `manimgl --config`") + log.info( + "If you want to create a local configuration file, you can create a file named" + f" `{__config_file__}`, or run `manimgl --config`" + ) custom_config = get_custom_config() check_temporary_storage(custom_config) @@ -260,7 +291,12 @@ def get_configuration(args): "quiet": args.quiet, } - module = get_module(args.file) + if args.embed is None: + module = get_module(args.file) + else: + with insert_embed_line(args.file, int(args.embed)) as alt_file: + module = get_module(alt_file) + config = { "module": module, "scene_names": args.scene_names, From 82bd02d21fbd89b71baa21e077e143f440df9014 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:08:35 -0800 Subject: [PATCH 16/37] Fix angle_between_vectors, add rotation_between_vectors --- manimlib/utils/space_ops.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 0e8c55e3..acc35385 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -12,6 +12,14 @@ from manimlib.constants import TAU from manimlib.utils.iterables import adjacent_pairs +def cross(v1, v2): + return [ + v1[1] * v2[2] - v1[2] * v2[1], + v1[2] * v2[0] - v1[0] * v2[2], + v1[0] * v2[1] - v1[1] * v2[0] + ] + + def get_norm(vect): return sum((x**2 for x in vect))**0.5 @@ -147,6 +155,13 @@ def z_to_vector(vector): return rotation_matrix(angle, axis=axis) +def rotation_between_vectors(v1, v2): + return rotation_matrix( + angle=angle_between_vectors(v1, v2), + axis=normalize(np.cross(v1, v2)) + ) + + def angle_of_vector(vector): """ Returns polar coordinate theta when vector is project on xy plane @@ -159,8 +174,7 @@ def angle_between_vectors(v1, v2): Returns the angle between two 3D vectors. This angle will always be btw 0 and pi """ - diff = (angle_of_vector(v2) - angle_of_vector(v1)) % TAU - return min(diff, TAU - diff) + return math.acos(np.dot(normalize(v1), normalize(v2))) def project_along_vector(point, vector): @@ -186,14 +200,6 @@ def normalize_along_axis(array, axis, fall_back=None): return array -def cross(v1, v2): - return np.array([ - v1[1] * v2[2] - v1[2] * v2[1], - v1[2] * v2[0] - v1[0] * v2[2], - v1[0] * v2[1] - v1[1] * v2[0] - ]) - - def get_unit_normal(v1, v2, tol=1e-6): v1 = normalize(v1) v2 = normalize(v2) From 0e78027186a976f7e5fa8d586f586bf6e6baab8d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:28:48 -0800 Subject: [PATCH 17/37] Improve point_from_proportion to account for arc length --- manimlib/mobject/types/vectorized_mobject.py | 28 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a6586d87..1c79164a 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -12,6 +12,7 @@ from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points from manimlib.utils.bezier import get_quadratic_approximation_of_cubic from manimlib.utils.bezier import interpolate +from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import partial_quadratic_bezier_points from manimlib.utils.color import rgb_to_hex @@ -546,12 +547,35 @@ class VMobject(Mobject): def get_num_curves(self): return self.get_num_points() // self.n_points_per_curve - def point_from_proportion(self, alpha): + def quick_point_from_proportion(self, alpha): + # Assumes all curves have the same length, so is inaccurate num_curves = self.get_num_curves() n, residue = integer_interpolate(0, num_curves, alpha) curve_func = self.get_nth_curve_function(n) return curve_func(residue) + def point_from_proportion(self, alpha): + if alpha <= 0: + return self.get_start() + elif alpha >= 1: + return self.get_end() + + partials = [0] + for tup in self.get_bezier_tuples(): + # Approximate length with straight line from start to end + arclen = get_norm(tup[0] - tup[-1]) + partials.append(partials[-1] + arclen) + full = partials[-1] + if full == 0: + return self.get_start() + # First index where the partial lenth is more alpha times the full length + i = next( + (i for i, x in enumerate(partials) if x >= full * alpha), + len(partials) # Default + ) + residue = inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha) + return self.get_nth_curve_function(i - 1)(residue) + def get_anchors_and_handles(self): """ returns anchors1, handles, anchors2, @@ -706,7 +730,7 @@ class VMobject(Mobject): if len(points) == 1: return np.repeat(points, nppc * n, 0) - bezier_groups = self.get_bezier_tuples_from_points(points) + bezier_groups = list(self.get_bezier_tuples_from_points(points)) norms = np.array([ get_norm(bg[nppc - 1] - bg[0]) for bg in bezier_groups From a7173142bf93fd309def0cc10f3c56f5e6972332 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:29:10 -0800 Subject: [PATCH 18/37] Fix VMobject.fade --- manimlib/mobject/types/vectorized_mobject.py | 31 ++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 1c79164a..b89492fa 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -188,9 +188,9 @@ class VMobject(Mobject): def get_style(self): return { - "fill_rgba": self.data['fill_rgba'], - "stroke_rgba": self.data['stroke_rgba'], - "stroke_width": self.data['stroke_width'], + "fill_rgba": self.data['fill_rgba'].copy(), + "stroke_rgba": self.data['stroke_rgba'].copy(), + "stroke_width": self.data['stroke_width'].copy(), "stroke_background": self.draw_stroke_behind_fill, "reflectiveness": self.get_reflectiveness(), "gloss": self.get_gloss(), @@ -222,16 +222,17 @@ class VMobject(Mobject): return self def fade(self, darkness=0.5, recurse=True): - factor = 1.0 - darkness - self.set_fill( - opacity=factor * self.get_fill_opacity(), - recurse=False, - ) - self.set_stroke( - opacity=factor * self.get_stroke_opacity(), - recurse=False, - ) - super().fade(darkness, recurse) + mobs = self.get_family() if recurse else [self] + for mob in mobs: + factor = 1.0 - darkness + mob.set_fill( + opacity=factor * mob.get_fill_opacity(), + recurse=False, + ) + mob.set_stroke( + opacity=factor * mob.get_stroke_opacity(), + recurse=False, + ) return self def get_fill_colors(self): @@ -508,10 +509,10 @@ class VMobject(Mobject): nppc = self.n_points_per_curve remainder = len(points) % nppc points = points[:len(points) - remainder] - return [ + return ( points[i:i + nppc] for i in range(0, len(points), nppc) - ] + ) def get_bezier_tuples(self): return self.get_bezier_tuples_from_points(self.get_points()) From 781a9934fda6ba11f22ba32e8ccddcb3ba78592e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:29:24 -0800 Subject: [PATCH 19/37] Add shortcut for setting black background stroke --- manimlib/mobject/types/vectorized_mobject.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index b89492fa..e80cfbc9 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -135,6 +135,10 @@ class VMobject(Mobject): mob.draw_stroke_behind_fill = background return self + def set_backstroke(self, color=BLACK, width=3, background=True): + self.set_stroke(color, width, background=background) + return self + def align_stroke_width_data_to_points(self, recurse=True): for mob in self.get_family(recurse): mob.data["stroke_width"] = resize_with_interpolation( From fbbea47d1190fb6ed8641497e26f3b6d2c31bd50 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:37:01 -0800 Subject: [PATCH 20/37] Change temp embed file name --- manimlib/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index dce5e657..258f6ea6 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -187,8 +187,7 @@ def insert_embed_line(file_name, lineno): n_spaces = len(line) - len(line.lstrip()) lines.insert(lineno - 1, " " * n_spaces + "self.embed()\n") - # Where should this really live? - alt_file = file_name.replace(".py", "_alt.py") + alt_file = file_name.replace(".py", "_inserted_embed.py") with open(alt_file, 'w') as fp: fp.writelines(lines) From e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:37:27 -0800 Subject: [PATCH 21/37] use quick_point_from_proportion for graph points --- manimlib/mobject/coordinate_systems.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 25e5d587..0177a2d6 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -151,14 +151,14 @@ class CoordinateSystem(): else: alpha = binary_search( function=lambda a: self.point_to_coords( - graph.point_from_proportion(a) + graph.quick_point_from_proportion(a) )[0], target=x, lower_bound=self.x_range[0], upper_bound=self.x_range[1], ) if alpha is not None: - return graph.point_from_proportion(alpha) + return graph.quick_point_from_proportion(alpha) else: return None From f3ecebee4327f909177b78bde286618b55358337 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:37:45 -0800 Subject: [PATCH 22/37] Remove unnecessary import --- manimlib/mobject/svg/text_mobject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 34f0ed15..cf78d111 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -1,4 +1,3 @@ -import copy import hashlib import os import re From 2cce4ccdd72cfc9babfd23d9d9a58bda9cfca528 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:38:08 -0800 Subject: [PATCH 23/37] Exchange gloss for reflectiveness --- manimlib/mobject/types/dot_cloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index 3b0afd18..8bc0797e 100644 --- a/manimlib/mobject/types/dot_cloud.py +++ b/manimlib/mobject/types/dot_cloud.py @@ -109,8 +109,8 @@ class DotCloud(PMobject): self.set_radii(scale_factor * self.get_radii()) return self - def make_3d(self, gloss=0.5, shadow=0.2): - self.set_gloss(gloss) + def make_3d(self, reflectiveness=0.5, shadow=0.2): + self.set_reflectiveness(reflectiveness) self.set_shadow(shadow) self.apply_depth_test() return self From ee2f68cd4916f85ca3a1f2eff211567a8856ed54 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:38:30 -0800 Subject: [PATCH 24/37] Exchange gloss for reflectiveness --- manimlib/mobject/types/surface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index 7ec4621c..e2c89b4d 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -20,7 +20,8 @@ class Surface(Mobject): "resolution": (101, 101), "color": GREY, "opacity": 1.0, - "gloss": 0.3, + "reflectiveness": 0.3, + "gloss": 0.1, "shadow": 0.4, "prefered_creation_axis": 1, # For du and dv steps. Much smaller and numerical error From 0b898a5594203668ed9cad38b490ab49ba233bd4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 16 Nov 2021 17:38:43 -0800 Subject: [PATCH 25/37] Add always_sort_to_camera for surfaces --- manimlib/mobject/types/surface.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index e2c89b4d..1160c1ae 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -162,6 +162,11 @@ class Surface(Mobject): tri_is[k::3] = tri_is[k::3][indices] return self + def always_sort_to_camera(self, camera): + self.add_updater(lambda m: m.sort_faces_back_to_front( + camera.get_location() - self.get_center() + )) + # For shaders def get_shader_data(self): s_points, du_points, dv_points = self.get_surface_points_and_nudged_points() From e899604a2d05f78202fcb3b9824ec34647237eae Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 17 Nov 2021 12:48:17 -0800 Subject: [PATCH 26/37] Add getter methods for specific euler angles --- manimlib/camera/camera.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 2bd725ea..369bfa19 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -122,6 +122,15 @@ class CameraFrame(Mobject): self.refresh_rotation_matrix() return self + def get_theta(self): + return self.data["euler_angles"][0] + + def get_phi(self): + return self.data["euler_angles"][1] + + def get_gamma(self): + return self.data["euler_angles"][2] + def get_shape(self): return (self.get_width(), self.get_height()) From 25045143a1211d58b508574a3d3ca6058fa508ea Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 17 Nov 2021 12:49:08 -0800 Subject: [PATCH 27/37] Have mobject uniforms supercede camera uniforms --- manimlib/camera/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 369bfa19..18adce91 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -437,7 +437,7 @@ class Camera(object): for name, path in shader_wrapper.texture_paths.items(): tid = self.get_texture_id(path) shader[name].value = tid - for name, value in it.chain(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()): + for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()): try: if isinstance(value, np.ndarray): value = tuple(value) From fbc329d7ce3b11821d47adf6052d932f7eff724a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 17 Nov 2021 12:49:53 -0800 Subject: [PATCH 28/37] Small bug fix for angle_between_vectors --- manimlib/utils/space_ops.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index acc35385..52044d32 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -10,6 +10,7 @@ from manimlib.constants import OUT from manimlib.constants import PI from manimlib.constants import TAU from manimlib.utils.iterables import adjacent_pairs +from manimlib.utils.simple_functions import clip def cross(v1, v2): @@ -174,7 +175,7 @@ def angle_between_vectors(v1, v2): Returns the angle between two 3D vectors. This angle will always be btw 0 and pi """ - return math.acos(np.dot(normalize(v1), normalize(v2))) + return math.acos(clip(np.dot(normalize(v1), normalize(v2)), -1, 1)) def project_along_vector(point, vector): From d2182b9112300558b6c074cefd685f97c10b3898 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 18 Nov 2021 17:51:56 -0800 Subject: [PATCH 29/37] Make sure set_length returns self --- manimlib/mobject/geometry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 1b88ec35..02422210 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -507,6 +507,7 @@ class Line(TipableVMobject): def set_length(self, length, **kwargs): self.scale(length / self.get_length(), **kwargs) + return self class DashedLine(Line): From eea3c6b29438f9e9325329c4355e76b9f635e97a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 18 Nov 2021 17:52:17 -0800 Subject: [PATCH 30/37] Better align SurfaceMesh to the corresponding surface polygons --- manimlib/mobject/three_dimensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index 949e643b..01abe501 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -36,8 +36,8 @@ class SurfaceMesh(VGroup): # 'indices' are treated as floats. Later, there will be # an interpolation between the floor and ceiling of these # indices - u_indices = np.linspace(0, full_nu, part_nu) - v_indices = np.linspace(0, full_nv, part_nv) + u_indices = np.linspace(0, full_nu - 1, part_nu) + v_indices = np.linspace(0, full_nv - 1, part_nv) points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points() normals = uv_surface.get_unit_normals() From 407c53f97c061bfd8a53beacd88af4c786f9e9ee Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 18 Nov 2021 17:52:48 -0800 Subject: [PATCH 31/37] Have rotation_between_vectors handle identical/similar vectors --- manimlib/utils/space_ops.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 52044d32..9c5e84d2 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -157,6 +157,8 @@ def z_to_vector(vector): def rotation_between_vectors(v1, v2): + if np.all(np.isclose(v1, v2)): + return np.identity(3) return rotation_matrix( angle=angle_between_vectors(v1, v2), axis=normalize(np.cross(v1, v2)) From bcd09906bea5eaaa5352e7bee8f3153f434cf606 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 22 Nov 2021 08:05:59 -0800 Subject: [PATCH 32/37] Fix bug in ShowSubmobjectsOneByOne --- manimlib/animation/creation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index 6a69815f..8101b12a 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -174,16 +174,12 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets): "int_func": np.ceil, } - def __init__(self, group, **kwargs): - new_group = Group(*group) - super().__init__(new_group, **kwargs) - def update_submobject_list(self, index): # N = len(self.all_submobs) if index == 0: self.mobject.set_submobjects([]) else: - self.mobject.set_submobjects(self.all_submobs[index - 1]) + self.mobject.set_submobjects([self.all_submobs[index - 1]]) # TODO, this is broken... From e9afb0ee3356f88604f76b9a05331767757280d3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 30 Nov 2021 11:28:26 -0800 Subject: [PATCH 33/37] Fix tiny PEP errors --- manimlib/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/__main__.py b/manimlib/__main__.py index c8faafae..d6af540d 100644 --- a/manimlib/__main__.py +++ b/manimlib/__main__.py @@ -10,7 +10,7 @@ def main(): print(f"ManimGL \033[32mv{__version__}\033[0m") args = manimlib.config.parse_cli() - if args.version and args.file == None: + if args.version and args.file is None: return if args.log_level: manimlib.logger.log.setLevel(args.log_level) @@ -24,5 +24,6 @@ def main(): for scene in scenes: scene.run() + if __name__ == "__main__": main() From ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 30 Nov 2021 11:29:12 -0800 Subject: [PATCH 34/37] Match fix_in_frame status for FlashAround mobject --- manimlib/animation/indication.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index 1ff26159..f99de961 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -224,6 +224,8 @@ class FlashAround(VShowPassingFlash): def __init__(self, mobject, **kwargs): digest_config(self, kwargs) path = self.get_path(mobject) + if mobject.is_fixed_in_frame: + path.fix_in_frame() path.insert_n_curves(self.n_inserted_curves) path.set_points(path.get_points_without_null_curves()) path.set_stroke(self.color, self.stroke_width) From ba23fbe71e4a038201cd7df1d200514ed1c13bc2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 30 Nov 2021 11:30:34 -0800 Subject: [PATCH 35/37] Make sure Mobject.is_fixed_in_frame stays updated with uniforms --- manimlib/mobject/mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index fa555397..3b5e7b9c 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1386,11 +1386,13 @@ class Mobject(object): @affects_shader_info_id def fix_in_frame(self): self.uniforms["is_fixed_in_frame"] = 1.0 + self.is_fixed_in_frame = True return self @affects_shader_info_id def unfix_from_frame(self): self.uniforms["is_fixed_in_frame"] = 0.0 + self.is_fixed_in_frame = False return self @affects_shader_info_id From 49743daf3244bfa11a427040bdde8e2bb79589e8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 30 Nov 2021 11:30:50 -0800 Subject: [PATCH 36/37] Add Mobject.insert_submobject method --- manimlib/mobject/mobject.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 3b5e7b9c..1050c3ee 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -313,6 +313,11 @@ class Mobject(object): self.assemble_family() return self + def insert_submobject(self, index, new_submob): + self.submobjects.insert(index, new_submob) + self.assemble_family() + return self + def set_submobjects(self, submobject_list): self.remove(*self.submobjects) self.add(*submobject_list) From 9dd1f47dabca1580d6102e34e44574b0cba556e7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 30 Nov 2021 11:41:33 -0800 Subject: [PATCH 37/37] Create single progress display for full scene render When a scene is written to file, it will now do a preliminary run of a copy of the scene with skip_animations turned on to count the total frames, which has the added benefit of catching runtime errors early, and allowing an quicker preview of the last frame to be sure everything will render as expected. The Progress display bars for individual animations are replaced with a more global progress display bar showing the full render time for the scene. This has the downside that all the non-rendering computations in a scene are run twice, so any scene with slow computations unrelated to rendering will take longer. But those are rarer, so the benefits seem worth it. --- manimlib/extract_scene.py | 24 +++++++++++++- manimlib/scene/scene.py | 50 ++++++++++++----------------- manimlib/scene/scene_file_writer.py | 26 +++++++++++++++ 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index e08f9f50..27f01887 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -1,5 +1,6 @@ import inspect import sys +import copy from manimlib.scene.scene import Scene from manimlib.config import get_custom_config @@ -38,7 +39,7 @@ def prompt_user_for_choice(scene_classes): "\nScene Name or Number: " ) return [ - name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str)-1] + name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1] for split_str in user_input.replace(" ", "").split(",") ] except IndexError: @@ -67,6 +68,24 @@ def get_scene_config(config): ]) +def compute_total_frames(scene_class, scene_config): + """ + When a scene is being written to file, a copy of the scene is run with + skip_animations set to true so as to count how many frames it will require. + This allows for a total progress bar on rendering, and also allows runtime + errors to be exposed preemptively for long running scenes. The final frame + is saved by default, so that one can more quickly check that the last frame + looks as expected. + """ + pre_config = copy.deepcopy(scene_config) + pre_config["file_writer_config"]["write_to_movie"] = False + pre_config["file_writer_config"]["save_last_frame"] = True + pre_config["skip_animations"] = True + pre_scene = scene_class(**pre_config) + pre_scene.run() + return int(pre_scene.time * scene_config["camera_config"]["frame_rate"]) + + def get_scenes_to_render(scene_classes, scene_config, config): if config["write_all"]: return [sc(**scene_config) for sc in scene_classes] @@ -76,6 +95,9 @@ def get_scenes_to_render(scene_classes, scene_config, config): found = False for scene_class in scene_classes: if scene_class.__name__ == scene_name: + fw_config = scene_config["file_writer_config"] + if fw_config["write_to_movie"]: + fw_config["total_frames"] = compute_total_frames(scene_class, scene_config) scene = scene_class(**scene_config) result.append(scene) found = True diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index bfc011d4..f49d3ad8 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -282,48 +282,42 @@ class Scene(object): self.skip_time += self.time # Methods associated with running animations - def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False): + def get_time_progression(self, run_time, n_iterations=None, desc="", override_skip_animations=False): if self.skip_animations and not override_skip_animations: - times = [run_time] + return [run_time] else: step = 1 / self.camera.frame_rate times = np.arange(0, run_time, step) - time_progression = ProgressDisplay( + + if self.file_writer.has_progress_display: + self.file_writer.set_progress_display_subdescription(desc) + return times + + return ProgressDisplay( times, total=n_iterations, leave=self.leave_progress_bars, - ascii=True if platform.system() == 'Windows' else None + ascii=True if platform.system() == 'Windows' else None, + desc=desc, ) - return time_progression def get_run_time(self, animations): return np.max([animation.run_time for animation in animations]) def get_animation_time_progression(self, animations): run_time = self.get_run_time(animations) - time_progression = self.get_time_progression(run_time) - time_progression.set_description("".join([ - f"Animation {self.num_plays}: {animations[0]}", - ", etc." if len(animations) > 1 else "", - ])) + description = f"{self.num_plays} {animations[0]}" + if len(animations) > 1: + description += ", etc." + time_progression = self.get_time_progression(run_time, desc=description) return time_progression - def get_wait_time_progression(self, duration, stop_condition): + def get_wait_time_progression(self, duration, stop_condition=None): + kw = {"desc": f"{self.num_plays} Waiting"} if stop_condition is not None: - time_progression = self.get_time_progression( - duration, - n_iterations=-1, # So it doesn't show % progress - override_skip_animations=True - ) - time_progression.set_description( - "Waiting for {}".format(stop_condition.__name__) - ) - else: - time_progression = self.get_time_progression(duration) - time_progression.set_description( - "Waiting {}".format(self.num_plays) - ) - return time_progression + kw["n_iterations"] = -1 # So it doesn't show % progress + kw["override_skip_animations"] = True + return self.get_time_progression(duration, **kw) def anims_from_play_args(self, *args, **kwargs): """ @@ -488,13 +482,9 @@ class Scene(object): time_progression.close() break self.unlock_mobject_data() - elif self.skip_animations: - # Do nothing - return self else: self.update_frame(duration) - n_frames = int(duration * self.camera.frame_rate) - for n in range(n_frames): + for n in self.get_wait_time_progression(duration): self.emit_frame() return self diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 3269b416..7bc8b2eb 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -5,6 +5,7 @@ import subprocess as sp import os import sys import platform +from tqdm import tqdm as ProgressDisplay from manimlib.constants import FFMPEG_BIN from manimlib.utils.config_ops import digest_config @@ -35,12 +36,15 @@ class SceneFileWriter(object): "open_file_upon_completion": False, "show_file_location_upon_completion": False, "quiet": False, + "total_frames": 0, + "progress_description_len": 35, } def __init__(self, scene, **kwargs): digest_config(self, kwargs) self.scene = scene self.writing_process = None + self.has_progress_display = False self.init_output_directories() self.init_audio() @@ -205,15 +209,37 @@ class SceneFileWriter(object): command += [self.temp_file_path] self.writing_process = sp.Popen(command, stdin=sp.PIPE) + if self.total_frames > 0: + self.progress_display = ProgressDisplay( + range(self.total_frames), + leave=False, + ascii=True if platform.system() == 'Windows' else None, + desc="Full render: " + ) + self.has_progress_display = True + + def set_progress_display_subdescription(self, desc): + desc_len = self.progress_description_len + full_desc = f"Full render ({desc})" + if len(full_desc) > desc_len: + full_desc = full_desc[:desc_len - 4] + "...)" + else: + full_desc += " " * (desc_len - len(full_desc)) + self.progress_display.set_description(full_desc) + def write_frame(self, camera): if self.write_to_movie: raw_bytes = camera.get_raw_fbo_data() self.writing_process.stdin.write(raw_bytes) + if self.has_progress_display: + self.progress_display.update() def close_movie_pipe(self): self.writing_process.stdin.close() self.writing_process.wait() self.writing_process.terminate() + if self.has_progress_display: + self.progress_display.close() shutil.move(self.temp_file_path, self.final_file_path) def combine_movie_files(self):