Manim物理模拟:别自己写欧拉了!
Manim物理模拟:别自己写欧拉了!
如果你曾经用Manim做过物理动画,大概率会陷入一个“经典陷阱”:自己写欧拉法(Euler Method)来更新粒子的位置和速度。一开始感觉挺简单——v += a*dt; x += v*dt,两行代码搞定。但当你开始模拟弹簧、行星轨道、或者任何稍微复杂的系统时,你会发现:欧拉法不仅精度低,而且会导致能量漂移,动画看起来就像“物理崩坏”。更糟糕的是,你每次都要手动管理一堆self.add_updater和状态变量,代码迅速膨胀。但好消息是:Manim本身就是一个面向动画的数学引擎,它内置了比欧拉法更优雅的物理模拟工具。本文将从基础到进阶,教会你如何用Manim“正当地”做物理模拟,而不是自己造欧拉轮子。—## 一、为什么“别自己写欧拉”?先看一个简单例子:模拟一个弹簧上的质点。如果你用欧拉法,代码大概长这样:pythonclass EulerSpring(Scene): def construct(self): mass = Dot(ORIGIN, color=YELLOW) spring = Line(LEFT*2, mass.get_center(), color=BLUE) self.add(mass, spring) v = 0 k = 0.5 dt = 0.01 def update(obj, dt): nonlocal v # 欧拉更新:先算加速度,再更新速度,最后更新位置 a = -k * obj.get_center()[0] v += a * dt obj.shift(RIGHT * v * dt) spring.put_start_and_end_on(LEFT*2, obj.get_center()) mass.add_updater(update) self.wait(3)这段代码的问题:1. 精度低:欧拉法是一阶方法,每步误差与dt²成正比,长时间模拟会导致轨道漂移。2. 能量不守恒:系统会慢慢“吸能”或“放能”,动画看起来不真实。3. 状态管理混乱:需要手动定义v、k、dt等变量,并且把它们“闭包”进updater里,可读性差。如果换成Manim自带的MovingCamera和ValueTracker,或者更优雅的方式,会怎样?—## 二、用ValueTracker + Updater 做“半物理”模拟Manim的ValueTracker可以存储数值,并且支持插值。我们可以用它来驱动粒子运动,而不必手动写微分方程。但注意:这依然是“动画驱动”,不是真正的物理模拟——不过对于演示来说,已经足够。pythonclass ValueTrackerDemo(Scene): def construct(self): # 用一个ValueTracker来存储时间t t = ValueTracker(0) dot = Dot(UP*2, color=RED) path = TracedPath(dot.get_center, stroke_color=RED, stroke_width=2) self.add(dot, path) def update_dot(d): # 从ValueTracker读取时间 time = t.get_value() # 这里可以放任何解析函数,比如简谐运动 x = np.sin(time) y = np.cos(time) * 2 d.move_to(np.array([x, y, 0])) dot.add_updater(update_dot) self.play(t.animate.set_value(2*PI), run_time=3) self.wait()优点:代码简洁,适合演示解析解。 缺点:不能模拟复杂力场(比如多体引力,或者非线性摩擦)。—## 三、进阶:使用 ODE 模块(Scipy)实现真实物理Manim本身没有内置ODE求解器,但你可以结合scipy.integrate.solve_ivp(Runge-Kutta方法,比欧拉高好几个数量级)来获取高精度轨迹,然后把这轨迹“喂”给Manim的Mobject。这种“先算物理,再放动画”的做法,才是工程级解法。### 示例:双摆模拟(混沌系统)双摆是经典混沌系统,欧拉法完全不可用。我们用solve_ivp求解,然后让Manim播放结果。pythonimport numpy as npfrom scipy.integrate import solve_ivpfrom manim import *class DoublePendulum(Scene): def construct(self): # 物理参数 L1, L2 = 2, 2 m1, m2 = 1, 1 g = 9.8 def deriv(t, y): # y = [theta1, omega1, theta2, omega2] q1, w1, q2, w2 = y delta = q2 - q1 den = 2*m1 + m2 - m2*np.cos(2*q1 - 2*q2) a1 = -g*(2*m1+m2)*np.sin(q1) - m2*g*np.sin(q1-2*q2) a2 = -2*np.sin(q1-q2)*m2*(w2**2*L2 + w1**2*L1*np.cos(q1-q2)) a1 += a2 a1 /= L1*den b1 = 2*np.sin(q1-q2)*(w1**2*L1*(m1+m2) + g*(m1+m2)*np.cos(q1) + w2**2*L2*m2*np.cos(q1-q2)) b1 /= L2*den return [w1, a1, w2, b1] # 初始条件:theta1=120°, theta2=10°, 静止 y0 = [np.radians(120), 0, np.radians(10), 0] t_span = (0, 15) sol = solve_ivp(deriv, t_span, y0, method='RK45', t_eval=np.linspace(0, 15, 300)) # 转换为笛卡尔坐标 the1, the2 = sol.y[0], sol.y[2] x1 = L1*np.sin(the1) y1 = -L1*np.cos(the1) x2 = x1 + L2*np.sin(the2) y2 = y1 - L2*np.cos(the2) # 创建动画对象 p1 = Dot(ORIGIN, color=BLUE) p2 = Dot(np.array([x1[0], y1[0], 0]), color=YELLOW) p3 = Dot(np.array([x2[0], y2[0], 0]), color=RED) rod1 = Line(p1.get_center(), p2.get_center(), color=BLUE) rod2 = Line(p2.get_center(), p3.get_center(), color=YELLOW) trace = TracedPath(p3.get_center, stroke_color=RED, stroke_width=2) self.add(p1, p2, p3, rod1, rod2, trace) # 使用ValueTracker控制时间索引 idx_tracker = ValueTracker(0) def update_objs(): i = int(idx_tracker.get_value()) if i >= len(x2): i = len(x2)-1 pos2 = np.array([x1[i], y1[i], 0]) pos3 = np.array([x2[i], y2[i], 0]) p2.move_to(pos2) p3.move_to(pos3) rod1.put_start_and_end_on(p1.get_center(), pos2) rod2.put_start_and_end_on(pos2, pos3) self.add_updater(update_objs) self.play(idx_tracker.animate.set_value(len(x2)-1), run_time=15, rate_func=linear) self.wait()关键点:- 物理计算完全独立(用scipy),动画只是“播放器”。- 避免了欧拉法的一切问题。- 代码虽然长,但逻辑清晰:先算轨迹,再画动画。—## 四、更高级:用ManimPhysics库(或自封装)如果你经常做物理模拟,建议自己封装一个简单的Particle类,内部维护状态和ODE求解器。这样其他项目可以直接复用。pythonclass Particle: def __init__(self, pos, vel, mass=1, color=YELLOW): self.pos = np.array(pos, dtype=float) self.vel = np.array(vel, dtype=float) self.mass = mass self.dot = Dot(self.pos, color=color) self.trace = TracedPath(self.dot.get_center, stroke_color=color) def update_physics(self, dt, forces): # 使用半隐式欧拉(比显式好一些),但实际可以用RK4 acc = forces(self) / self.mass self.vel += acc * dt self.pos += self.vel * dt self.dot.move_to(self.pos)但建议:直接调用scipy的solve_ivp,因为半隐式欧拉依然不够稳定。—## 五、总结**“别自己写欧拉”** 的核心思想是: 1. 欧拉法精度低、能量漂移,不适合长时间模拟。 2. Manim不是物理引擎,但可以结合scipy等数值库,实现高精度物理动画。 3. 正确做法:用solve_ivp(RK45)计算轨迹,然后用Manim的ValueTracker和Updater来播放结果。 4. 代码组织:物理计算与动画分离,让代码可维护、可复用。如果你只是演示简单运动,可以用解析解+ValueTracker;如果是复杂系统,直接用scipy。记住:动画只是展示,物理计算请交给专业的数值库。希望这篇文章能帮你告别手写欧拉,写出既精确又优雅的Manim物理模拟。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)