这是我上一篇
manim第一阶段总结
的后续篇,如果没有看过前一篇,建议先去看一看
前言
本篇文章主要介绍的内容分为
绘制函数曲线、初识三维场景、使用SVG矢量图层
三部分,学完这三个部分之后,我们对manim的入门就算结束了,这个时候我们就已经能够做出一些有模有样的小作品了,同时也拥有了独自继续深入manim的能力。同时,这次的总结将不是对具体技术细节的总结,取而代之的是我这次的学习感悟。
绘制函数曲线
这个部分又分为三个部分:设置笛卡尔坐标系、确定要绘制的函数、将函数绘制在笛卡尔坐标系中
设置笛卡尔坐标系
这个部分有两个方面:设置坐标轴、设置视频网格
1、设置坐标轴
我们来看个小例子
class PlotFunctions(GraphScene):
CONFIG = {
"x_min" : 0,
"x_max" : 10.3,
"x_axis_width": 9,
"x_tick_frequency": 1,
"x_labeled_nums" :range(0,11,2),
"x_axis_label": "$x$",
"y_min" : 0,
"y_max" : 2,
"y_axis_height": 6,
"y_tick_frequency": 1,
"y_labeled_nums" :range(0, 3, 1),
"exclude_zero_label": True,
"graph_origin" : ORIGIN ,
"function_color" : RED ,
"axes_color" : BLUE,
"graph_origin": 2 * DOWN + 4 * LEFT,
}
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)", x_val=10)
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, run_time = 5),ShowCreation(func_graph2, run_time = 5))
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)
我们可以看到,这个场景子类不是继承于Scene,而是继承于GraphScene,GraphScene是继承于Scene,GraphScene是专门用于绘制曲线的绘图场景类。我们不妨进去看看(自己去看看),如果你进去看了,就会知道GraphScene存放在一个单独的graph_scene.py文件中,里面定义了许多和坐标系相关的方法,具体的细节,如果感兴趣而且时间很充裕就去看一看(我就先不看了),我们直接说明如何利用这个GraphScene来实现带有横纵坐标轴的场景。
在PlotFunctions中我们会发现一个CONFIG字典,这个字典里是关于坐标轴设置的参数,这些参数在GraphScene中都可以找到,
通过在GraphScene的子类PlotFunctions中重写这些参数
,我们就可以覆盖GraphScene中的默认参数,从而达到设置坐标轴的目的。没有被重写的都会按照默认的参数设置。下面是这些参数的含义
-
x_axis_width:x轴所占的屏幕宽度,在前一篇文章中,我们讲过,视频高度默认被等分为八份,宽度按照与高度相同的分割尺寸被分割,
比如高10,被分割成五分,那么宽20,就会被分割成10份
。 - x_min、x_max:坐标轴的最大最小数值
- x_tick_frequency:坐标轴刻度频率
- x_labeled_nums:显示坐标轴上的数值
-
x_axis_label:坐标轴的标签
y具有类似的含义,不再赘述
- graph_origin:坐标轴原点所在位置
剩下的一些参数的效果,小伙伴们可以自己尝试一下,记住,一定要自己动手尝试哦!!
参数都设置完之后,我们通过调用self.setup_axes(animate=True)这个函数便可以实现坐标轴的显示了,animate=True是用来展示坐标轴出现动画的。
2、设置视频网格
从一个小例子看起
class DrawAnAxis(Scene):
CONFIG = { "plane_kwargs" : {
"x_line_frequency" : 0.5,
"y_line_frequency" : 0.5
}
}
def construct(self):
my_plane = NumberPlane(**self.plane_kwargs)
my_plane.add(my_plane.get_axis_labels())
self.play(Write(my_plane, run_time = 5))
视频网格的设置目标是
在视频中设置网格线
,这个目标是通过NumberPlane这个类实现的,我们看到这个类接收了一个参数**self.plane_kwargs。到这里,如果你有心的话,你会发现一个神奇的地方,self可以直接跨过CONFIG这个字典直接使用”plane_kwargs”,
在这里需要解释一下,在每一个scene或者mobject被创建的时候,有一个叫做digest_config()(这个函数在Container的构造函数中被调用)的函数会被调用,这个函数会从子类逐级往上检查每一个类中的CONFIG字典,将所有检查到的键值对,都设置为类属性,对于相同的属性,子类可以覆盖父类。,所以,我们这里可以直接使用self调用CONFIG里面的键值对。
回到主线,我们把要设置的属性以字典的形式存放在plane_kwargs中,然后一起传递给NumberPlane,这是我们设置网格属性的方法,然后会实例化一个带有我们想要属性的网格对象my_plane,在实例化之后,网格还可以进行一些其它的设置,比如这里,我们给网格添加了x和y坐标轴。然后我们就可以使用我们在上一篇文章中讲过的各种对象展示的方法将网格添加到视频中。下面介绍一下,网格参数的意义,我就直接在函数中打注释(
没注释是它的一个难点,但没注释我却能很容易看懂,作者太强了
)了。
CONFIG = {
"axis_config": {
"stroke_color": RED,#坐标轴颜色
"stroke_width": 2,#坐标轴颜色
"include_ticks": False,#自己去试一下,我不知道怎么形容那种效果
"include_tip": False,#为坐标轴设置箭头
"line_to_number_buff": SMALL_BUFF,#暂时没懂
"label_direction": DR,#暂时没懂
"number_scale_val": 0.5,#暂时没懂
},
"y_axis_config": {
"label_direction": DR,#暂时没懂
},
"background_line_style": {
"stroke_color": BLUE_D,#网格线颜色
"stroke_width": 2,#网格线宽度
"stroke_opacity": 1,#网格线透明度
},
# Defaults to a faded version of line_config
"faded_line_style": None,
"x_line_frequency": 0.1,#控制x轴网格线数量
"y_line_frequency": 0.1,#控制y轴网格线数量
"faded_line_ratio": 1,#暂时没懂
"make_smooth_after_applying_functions": True,#暂时没懂
}
确定要绘制的函数
我们使用两种方法来定义函数曲线:python函数、lambda表达式
1、python函数
看一个简单的例子
class PlotFunctions(GraphScene):
CONFIG = {
"x_min" : -10,
"x_max" : 10.3,
"x_axis_width": 14,
"x_tick_frequency": 1,
"x_labeled_nums" :range(-10,11,2),
"x_axis_label": "$x$",
"y_min" : -5,
"y_max" : 5,
"y_axis_height": 8,
"y_tick_frequency": 1,
"y_labeled_nums" :range(-5, 6, 1),
"exclude_zero_label": True,
"graph_origin" : ORIGIN ,
"function_color" : RED ,
"axes_color" : BLUE,
"graph_origin": 0 * DOWN + 0 * LEFT,
}
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)", x_val=10)
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, run_time = 5),ShowCreation(func_graph2, run_time = 5))
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)
我们这里通过继承GraphScene来实现函数曲线的绘制,我们调用self的.get_graph方法获取两个曲线对象func_graph和func_graph2,获取的这两个曲线对象都是基于当前坐标系的,下面我们继续调用self的.get_vertical_line_to_graph方法和get_graph_label方法获得相对于某个函数曲线的垂直线和标签,最下面就是动画展示曲线。我们可以看到,最后是两个函数,
在这两个函数中,我们使用了Numpy模块
2、lambda表达式
python中的lambda表达式比C++中简单一点,下面是一个有点复杂的例子,但理清了其实一点也不复杂
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()
我们重点关注一下CONFIG里面的”taylor”键所对应的值,这个值是一个lambda表达式列表,我们可以通过这种方法很容易地实现很多小型函数(曲线函数一般都是比较小的函数)的快速定义,下面的approx_graphs和term_num都使用了列表推导式,从而实现了
有规则的一系列对象的快速构建
,VectorizedPoint(3*DOWN)相当于创建了一个占位符。最后的显示中,作者还使用了enumerate方法(
enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中,可以在Python解释器中把一个可迭代对象放入enumerate()中看看会返回什么值
)。
将函数绘制在笛卡尔坐标系中
这个好像没什么可讲的了,前面基本上已经讲完了,就是像展示普通对象一样,将函数曲线对象用特定的展示方法展示出来。
初识三维场景
这是manim中的三维坐标结构,三维动画的制作,主要是三维视角的移动,
说得通俗一点就是你拿着一个摄像机对着这个坐标系拍摄,你可以从各个角度,各种距离进行拍摄,拍摄出来的视频就相当于我们制作出来的动画。
,下面我们主要介绍几个三维动画制作相关的函数。
还是从一个小例子看起
class My3DScene(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.add(plane.get_axis_labels())
self.add(plane)
circle = Circle(fill_color = BLUE)
text = TextMobject("Good night")
text.set_color(GREEN)
self.set_camera_orientation(phi=0, theta = 0)
self.wait()
self.move_camera(phi=PI/4,run_time=1) #currently broken in manim
self.move_camera(theta=PI/4,run_time=1)
self.begin_ambient_camera_rotation(rate=0.1)
self.play(ShowCreation(circle, run_time = 3))
self.play(ApplyMethod(circle.shift, UP+4*OUT, run_time = 4))
self.move_camera(theta = -PI/2, phi=PI/5, run_time=5)
self.play(Transform(circle, text, run_time = 3))
self.wait(6)
到这里,我们又见到一个新的类(
有没有想要创建一个属于自己的新的通用类的冲动
),这个类叫做ThreeDScene,这是基于Scene构建的3D类,我们通过子类化这个类,就可以实现各种3D效果(是不是很简单),下面我们来讲解具体的函数。
-
.set_camera_orientation(phi=0, theta = 0):用来设置摄像机(我这里把观察角度形象地称之为摄像机)的位置,这个位置可以通过四个参数来确定,他们分别是
θ\theta
θ
、
ϕ\phi
ϕ
、
γ\gamma
γ
和
r(
d
i
s
t
a
n
c
e
)
r(distance)
r
(
d
i
s
t
a
n
c
e
)
。对应着上面展示的三维坐标图,其中的
γ\gamma
γ
是一个欧拉角(什么是欧拉角?自己去查资料)。具体是什么效果,自己可以实际操作一下,一定要实际操作哦!! - .move_camera:这是用来移动摄像机的,使用方法和.set_camera_orientation差不多,但还是多了几个参数,具体是什么意思,还没有去尝试。
- .begin_ambient_camera_rotation:设置一种缓慢的旋转效果。尝试一下就知道是什么意思了,一定要尝试哦!!
使用SVG矢量图层
小提示
:阅读本节需要xml的知识(没学过的学一下,不难)
这章主要分为两个部分:SVG对象生成大致流程,SVG对象生成的重要细节。
SVG对象生成大致流程
你一定很好奇3b1b中的PI动画是如何制作出来的。manim中有一个SVGMobject可以导入SVG文件来生成一个SVG对象,我下面来讲解一下这个流程。
从一个小例子开始讲起
import os
import warnings
import numpy as np
from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.geometry import Circle
from manimlib.mobject.svg.drawings import ThoughtBubble
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.svg.tex_mobject import TextMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import normalize
MY_CREATURE_DIR = r"C:\Users\jiage\Desktop\animation\manim\media\svg_images"
class MyCreature(SVGMobject):
CONFIG = {
"color": None,
"file_name_prefix": "mycreatures",
"stroke_width": 0,
"stroke_color": BLACK,
"fill_opacity": 1.0,
"height": 3,
"corner_scale_factor": 0.75,
"flip_at_start": False,
"is_looking_direction_purposeful": False,
"start_corner": None,
# Range of proportions along body where arms are
"right_arm_range": [0.55, 0.7],
"left_arm_range": [.34, .462],
"pupil_to_eye_width_ratio": 0.4,
"pupil_dot_to_pupil_width_ratio": 0.3,
"body_index": 1,
"head_index": 2,
"head_color":BLUE,
"body_color":BLUE,
"eye_left_out_index": 3,
"eye_right_out_index": 4,
"eye_left_in_index": 5,
"eye_right_in_index": 6,
"mouth_index": 7,
}
def __init__(self, mode="plain", creature_name = "tau",creature_action = "ur", height = 3,**kwargs):#mode: look to the upper left;look to the top right corner;look in the lower left corner;look to the lower right corner
self.height = height
digest_config(self, kwargs)
self.mode = mode
self.parts_named = False
try:
svg_file = os.path.join(
MY_CREATURE_DIR,
"%s_%s_%s.svg" % (self.file_name_prefix, creature_name, creature_action)
)
SVGMobject.__init__(self, file_name=svg_file, **kwargs)
except Exception:
warnings.warn("No %s design with mode %s" %
(self.file_name_prefix, mode))
svg_file = os.path.join(
MY_CREATURE_DIR,
"mycreature_tau_plain.svg",
)
SVGMobject.__init__(self, mode="plain", file_name=svg_file, **kwargs)
def name_parts(self):
self.head = self.submobjects[self.head_index]
self.body = self.submobjects[self.body_index]
self.eye_left_out = self.submobjects[self.eye_left_out_index]
self.eye_right_out = self.submobjects[self.eye_right_out_index]
self.eye_left_in = self.submobjects[self.eye_left_in_index]
self.eye_right_in = self.submobjects[self.eye_right_in_index]
self.mouth = self.submobjects[self.mouth_index]
self.mouth.stroke_width = 1
self.parts_named = True
def init_colors(self):
SVGMobject.init_colors(self)
self.name_parts()
self.head.set_fill(self.head_color, opacity = 1)
self.body.set_fill(self.body_color, opacity = 1)
self.eye_left_out.set_fill(WHITE, opacity = 1)
self.eye_right_out.set_fill(WHITE, opacity = 1)
self.eye_left_in.set_fill(BLACK, opacity = 1)
self.eye_right_in.set_fill(BLACK, opacity = 1)
这个小例子
是我按照
manim中关于pi动画的工程
改编而成的,这是一个独立的文件,文件名叫做my_svg_class.py,顾名思义,这里面是我定义的svg对象。文件开头是一堆的模块导入,不用仔细研究它们的作用,一股脑导入就行了,下面的MY_CREATURE_DIR是一个字符串,里面存放的是我用到的svg文件的路径(关于字符串前面加上一个r的作用(在windows中路径名是以
\
作为分隔符,而
\
也是转义标志,r将会禁止所有的字符解释作用,所以在这里,r的作用就是为了防止字符串在不该被转义的地方被转义),下面是一个MyCreature类,这个类是SVGMobject的子类,为了仔细观察构造函数,我们把它提出来。
def __init__(self, mode="plain", creature_name = "tau",creature_action = "ur", height = 3,**kwargs):#mode: look to the upper left;look to the top right corner;look in the lower left corner;look to the lower right corner
self.height = height
digest_config(self, kwargs)
self.mode = mode
self.parts_named = False
try:
svg_file = os.path.join(
MY_CREATURE_DIR,
"%s_%s_%s.svg" % (self.file_name_prefix, creature_name, creature_action)
)
SVGMobject.__init__(self, file_name=svg_file, **kwargs)
except Exception:
warnings.warn("No %s design with mode %s" %
(self.file_name_prefix, mode))
svg_file = os.path.join(
MY_CREATURE_DIR,
"mycreature_tau_plain.svg",
)
SVGMobject.__init__(self, mode="plain", file_name=svg_file, **kwargs)
这个构造函数中的参数先不要看,我们重点来观察一下,它是怎样读取我的svg文件并由此生成一个svg对象的。看try下面,有一个字符串拼接过程,这里,我把我的文件夹的路径,和我的文件的文件名拼接起来,我的文件名由三个部分组成,第一部分是固定的,第二部分是什么动画角色(
具体的动画角色,需要自己去绘制矢量图片(所以要学习xml),比如3b1b动画里面就是一个PI,我这里使用的是TAU,如果想要文件的可以在评论区留言
),第三部分是确定这个动画角色是什么动作(其实一个动作就对应着一个SVG文件),这些参数都是通过类传参传进来的,具体的传参设置,后面再说,然后就是调用SVGMobject的构造函数来构造具体的SVG对象了,下面的except,自己看看吧,如果上面懂,下面也应该懂了。下面我们来看看具体怎么使用。
这是一个小例子
from manimlib.imports import *
from my_svg_class import MyCreature
class SVG_SHOW_SCENE(Scene):
def construct(self):
creature1 = MyCreature(creature_name = "tau", creature_action = "ur")
creature1.shift(5*LEFT + 2*DOWN)
creature2 = MyCreature(creature_name = "tau", creature_action = "dr")
creature2.shift(5*LEFT + 2*UP)
self.play(FadeIn(creature1, run_time = 3))
self.play(Transform(creature1, creature2, run_time = 3))
先把类导进来,然后构建一个场景类,我调用MyCreature,选定tau动画角色(
我后面可能不喜欢这个角色,这是为了更换方便
),确定眼睛朝向ur(右上方),这个时候还是上几张图片吧,好了,我们下面就可以用前面学过的各种移动、变换、布局来操作这些SVG对象了
SVG对象生成的
重要
细节
重要
1、关于SVGMobject如何解释SVG文件
是不是以为把SVG图像画出来,用__init__导入一下就没事了,不是那么简单的,如果你简单地这样做,那么你会发现你看到的都是一种颜色。SVGMobject在对xml的解析中,会把所有的元素都变成一个个带有数字标签的submobject对象(数字标签的顺序与xml文件中对应元素的顺序一样),我们需要一个一个地设置这些submobject的属性,这些submobject是通过数字索引得到的,下面是具体的两个设置属性的函数,具体的数字索引键值对在CONFIG字典中
def name_parts(self):
self.head = self.submobjects[self.head_index]
self.body = self.submobjects[self.body_index]
self.eye_left_out = self.submobjects[self.eye_left_out_index]
self.eye_right_out = self.submobjects[self.eye_right_out_index]
self.eye_left_in = self.submobjects[self.eye_left_in_index]
self.eye_right_in = self.submobjects[self.eye_right_in_index]
self.mouth = self.submobjects[self.mouth_index]
self.mouth.stroke_width = 1
self.parts_named = True
def init_colors(self):
SVGMobject.init_colors(self)
self.name_parts()
self.head.set_fill(self.head_color, opacity = 1)
self.body.set_fill(self.body_color, opacity = 1)
self.eye_left_out.set_fill(WHITE, opacity = 1)
self.eye_right_out.set_fill(WHITE, opacity = 1)
self.eye_left_in.set_fill(BLACK, opacity = 1)
self.eye_right_in.set_fill(BLACK, opacity = 1)
其中的init_colors()是虚函数,会被自动调用。
2、关于SVG对象移动的问题
manim中,有很多东西,我们见到的样子,和它们的真实模样是不一样的。看下面这段代码
def construct(self):
creature1 = MyCreature(creature_name = "tau", creature_action = "ur")
creature1.shift(5*LEFT + 2*DOWN)
creature2 = MyCreature(creature_name = "tau", creature_action = "dr")
creature2.shift(5*LEFT + 2*UP)
creature3 = MyCreature(creature_name="tau", creature_action="dl")
creature3.shift(2*UP + 5*RIGHT)
self.play(FadeIn(creature1))
self.play(Transform(creature1, creature2, run_time = 3))
self.play(Transform(creature2, creature3, run_time = 3))
我创建了三个动画对象,一个放在左下角,一个放在左上角,一个放在右上角,我先显示creature1,然后将creature1变换为creature2,最后将creature2变成creature3,看似没有问题,其实是有问题的。问题就出在我对Transform的理解上,Transform在把creature1变换成creature2的过程中的真实操作应当是:creature1 = creature2,然后把creature1以带有移动路径的效果再次显示出来。creature并没有显示出来(
我以前一直以为是先把creature1消除,然后再把creature2显示出来
)。所以代码应当改成下面这样,才能实现动画角色的正常移动
def construct(self):
creature1 = MyCreature(creature_name = "tau", creature_action = "ur")
creature1.shift(5*LEFT + 2*DOWN)
creature2 = MyCreature(creature_name = "tau", creature_action = "dr")
creature2.shift(5*LEFT + 2*UP)
creature3 = MyCreature(creature_name="tau", creature_action="dl")
creature3.shift(2*UP + 5*RIGHT)
self.play(FadeIn(creature1))
self.play(Transform(creature1, creature2, run_time = 3))
self.play(Transform(creature1, creature3, run_time = 3))
总结
这次博客的创作时间比较长,主要原因是中间有期末考试,所以耽搁下来了,今天终于写完了。
我在创造SVG对象的过程中花费了最长的时间,在这期间,我学习了xml,一个很有用的标记语言,除此之外,我还进一步地探索了manim源码,并创造了自己的SVG类,成就感还是满满的。