数学|直观数学-3blue1brown的动画制作

相信很多人都知道3Blue1Brown,这是一个由斯坦福大学的数学系学生Grant Sanderson 创建的YouTube 频道。该频道从独特的视觉角度解说高等数学,内容包括线性代数、微积分、神经网络、黎曼猜想、傅里叶变换以及四元数等等。
本人通过该视频频道获得了很多启发,同时也对其精良的视频制作技术产生了浓厚的兴趣。偶然的机会,得知其在Github上有专门开设了一个动画制作引擎:manim,地址在:
https://github.com/leekunhwee/manim数学|直观数学-3blue1brown的动画制作
文章图片
https://github.com/leekunhwee/manim
数学|直观数学-3blue1brown的动画制作
文章图片

3blue1brown制作的数学解析动画观点高,起点低,把非常复杂的数学原理讲述的非常生动形象,让没有太多数学基础的人也能够感受到数学的美感,今天我们就在UBUNTU18.04上,尝试安装一下3B1B的动画制作环境,说不定某天会用到。
安装环境:

  • Ubuntu 18.04.5 LTS
  • Anaconda Python 3.8.5
数学|直观数学-3blue1brown的动画制作
文章图片

安装过程:
下载manim
git clone https://github.com/leekunhwee/manim.git

数学|直观数学-3blue1brown的动画制作
文章图片

安装依赖,ffmpeg
这一步通过apt-get安装FFMPEG预编译包或者从源码开始编译都可以,从源码安装可以参考
ubuntu18.04编译FFMPEG_tugouxp的专栏-CSDN博客数学|直观数学-3blue1brown的动画制作
文章图片
https://blog.csdn.net/tugouxp/article/details/115491843安装依赖,miktex
Getting MiKTeX
下载地址页面有安装说明,按照说明安装即可。
数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

最后执行miktexsetup finish
数学|直观数学-3blue1brown的动画制作
文章图片

验证安装依赖包的版本信息
数学|直观数学-3blue1brown的动画制作
文章图片

安装python依赖包:
python -m pip install -r requirements.txt

数学|直观数学-3blue1brown的动画制作
文章图片

(base) caozilong@caozilong-Vostro-3268:~/mimal/manim$ python -m pip install -r requirements.txt Ignoring pycairo: markers 'sys_platform == "win32"' don't match your environment Ignoring pyreadline: markers 'sys_platform == "win32"' don't match your environment Collecting argparse Downloading argparse-1.4.0-py2.py3-none-any.whl (23 kB) Collecting colour Downloading colour-0.1.5-py2.py3-none-any.whl (23 kB) Requirement already satisfied: numpy in /home/caozilong/anaconda3/lib/python3.8/site-packages (from -r requirements.txt (line 3)) (1.19.2) Requirement already satisfied: Pillow in /home/caozilong/anaconda3/lib/python3.8/site-packages (from -r requirements.txt (line 4)) (8.0.1) Collecting progressbar Downloading progressbar-2.5.tar.gz (10 kB) Requirement already satisfied: scipy in /home/caozilong/anaconda3/lib/python3.8/site-packages (from -r requirements.txt (line 6)) (1.5.2) Requirement already satisfied: tqdm in /home/caozilong/anaconda3/lib/python3.8/site-packages (from -r requirements.txt (line 7)) (4.50.2) Requirement already satisfied: opencv-python in /home/caozilong/anaconda3/lib/python3.8/site-packages (from -r requirements.txt (line 8)) (4.5.3.56) Collecting pycairo==1.17.1 Downloading pycairo-1.17.1.tar.gz (194 kB) |████████████████████████████████| 194 kB 453 kB/s Collecting pydub==0.23.0 Downloading pydub-0.23.0-py2.py3-none-any.whl (28 kB) Building wheels for collected packages: progressbar, pycairo Building wheel for progressbar (setup.py) ... done Created wheel for progressbar: filename=progressbar-2.5-py3-none-any.whl size=12074 sha256=11707eec90e81c753d7d715ef81831bd0c497ff6a0322fd68fda3ce62789b021 Stored in directory: /home/caozilong/.cache/pip/wheels/2c/67/ed/d84123843c937d7e7f5ba88a270d11036473144143355e2747 Building wheel for pycairo (setup.py) ... done Created wheel for pycairo: filename=pycairo-1.17.1-cp38-cp38-linux_x86_64.whl size=258204 sha256=a72ee395c86fa713bad8c6df805187eaee0019fb47831de54b3bcbceeb37987f Stored in directory: /home/caozilong/.cache/pip/wheels/92/a5/7c/b88429bb8e47045f531dd3b5dedf5c4202c2750b502c29fb29 Successfully built progressbar pycairo Installing collected packages: argparse, colour, progressbar, pycairo, pydub Successfully installed argparse-1.4.0 colour-0.1.5 progressbar-2.5 pycairo-1.17.1 pydub-0.23.0 (base) caozilong@caozilong-Vostro-3268:~/mimal/manim$

conda install pycairo
(base) caozilong@caozilong-Vostro-3268:~/mimal/manim$ conda install pycairo Collecting package metadata (current_repodata.json): done Solving environment: - The environment is inconsistent, please check the package plan carefully The following packages are causing the inconsistency:- defaults/linux-64::anaconda==2020.11=py38_0 - defaults/linux-64::spyder==4.1.5=py38_0 - defaults/linux-64::astroid==2.4.2=py38_0 - defaults/noarch::python-language-server==0.35.1=py_0 - defaults/linux-64::pylint==2.6.0=py38\ done## Package Plan ##environment location: /home/caozilong/anaconda3added / updated specs: - pycairoThe following packages will be downloaded:package|build ---------------------------|----------------- _anaconda_depends-2020.07|py38_06 KB anaconda-custom|py38_135 KB astroid-2.5|py38h06a4308_1284 KB ca-certificates-2021.7.5|h06a4308_1113 KB certifi-2021.5.30|py38h06a4308_0138 KB conda-4.10.3|py38h06a4308_02.9 MB libllvm9-9.0.1|h4a3c616_121.0 MB openssl-1.1.1l|h7f8727e_02.5 MB pycairo-1.19.1|py38h708ec4a_073 KB snappy-1.1.8|he6710b0_040 KB wrapt-1.12.1|py38h7b6447c_150 KB ------------------------------------------------------------ Total:27.1 MBThe following NEW packages will be INSTALLED:_anaconda_dependspkgs/main/linux-64::_anaconda_depends-2020.07-py38_0 h5pypkgs/main/linux-64::h5py-2.10.0-py38h7918eee_0 libllvm9pkgs/main/linux-64::libllvm9-9.0.1-h4a3c616_1 pycairopkgs/main/linux-64::pycairo-1.19.1-py38h708ec4a_0 snappypkgs/main/linux-64::snappy-1.1.8-he6710b0_0 wraptpkgs/main/linux-64::wrapt-1.12.1-py38h7b6447c_1The following packages will be UPDATED:astroid2.4.2-py38_0 --> 2.5-py38h06a4308_1 ca-certificates2020.10.14-0 --> 2021.7.5-h06a4308_1 certifipkgs/main/noarch::certifi-2020.6.20-p~ --> pkgs/main/linux-64::certifi-2021.5.30-py38h06a4308_0 conda4.9.2-py38h06a4308_0 --> 4.10.3-py38h06a4308_0 openssl1.1.1h-h7b6447c_0 --> 1.1.1l-h7f8727e_0The following packages will be DOWNGRADED:anaconda2020.11-py38_0 --> custom-py38_1Proceed ([y]/n)? yDownloading and Extracting Packages anaconda-custom| 35 KB| ##################################### | 100% openssl-1.1.1l| 2.5 MB| ##################################### | 100% wrapt-1.12.1| 50 KB| ##################################### | 100% certifi-2021.5.30| 138 KB| ##################################### | 100% pycairo-1.19.1| 73 KB| ##################################### | 100% ca-certificates-2021 | 113 KB| ##################################### | 100% astroid-2.5| 284 KB| ##################################### | 100% _anaconda_depends-20 | 6 KB| ##################################### | 100% snappy-1.1.8| 40 KB| ##################################### | 100% libllvm9-9.0.1| 21.0 MB| ##################################### | 100% conda-4.10.3| 2.9 MB| ##################################### | 100% Preparing transaction: done Verifying transaction: done Executing transaction: done (base) caozilong@caozilong-Vostro-3268:~/mimal/manim$

验证用例:
python -m manim example_scenes.py SquareToCircle -pl

数学|直观数学-3blue1brown的动画制作
文章图片

python -m manim example_scenes.py WarpSquare -pl

数学|直观数学-3blue1brown的动画制作
文章图片

以一个例子说明3B1B的动画制作原理
from manimlib.imports import * import os import pyclbrclass Shapes(Scene): #A few simple shapes #Python 2.7 version runs in Python 3.7 without changes def construct(self): circle = Circle() square = Square() line=Line(np.array([3,0,0]),np.array([5,0,0])) triangle=Polygon(np.array([0,0,0]),np.array([1,1,0]),np.array([1,-1,0]))self.play(ShowCreation(circle)) self.play(FadeOut(circle)) self.play(GrowFromCenter(square)) self.play(Transform(square,triangle)) self.add(line)class MoreShapes(Scene): #A few more simple shapes #2.7 version runs in 3.7 without any changes #Note: I fixed my 'play command not found' issue by installing sox def construct(self): circle = Circle(color=PURPLE_A) square = Square(fill_color=GOLD_B, fill_opacity=1, color=GOLD_A) square.move_to(UP+LEFT) circle.surround(square) rectangle = Rectangle(height=2, width=3) ellipse=Ellipse(width=3, height=1, color=RED) ellipse.shift(2*DOWN+2*RIGHT) pointer = CurvedArrow(2*RIGHT,5*RIGHT,color=MAROON_C) arrow = Arrow(LEFT,UP) arrow.next_to(circle,DOWN+LEFT) rectangle.next_to(arrow,DOWN+LEFT) ring=Annulus(inner_radius=.5, outer_radius=1, color=BLUE) ring.next_to(ellipse, RIGHT)self.add(pointer) self.play(FadeIn(square)) self.play(Rotating(square),FadeIn(circle)) self.play(GrowArrow(arrow)) self.play(GrowFromCenter(rectangle), GrowFromCenter(ellipse), GrowFromCenter(ring))class MovingShapes(Scene): #Show the difference between .shift() and .move_to def construct(self): circle=Circle(color=TEAL_A) circle.move_to(LEFT) square=Circle() square.move_to(LEFT+3*DOWN)self.play(GrowFromCenter(circle), GrowFromCenter(square), rate=5) self.play(ApplyMethod(circle.move_to,RIGHT), ApplyMethod(square.shift,RIGHT)) self.play(ApplyMethod(circle.move_to,RIGHT+UP), ApplyMethod(square.shift,RIGHT+UP)) self.play(ApplyMethod(circle.move_to,LEFT+UP), ApplyMethod(square.shift,LEFT+UP))class AddingText(Scene): #Adding text on the screen def construct(self): my_first_text=TextMobject("Writing with manim is fun") second_line=TextMobject("and easy to do!") second_line.next_to(my_first_text,DOWN) third_line=TextMobject("for me and you!") third_line.next_to(my_first_text,DOWN)self.add(my_first_text, second_line) self.wait(2) self.play(Transform(second_line,third_line)) self.wait(2) second_line.shift(3*DOWN) self.play(ApplyMethod(my_first_text.shift,3*UP)) ###Try uncommenting the following### #self.play(ApplyMethod(second_line.move_to, LEFT_SIDE-2*LEFT)) #self.play(ApplyMethod(my_first_text.next_to,second_line))class AddingMoreText(Scene): #Playing around with text properties def construct(self): quote = TextMobject("Imagination is more important than knowledge") quote.set_color(RED) quote.to_edge(UP) quote2 = TextMobject("A person who never made a mistake never tried anything new") quote2.set_color(YELLOW) author=TextMobject("-Albert Einstein") author.scale(0.75) author.next_to(quote.get_corner(DOWN+RIGHT),DOWN)self.add(quote) self.add(author) self.wait(2) self.play(Transform(quote,quote2),ApplyMethod(author.move_to,quote2.get_corner(DOWN+RIGHT)+DOWN+2*LEFT)) self.play(ApplyMethod(author.scale,1.5)) author.match_color(quote2) self.play(FadeOut(quote))class RotateAndHighlight(Scene): #Rotation of text and highlighting with surrounding geometries def construct(self): square=Square(side_length=5,fill_color=YELLOW, fill_opacity=1) label=TextMobject("Text at an angle") label.bg=BackgroundRectangle(label,fill_opacity=1) label_group=VGroup(label.bg,label)#Order matters label_group.rotate(TAU/8) label2=TextMobject("Boxed text",color=BLACK) label2.bg=SurroundingRectangle(label2,color=BLUE,fill_color=RED, fill_opacity=.5) label2_group=VGroup(label2,label2.bg) label2_group.next_to(label_group,DOWN) label3=TextMobject("Rainbow") label3.scale(2) label3.set_color_by_gradient(RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE) label3.to_edge(DOWN)self.add(square) self.play(FadeIn(label_group)) self.play(FadeIn(label2_group)) self.play(FadeIn(label3))class BasicEquations(Scene): #A short script showing how to use Latex commands def construct(self): eq1=TextMobject("$\\vec{X}_0 \\cdot \\vec{Y}_1 = 3$") eq1.shift(2*UP) eq2=TexMobject(r"\vec{F}_{net} = \sum_i \vec{F}_i") eq2.shift(2*DOWN)self.play(Write(eq1)) self.play(Write(eq2))class ColoringEquations(Scene): #Grouping and coloring parts of equations def construct(self): line1=TexMobject(r"\text{The vector } \vec{F}_{net} \text{ is the net }",r"\text{force }",r"\text{on object of mass }") line1.set_color_by_tex("force", BLUE) line2=TexMobject("m", "\\text{ and acceleration }", "\\vec{a}", ".") line2.set_color_by_tex_to_color_map({ "m": YELLOW, "{a}": RED }) sentence=VGroup(line1,line2) sentence.arrange_submobjects(DOWN, buff=MED_LARGE_BUFF) self.play(Write(sentence))class UsingBraces(Scene): #Using braces to group text together def construct(self): eq1A = TextMobject("4x + 3y") eq1B = TextMobject("=") eq1C = TextMobject("0") eq2A = TextMobject("5x -2y") eq2B = TextMobject("=") eq2C = TextMobject("3") eq1B.next_to(eq1A,RIGHT) eq1C.next_to(eq1B,RIGHT) eq2A.shift(DOWN) eq2B.shift(DOWN) eq2C.shift(DOWN) eq2A.align_to(eq1A,LEFT) eq2B.align_to(eq1B,LEFT) eq2C.align_to(eq1C,LEFT)eq_group=VGroup(eq1A,eq2A) braces=Brace(eq_group,LEFT) eq_text = braces.get_text("A pair of equations")self.add(eq1A, eq1B, eq1C) self.add(eq2A, eq2B, eq2C) self.play(GrowFromCenter(braces),Write(eq_text))class UsingBracesConcise(Scene): #A more concise block of code with all columns aligned def construct(self): eq1_text=["4","x","+","3","y","=","0"] eq2_text=["5","x","-","2","y","=","3"] eq1_mob=TexMobject(*eq1_text) eq2_mob=TexMobject(*eq2_text) eq1_mob.set_color_by_tex_to_color_map({ "x":RED_B, "y":GREEN_C }) eq2_mob.set_color_by_tex_to_color_map({ "x":RED_B, "y":GREEN_C }) for i,item in enumerate(eq2_mob): item.align_to(eq1_mob[i],LEFT) eq1=VGroup(*eq1_mob) eq2=VGroup(*eq2_mob) eq2.shift(DOWN) eq_group=VGroup(eq1,eq2) braces=Brace(eq_group,LEFT) eq_text = braces.get_text("A pair of equations")self.play(Write(eq1),Write(eq2)) self.play(GrowFromCenter(braces),Write(eq_text))class PlotFunctions(GraphScene): CONFIG = { "x_min" : -10, "x_max" : 10.3, "y_min" : -1.5, "y_max" : 1.5, "graph_origin" : ORIGIN , "function_color" : RED , "axes_color" : GREEN, "x_labeled_nums" :range(-10,12,2),} def construct(self): self.setup_axes(animate=True) func_graph=self.get_graph(self.func_to_graph,self.function_color) func_graph2=self.get_graph(self.func_to_graph2) vert_line = self.get_vertical_line_to_graph(TAU,func_graph,color=YELLOW) graph_lab = self.get_graph_label(func_graph, label = "\\cos(x)") graph_lab2=self.get_graph_label(func_graph2,label = "\\sin(x)", x_val=-10, direction=UP/2) two_pi = TexMobject("x = 2 \\pi") label_coord = self.input_to_graph_point(TAU,func_graph) two_pi.next_to(label_coord,RIGHT+UP)self.play(ShowCreation(func_graph),ShowCreation(func_graph2)) self.play(ShowCreation(vert_line), ShowCreation(graph_lab), ShowCreation(graph_lab2),ShowCreation(two_pi))def func_to_graph(self,x): return np.cos(x)def func_to_graph2(self,x): return np.sin(x)class ExampleApproximation(GraphScene): CONFIG = { "function" : lambda x : np.cos(x), "function_color" : BLUE, "taylor" : [lambda x: 1, lambda x: 1-x**2/2, lambda x: 1-x**2/math.factorial(2)+x**4/math.factorial(4), lambda x: 1-x**2/2+x**4/math.factorial(4)-x**6/math.factorial(6), lambda x: 1-x**2/math.factorial(2)+x**4/math.factorial(4)-x**6/math.factorial(6)+x**8/math.factorial(8), lambda x: 1-x**2/math.factorial(2)+x**4/math.factorial(4)-x**6/math.factorial(6)+x**8/math.factorial(8) - x**10/math.factorial(10)], "center_point" : 0, "approximation_color" : GREEN, "x_min" : -10, "x_max" : 10, "y_min" : -1, "y_max" : 1, "graph_origin" : ORIGIN , "x_labeled_nums" :range(-10,12,2),} def construct(self): self.setup_axes(animate=True) func_graph = self.get_graph( self.function, self.function_color, ) approx_graphs = [ self.get_graph( f, self.approximation_color ) for f in self.taylor ]term_num = [ TexMobject("n = " + str(n),aligned_edge=TOP) for n in range(0,8)] #[t.to_edge(BOTTOM,buff=SMALL_BUFF) for t in term_num]#term = TexMobject("") #term.to_edge(BOTTOM,buff=SMALL_BUFF) term = VectorizedPoint(3*DOWN)approx_graph = VectorizedPoint( self.input_to_graph_point(self.center_point, func_graph) )self.play( ShowCreation(func_graph), ) for n,graph in enumerate(approx_graphs): self.play( Transform(approx_graph, graph, run_time = 2), Transform(term,term_num[n]) ) self.wait()class DrawAnAxis(Scene): CONFIG = { "plane_kwargs" : { "x_line_frequency" : 2, "y_line_frequency" :2 } }def construct(self): my_plane = NumberPlane(**self.plane_kwargs) my_plane.add(my_plane.get_axis_labels()) self.add(my_plane) #self.wait()class SimpleField(Scene): CONFIG = { "plane_kwargs" : { "color" : RED }, } def construct(self): plane = NumberPlane(**self.plane_kwargs) #Create axes and grid plane.add(plane.get_axis_labels())#add x and y label self.add(plane)#Place grid on screenpoints = [x*RIGHT+y*UP for x in np.arange(-5,5,1) for y in np.arange(-5,5,1) ]#List of vectors pointing to each grid pointvec_field = []#Empty list to use in for loop for point in points: field = 0.5*RIGHT + 0.5*UP#Constant field up and to right result = Vector(field).shift(point)#Create vector and shift it to grid point vec_field.append(result)#Append to listdraw_field = VGroup(*vec_field)#Pass list of vectors to create a VGroupself.play(ShowCreation(draw_field))#Draw VGroup on screenclass FieldWithAxes(Scene): CONFIG = { "plane_kwargs" : { "color" : RED_B }, "point_charge_loc" : 0.5*RIGHT-1.5*UP, } def construct(self): plane = NumberPlane(**self.plane_kwargs) #plane.main_lines.fade(.9)#doesn't work in most recent commit plane.add(plane.get_axis_labels()) self.add(plane)field = VGroup(*[self.calc_field(x*RIGHT+y*UP) for x in np.arange(-9,9,1) for y in np.arange(-5,5,1) ])self.play(ShowCreation(field))def calc_field(self,point): #This calculates the field at a single point. x,y = point[:2] Rx,Ry = self.point_charge_loc[:2] r = math.sqrt((x-Rx)**2 + (y-Ry)**2) efield = (point - self.point_charge_loc)/r**3 #efield = np.array((-y,x,0))/math.sqrt(x**2+y**2)#Try one of these two fields #efield = np.array(( -2*(y%2)+1 , -2*(x%2)+1 , 0 ))/3#Try one of these two fields return Vector(efield).shift(point)class ExampleThreeD(ThreeDScene): CONFIG = { "plane_kwargs" : { "color" : RED_B }, "point_charge_loc" : 0.5*RIGHT-1.5*UP, } def construct(self): plane = NumberPlane(**self.plane_kwargs) #plane.main_lines.fade(.9)#Doesn't work in most recent commit plane.add(plane.get_axis_labels()) self.add(plane)field2D = VGroup(*[self.calc_field2D(x*RIGHT+y*UP) for x in np.arange(-9,9,1) for y in np.arange(-5,5,1) ])self.set_camera_orientation(phi=PI/3,gamma=PI/5) self.play(ShowCreation(field2D)) self.wait() #self.move_camera(gamma=0,run_time=1)#Doesn't work in most recent commit #self.move_camera(phi=3/4*PI, theta=-PI/2)#Doesn't work in most recent commit self.begin_ambient_camera_rotation(rate=0.1) self.wait(6)def calc_field2D(self,point): x,y = point[:2] Rx,Ry = self.point_charge_loc[:2] r = math.sqrt((x-Rx)**2 + (y-Ry)**2) efield = (point - self.point_charge_loc)/r**3 return Vector(efield).shift(point)class EFieldInThreeD(ThreeDScene): CONFIG = { "plane_kwargs" : { "color" : RED_B }, "point_charge_loc" : 0.5*RIGHT-1.5*UP, } def construct(self): plane = NumberPlane(**self.plane_kwargs) #plane.main_lines.fade(.9)#Doesn't work in most recent commit plane.add(plane.get_axis_labels()) self.add(plane)field2D = VGroup(*[self.calc_field2D(x*RIGHT+y*UP) for x in np.arange(-9,9,1) for y in np.arange(-5,5,1) ])field3D = VGroup(*[self.calc_field3D(x*RIGHT+y*UP+z*OUT) for x in np.arange(-9,9,1) for y in np.arange(-5,5,1) for z in np.arange(-5,5,1)])self.play(ShowCreation(field3D)) self.wait() #self.move_camera(0.8*np.pi/2, -0.45*np.pi)#Doesn't work in most recent commit self.begin_ambient_camera_rotation() self.wait(6)def calc_field2D(self,point): x,y = point[:2] Rx,Ry = self.point_charge_loc[:2] r = math.sqrt((x-Rx)**2 + (y-Ry)**2) efield = (point - self.point_charge_loc)/r**3 return Vector(efield).shift(point)def calc_field3D(self,point): x,y,z = point Rx,Ry,Rz = self.point_charge_loc r = math.sqrt((x-Rx)**2 + (y-Ry)**2+(z-Rz)**2) efield = (point - self.point_charge_loc)/r**3 #efield = np.array((-y,x,z))/math.sqrt(x**2+y**2+z**2) return Vector(efield).shift(point)class MovingCharges(Scene): CONFIG = { "plane_kwargs" : { "color" : RED_B }, "point_charge_loc" : 0.5*RIGHT-1.5*UP, } def construct(self): plane = NumberPlane(**self.plane_kwargs) #plane.main_lines.fade(.9)#Doesn't work in most recent commit plane.add(plane.get_axis_labels()) self.add(plane)field = VGroup(*[self.calc_field(x*RIGHT+y*UP) for x in np.arange(-9,9,1) for y in np.arange(-5,5,1) ]) self.field=field source_charge = self.Positron().move_to(self.point_charge_loc) self.play(FadeIn(source_charge)) self.play(ShowCreation(field)) self.moving_charge()def calc_field(self,point): x,y = point[:2] Rx,Ry = self.point_charge_loc[:2] r = math.sqrt((x-Rx)**2 + (y-Ry)**2) efield = (point - self.point_charge_loc)/r**3 return Vector(efield).shift(point)def moving_charge(self): numb_charges=4 possible_points = [v.get_start() for v in self.field] points = random.sample(possible_points, numb_charges) particles = VGroup(*[ self.Positron().move_to(point) for point in points ]) for particle in particles: particle.velocity = np.array((0,0,0))self.play(FadeIn(particles)) self.moving_particles = particles self.add_foreground_mobjects(self.moving_particles ) self.always_continually_update = True self.wait(10)def field_at_point(self,point): x,y = point[:2] Rx,Ry = self.point_charge_loc[:2] r = math.sqrt((x-Rx)**2 + (y-Ry)**2) efield = (point - self.point_charge_loc)/r**3 return efielddef continual_update(self, *args, **kwargs): if hasattr(self, "moving_particles"): dt = self.frame_duration for p in self.moving_particles: accel = self.field_at_point(p.get_center()) p.velocity = p.velocity + accel*dt p.shift(p.velocity*dt)class Positron(Circle): CONFIG = { "radius" : 0.2, "stroke_width" : 3, "color" : RED, "fill_color" : RED, "fill_opacity" : 0.5, } def __init__(self, **kwargs): Circle.__init__(self, **kwargs) plus = TexMobject("+") plus.scale(0.7) plus.move_to(self) self.add(plus)class FieldOfMovingCharge(Scene): CONFIG = { "plane_kwargs" : { "color" : RED_B }, "point_charge_start_loc" : 5.5*LEFT-1.5*UP, } def construct(self): plane = NumberPlane(**self.plane_kwargs) #plane.main_lines.fade(.9)#Doesn't work in most recent commit plane.add(plane.get_axis_labels()) self.add(plane)field = VGroup(*[self.create_vect_field(self.point_charge_start_loc,x*RIGHT+y*UP) for x in np.arange(-9,9,1) for y in np.arange(-5,5,1) ]) self.field=field self.source_charge = self.Positron().move_to(self.point_charge_start_loc) self.source_charge.velocity = np.array((1,0,0)) self.play(FadeIn(self.source_charge)) self.play(ShowCreation(field)) self.moving_charge()def create_vect_field(self,source_charge,observation_point): return Vector(self.calc_field(source_charge,observation_point)).shift(observation_point)def calc_field(self,source_point,observation_point): x,y,z = observation_point Rx,Ry,Rz = source_point r = math.sqrt((x-Rx)**2 + (y-Ry)**2 + (z-Rz)**2) if r<0.0000001:#Prevent divide by zero efield = np.array((0,0,0)) else: efield = (observation_point - source_point)/r**3 return efielddef moving_charge(self): numb_charges=3 possible_points = [v.get_start() for v in self.field] points = random.sample(possible_points, numb_charges) particles = VGroup(self.source_charge, *[ self.Positron().move_to(point) for point in points ]) for particle in particles[1:]: particle.velocity = np.array((0,0,0)) self.play(FadeIn(particles[1:])) self.moving_particles = particles self.add_foreground_mobjects(self.moving_particles ) self.always_continually_update = True self.wait(10)def continual_update(self, *args, **kwargs): Scene.continual_update(self, *args, **kwargs) if hasattr(self, "moving_particles"): dt = self.frame_durationfor v in self.field: field_vect=np.zeros(3) for p in self.moving_particles: field_vect = field_vect + self.calc_field(p.get_center(), v.get_start()) v.put_start_and_end_on(v.get_start(), field_vect+v.get_start())for p in self.moving_particles: accel = np.zeros(3) p.velocity = p.velocity + accel*dt p.shift(p.velocity*dt)class Positron(Circle): CONFIG = { "radius" : 0.2, "stroke_width" : 3, "color" : RED, "fill_color" : RED, "fill_opacity" : 0.5, } def __init__(self, **kwargs): Circle.__init__(self, **kwargs) plus = TexMobject("+") plus.scale(0.7) plus.move_to(self) self.add(plus)HEAD_INDEX= 0 BODY_INDEX= 1 ARMS_INDEX= 2 LEGS_INDEX= 3class StickMan(SVGMobject): CONFIG = { "color" : BLUE_E, "file_name_prefix": "stick_man", "stroke_width" : 2, "stroke_color" : WHITE, "fill_opacity" : 1.0, "height" : 3, } def __init__(self, mode = "plain", **kwargs): digest_config(self, kwargs) self.mode = mode self.parts_named = False try: svg_file = os.path.join( SVG_IMAGE_DIR, "%s_%s.svg" % (self.file_name_prefix, mode) ) SVGMobject.__init__(self, file_name=svg_file, **kwargs) except: warnings.warn("No %s design with mode %s" % (self.file_name_prefix, mode)) svg_file = os.path.join( SVG_IMAGE_DIR, "stick_man_plain.svg", ) SVGMobject.__init__(self, mode="plain", file_name=svg_file, **kwargs)def name_parts(self): self.head = self.submobjects[HEAD_INDEX] self.body = self.submobjects[BODY_INDEX] self.arms = self.submobjects[ARMS_INDEX] self.legs = self.submobjects[LEGS_INDEX] self.parts_named = Truedef init_colors(self): SVGMobject.init_colors(self) if not self.parts_named: self.name_parts() self.head.set_fill(self.color, opacity = 1) self.body.set_fill(RED, opacity = 1) self.arms.set_fill(YELLOW, opacity = 1) self.legs.set_fill(BLUE, opacity = 1) return selfclass Waving(Scene): def construct(self): start_man = StickMan() plain_man = StickMan() waving_man = StickMan("wave")self.add(start_man) self.wait() self.play(Transform(start_man,waving_man)) self.play(Transform(start_man,plain_man))self.wait()class CirclesAndSquares(SVGMobject): CONFIG = { "color" : BLUE_E, "file_name_prefix": "circles_and_squares", "stroke_width" : 2, "stroke_color" : WHITE, "fill_opacity" : 1.0, "height" : 3, "start_corner" : None, "circle_index" : 0, "line1_index" :1, "line2_index" : 2, "square1_index" : 3, "square2_index" : 4, } def __init__(self, mode = "plain", **kwargs): digest_config(self, kwargs) self.mode = mode self.parts_named = False try: svg_file = os.path.join( SVG_IMAGE_DIR, "%s_%s.svg" % (self.file_name_prefix, mode) ) SVGMobject.__init__(self, file_name=svg_file, **kwargs) except: warnings.warn("No %s design with mode %s" % (self.file_name_prefix, mode)) svg_file = os.path.join( SVG_IMAGE_DIR, "circles_and_squares_plain.svg", ) SVGMobject.__init__(self, mode="plain", file_name=svg_file, **kwargs)def name_parts(self): self.circle = self.submobjects[self.circle_index] self.line1 = self.submobjects[self.line1_index] self.line2 = self.submobjects[self.line2_index] self.square1 = self.submobjects[self.square1_index] self.square2 = self.submobjects[self.square2_index] self.parts_named = Truedef init_colors(self): SVGMobject.init_colors(self) self.name_parts() self.circle.set_fill(RED, opacity = 1) self.line1.set_fill(self.color, opacity = 0) self.line2.set_fill(self.color, opacity = 0) self.square1.set_fill(GREEN, opacity = 1) self.square2.set_fill(BLUE, opacity = 1) return selfclass SVGCircleAndSquare(Scene): def construct(self): thingy = CirclesAndSquares()self.add(thingy) self.wait()if __name__ == "__main__": # Call this file at command line to make sure all scenes work with version of manim # type "python manim_tutorial_P37.py" at command line to run all scenes in this file #Must have "import os" and"import pyclbr" at start of file to use this ###Using Python class browser to determine which classes are defined in this file module_name = 'manim_tutorial_P37'#Name of current file module_info = pyclbr.readmodule(module_name)for item in module_info.values(): if item.module==module_name: print(item.name) os.system("python -m manim manim_tutorial_P37.py %s -l" % item.name)#Does not play files

执行测试
python -m manim manim_tutorial_P37.py MoreShapes -pl

效果:
数学|直观数学-3blue1brown的动画制作
文章图片

还有其他一些例子,比如:
数学|直观数学-3blue1brown的动画制作
文章图片

支持字体:
此时的环境不支持字体显示,执行带有字体处理的用例会失败,所以接下来需要支持字体处理
安装字体文件:
  1. physics.sty:https://mirrors.ctan.org/macros/latex/contrib/physics/physics.sty
  2. dsfont.sty:
将这两个文件下载到:
/usr/share/texlive/texmf-dist/tex/latex/physics/physics.sty

/usr/share/texlive/texmf-dist/tex/latex/dsfont/
目录
最后执行sudo mktexlsr,如下图所示:
数学|直观数学-3blue1brown的动画制作
文章图片

OK,现在可以支持字体显示了,我们重新运行用例:
python -m manim manim_tutorial_P37.py PlotFunctions-pl

数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

manim_tutorial_P37.py中有21个用例,安装字体后都可以运行
数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

【数学|直观数学-3blue1brown的动画制作】数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片



数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片


数学|直观数学-3blue1brown的动画制作
文章图片

数学|直观数学-3blue1brown的动画制作
文章图片

高端一些的例子:
git clone https://github.com/3b1b/manim.git

数学|直观数学-3blue1brown的动画制作
文章图片

pip install -e .
数学|直观数学-3blue1brown的动画制作
文章图片

验证命令:
manimgl example_scenes.py OpeningManimExample
或者
manim-render example_scenes.py OpeningManimExample
效果:

总结:
3B1B 动画的制作思路是:根据自己想在场景中展现的内容和效果编写一系列的类,然后通过命令行对每个类进行实例化,前面输入的测试命令其实就包含了类的实例化过程,而每个类被实例化后都将得到一个动画片段,通过视频制作软件将各个片段衔接起来并配音,就能得到大家喜闻乐见的 3B1B 教学动画了。
结束!

    推荐阅读