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() 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... 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) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 8144dd48..18adce91 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 @@ -121,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()) @@ -139,6 +149,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 +214,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 +327,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 +360,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)] @@ -410,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) @@ -427,14 +454,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(), } @@ -445,6 +476,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") @@ -468,4 +501,5 @@ class Camera(object): class ThreeDCamera(Camera): CONFIG = { "samples": 4, + "anti_alias_width": 0, } diff --git a/manimlib/config.py b/manimlib/config.py index 8992ce5b..258f6ea6 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,30 @@ 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") + + alt_file = file_name.replace(".py", "_inserted_embed.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 +223,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 +257,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 +290,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, @@ -282,7 +317,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 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/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 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): diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index b5e89fc0..1050c3ee 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): @@ -153,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): @@ -308,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) @@ -394,6 +404,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 +412,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 +986,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 +1002,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): @@ -1371,11 +1391,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 @@ -1617,8 +1639,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/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) 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 diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index 277e552c..01abe501 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 - 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() @@ -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) diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index de5ce517..8bc0797e 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() @@ -98,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 diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index 7ec4621c..1160c1ae 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 @@ -161,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() diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index b93ff2ee..e80cfbc9 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 @@ -74,7 +75,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 @@ -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( @@ -150,6 +154,7 @@ class VMobject(Mobject): stroke_rgba=None, stroke_width=None, stroke_background=True, + reflectiveness=None, gloss=None, shadow=None, recurse=True): @@ -177,6 +182,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: @@ -185,10 +192,11 @@ 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(), "shadow": self.get_shadow(), } @@ -218,16 +226,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): @@ -277,9 +286,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() @@ -504,10 +513,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()) @@ -543,12 +552,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, @@ -629,17 +661,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 @@ -701,7 +735,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 @@ -797,7 +831,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 diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 118404be..f49d3ad8 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() @@ -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): 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, } } 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 8315f9ec..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; @@ -75,7 +77,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 +120,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); @@ -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/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; 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 diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 0e8c55e3..9c5e84d2 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -10,6 +10,15 @@ 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): + 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): @@ -147,6 +156,15 @@ def z_to_vector(vector): return rotation_matrix(angle, axis=axis) +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)) + ) + + def angle_of_vector(vector): """ Returns polar coordinate theta when vector is project on xy plane @@ -159,8 +177,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(clip(np.dot(normalize(v1), normalize(v2)), -1, 1)) def project_along_vector(point, vector): @@ -186,14 +203,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)