如何用Manim+SciPy/SymPy/Numpy实现最速降线动画并验证小球速度
问题解答
一、现有小球动画的物理合理性与速度计算
1. 物理合理性判断
你当前的动画不符合物理规律:代码中通过ValueTracker的t匀速变化来更新小球的x坐标,相当于小球沿x轴匀速移动,完全忽略了重力作用下的运动规律——真实情况下,小球沿曲线下滑的速度会随下落高度变化,x方向的运动速度并非恒定值。
2. 速度计算方法
假设小球无摩擦下滑,根据机械能守恒,速度大小由下落的高度差决定:
v = √(2gΔh)
其中Δh = y₀ - y(x),y₀是初始位置的y坐标,g是重力加速度(通常取9.8m/s²,简化计算时可设为10)
速度的方向是曲线在当前点的切线方向,可通过对曲线函数求导得到切线斜率,进而推导方向向量。
3. 修正动画的核心思路
要让动画符合物理规律,不能直接让x随时间匀速变化,需通过数值积分求解小球的运动方程:
- 设曲线为y=f(x),切线斜率k=f’(x),结合速度大小v=√(2g(y₀ - y)),推导x、y方向的速度分量关系
- 用欧拉法或龙格-库塔法进行数值迭代,逐帧更新小球的x、y位置
二、用Manim制作最速降线(Brachistochrone)动画
最速降线的本质是摆线,参数方程为:
x = r(θ - sinθ) y = r(1 - cosθ)
其中r为摆线半径,θ取值范围通常为0到2π(对应摆线的一个完整拱)。
完整实现代码
from manim import * import numpy as np class Brachistochrone(Scene): def construct(self): # 设置坐标轴 ax = Axes( x_range=[0, 7], y_range=[0, 4], axis_config={"include_tip": False, "color": GREY} ) labels = ax.get_axis_labels(x_label="x", y_label="y") self.add(ax, labels) # 摆线参数(r=1) r = 1 def brachistochrone(theta): return np.array([r*(theta - np.sin(theta)), r*(1 - np.cos(theta)), 0]) # 生成对比曲线:最速降线、直线、抛物线 theta_space = np.linspace(0, 2*np.pi, 200) brachistochrone_points = [brachistochrone(t) for t in theta_space] brachistochrone_curve = VMobject().set_points_as_corners(brachistochrone_points).set_color(RED) # 直线:从起点(0,2r)到终点(2πr,0) start_point = ax.c2p(0, 2*r) end_point = ax.c2p(2*np.pi*r, 0) line = Line(start_point, end_point, color=BLUE) # 抛物线:y = -0.1(x - πr)^2 + 2r def parabola(x): return -0.1*(x - np.pi*r)**2 + 2*r parabola_curve = ax.plot(parabola, x_range=[0, 2*np.pi*r], color=GREEN) # 创建三个对比小球 dot_b = Dot(start_point, color=RED) dot_l = Dot(start_point, color=BLUE) dot_p = Dot(start_point, color=GREEN) self.add(brachistochrone_curve, line, parabola_curve) self.add(dot_b, dot_l, dot_p) # 最速降线小球运动更新器 def update_brachistochrone_dot(dot, dt): current_pos = ax.p2c(dot.get_center()) # 近似匹配当前位置对应的theta theta_idx = np.argmin(np.abs(r*(theta_space - np.sin(theta_space)) - current_pos[0])) theta = theta_space[theta_idx] # 计算速度大小(机械能守恒) initial_y = 2*r current_y = current_pos[1] v = np.sqrt(2*9.8*(initial_y - current_y)) if (initial_y - current_y) > 0 else 0 # 计算切线方向速度分量 dx_dtheta = r*(1 - np.cos(theta)) dy_dtheta = r*np.sin(theta) if dx_dtheta == 0: return speed_norm = np.sqrt(dx_dtheta**2 + dy_dtheta**2) v_x = v * dx_dtheta / speed_norm v_y = v * dy_dtheta / speed_norm # 更新位置(Manim y轴向上,物理下落方向向下,故v_y取负) new_x = current_pos[0] + v_x * dt new_y = current_pos[1] - v_y * dt if new_x >= 2*np.pi*r: dot.move_to(end_point) return dot.move_to(ax.c2p(new_x, new_y)) # 直线小球运动更新器 def update_line_dot(dot, dt): current_pos = ax.p2c(dot.get_center()) initial_y = 2*r current_y = current_pos[1] v = np.sqrt(2*9.8*(initial_y - current_y)) if (initial_y - current_y) > 0 else 0 # 直线斜率k = -1/π k = -1/np.pi speed_norm = np.sqrt(1 + k**2) v_x = v / speed_norm v_y = v * k / speed_norm new_x = current_pos[0] + v_x * dt new_y = current_pos[1] - v_y * dt if new_x >= 2*np.pi*r: dot.move_to(end_point) return dot.move_to(ax.c2p(new_x, new_y)) # 抛物线小球运动更新器 def update_parabola_dot(dot, dt): current_pos = ax.p2c(dot.get_center()) initial_y = 2*r current_y = current_pos[1] v = np.sqrt(2*9.8*(initial_y - current_y)) if (initial_y - current_y) > 0 else 0 # 抛物线导数y’ = -0.2(x - πr) dy_dx = -0.2*(current_pos[0] - np.pi*r) speed_norm = np.sqrt(1 + dy_dx**2) v_x = v / speed_norm v_y = v * dy_dx / speed_norm new_x = current_pos[0] + v_x * dt new_y = current_pos[1] - v_y * dt if new_x >= 2*np.pi*r: dot.move_to(end_point) return dot.move_to(ax.c2p(new_x, new_y)) # 添加更新器并播放动画 dot_b.add_updater(update_brachistochrone_dot) dot_l.add_updater(update_line_dot) dot_p.add_updater(update_parabola_dot) self.wait(5) # 到达终点后移除更新器 dot_b.remove_updater(update_brachistochrone_dot) dot_l.remove_updater(update_line_dot) dot_p.remove_updater(update_parabola_dot) self.wait()
关键说明
- 最速降线采用摆线参数方程生成,这是唯一能让小球从起点到终点耗时最短的曲线
- 动画通过机械能守恒计算速度,结合曲线切线斜率分解速度分量,用数值积分更新位置,完全符合物理规律
- 加入直线、抛物线的对比,可直观展示最速降线的“最快”特性
内容的提问来源于stack exchange,提问作者Freya the Goddess
相关产品推荐
相关产品推荐

