一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

利用 SymPy 实现 Manim 曲线匀速绘制的解决方案

时间:2026-06-02 09:35:01 编辑:袖梨 来源:一聚教程网

绘制极坐标曲线时,参数均匀分布往往导致绘制速度不均。本文将介绍如何利用SymPy实现弧长参数化,解决Manim动画中的曲线绘制问题。

用 SymPy 解决 Manim 曲线绘制速度不均的问题

绘制参数曲线的基本思路很简单:先计算曲线上的点,再用Create动画一笔画出。但在实际渲染时,常会出现花瓣尖端绘制过快、中间部分过慢的问题。

问题根源在于:当参数θ均匀变化时,曲线上点的实际移动距离并不均匀。要让画笔匀速移动,必须采用弧长参数化方法,使参数按照弧长均匀分布。

手工计算弧长参数化几乎不可能完成,因为需要先积分求弧长,再反解参数方程。幸运的是,SymPy可以完美解决这个问题。

1. 痛点场景还原:一个具体的例子

以下代码展示了典型的绘制速度不均问题:

from manim import *
import numpy as npclass BadRoseCurve(Scene):
    def construct(self):
        axes = Axes(x_range=[-3, 3], y_range=[-3, 3])
        self.add(axes)        # 用累积密度法生成非均匀 theta:尖端稀疏(快),中部密集(慢)
        n_points = 500
        t = np.linspace(0, 2 * PI, n_points)
        # 密度函数:在 cos(5t)=0 处(花瓣中部)密度高,在 |cos(5t)|=1 处(尖端)密度低
        density = 1 + 3 * np.sin(5 * t) ** 2
        theta = np.cumsum(density)
        theta = theta / theta[-1] * PI  # 归一化到 [0, π](k为奇数时只需π即可画完)
        r = np.cos(5 * theta)
        x = r * 2 * np.cos(theta)
        y = r * 2 * np.sin(theta)        points = [axes.c2p(x[i], y[i]) for i in range(n_points)]
        curve = VMobject(color=PINK, stroke_width=3)
        curve.set_points_as_corners(points)        # 用 Create 画曲线——速度明显不均匀!
        self.play(Create(curve), run_time=5, rate_func=linear)
        self.wait()

运行这段代码会发现,花瓣尖端绘制过快,而中部绘制过慢。虽然设置了rate_func=linear,但曲线点的分布不均导致视觉速度波动。

2. SymPy 解决方案:弧长参数化三步走

实现匀速绘制的核心是生成等弧长分布的参数点,具体分为三个步骤:

  1. 计算弧长函数L(θ),表示从起点到参数θ的弧长
  2. 将总弧长等分,得到目标弧长值s₁,s₂,...
  3. 对每个sᵢ,求解方程L(θ)=sᵢ,得到对应的θᵢ

SymPy可以高效完成这三步计算。

2.1 用 SymPy 推导弧长函数

import sympy as sptheta = sp.Symbol('theta', real=True)
n = 5  # 五瓣玫瑰线# 极坐标方程(注意这里放大了2倍,与痛点代码中的 r*2 保持一致)
r = 2 * sp.cos(n * theta)# 极坐标 → 直角坐标(符号推导,零误差)
x = r * sp.cos(theta)  # x = r(θ)·cos(θ)
y = r * sp.sin(theta)  # y = r(θ)·sin(θ)# 弧长微元:ds/dθ = sqrt((dx/dθ)² + (dy/dθ)²)
dx_dtheta = sp.diff(x, theta)
dy_dtheta = sp.diff(y, theta)# 弧长微元表达式(注意:这里不算积分,只保留被积函数)
ds_dtheta = sp.sqrt(dx_dtheta**2 + dy_dtheta**2)
print("弧长微元 ds/dθ =", sp.simplify(ds_dtheta))# 运行结果:
'''
弧长微元 ds/dθ = 2*sqrt((2*sin(4*theta) + 
                     3*sin(6*theta))**2 + 
                     (2*cos(4*theta) -
                     3*cos(6*theta))**2)
'''

虽然弧长表达式较为复杂,但SymPy可以将其转换为数值计算函数。

2.2 用 nsolve 反解等弧长参数点

import numpy as np
from scipy.integrate import quad
from scipy.optimize import bisect  # 数值求根,稳定且快# 把弧长微元转成可数值计算的函数
ds_func = sp.lambdify(theta, ds_dtheta, 'numpy')# 数值弧长函数:L(t) = ∫₀^t ds
def arc_length(t):
    """计算从 0 到 t 的弧长"""
    result, _ = quad(ds_func, 0, t, limit=100)
    return result# 计算总弧长
total_length = arc_length(2 * np.pi)
print(f"总弧长: {total_length:.4f}")# 等分弧长,用数值求根反解对应的 theta
N = 500
s_values = np.linspace(0, total_length, N)
theta_vals = []# 辅助函数:f(t) = L(t) - s,我们要找 f(t)=0 的根
def f(t, s):
    return arc_length(t) - sfor i, s in enumerate(s_values):
    if i == 0:
        theta_vals.append(0.0)  # s=0 对应 theta=0
        continue    # 初始搜索区间:用上一个 theta 作为左边界
    # 弧长是单调递增的,所以解一定在 [左边界, 右边界] 之间
    left = theta_vals[-1]  # 上一个已解出的 theta
    right = left + 0.5     # 向右扩展,足够覆盖下一个等分点    # 扩展右边界直到 f(right, s) > 0(确保根在区间内)
    while f(right, s) < 0:
        right += 0.5    # 二分法求根(稳定、快速)
    sol = bisect(f, left, right, args=(s,), xtol=1e-8)
    theta_vals.append(float(sol))theta_vals = np.array(theta_vals)

关键点说明

  • sp.lambdify将符号表达式转换为NumPy函数
  • sp.nsolve数值解方程,配合合理猜测值可提高效率
  • 使用弧长占比估算初始猜测值,接近真实解

3. Manim 联动实战

将上述计算与Manim动画结合,完整代码如下:

from manim import *
import numpy as np
import sympy as sp
from scipy.integrate import quad
from scipy.optimize import bisectclass UniformRoseCurve(Scene):
    def construct(self):
        # ===== SymPy + 数值积分计算等弧长参数点 =====
        theta = sp.Symbol("theta", real=True)
        n = 5        # 极坐标方程 r = 2*cos(5θ)
        r = 2 * sp.cos(n * theta)        # 极坐标转直角坐标
        x_expr = r * sp.cos(theta)
        y_expr = r * sp.sin(theta)        # 弧长微元
        dx = sp.diff(x_expr, theta)
        dy = sp.diff(y_expr, theta)
        ds_dtheta = sp.sqrt(dx**2 + dy**2)        # 数值弧长函数
        ds_func = sp.lambdify(theta, ds_dtheta, "numpy")        def arc_length(t):
            val, _ = quad(ds_func, 0, t, limit=100)
            return val        # 总弧长(k为奇数时只需π即可画完)
        total_len = arc_length(np.pi)
        print(f"总弧长: {total_len:.4f}")        # 用二分法反解等弧长 theta(速度快)
        N = 500
        s_vals = np.linspace(0, total_len, N)
        theta_vals = [0.0]        def f(t, s):
            return arc_length(t) - s        for i in range(1, N):
            s = s_vals[i]
            left = theta_vals[-1]
            right = left + 0.5
            # 扩展右边界直到 f(right) > 0
            while f(right, s) < 0:
                right += 0.5
            sol = bisect(f, left, right, args=(s,), xtol=1e-8)
            theta_vals.append(float(sol))        theta_vals = np.array(theta_vals)        # 计算直角坐标点
        x_func = sp.lambdify(theta, x_expr, "numpy")
        y_func = sp.lambdify(theta, y_expr, "numpy")        # ===== Manim 动画 =====
        axes = Axes(x_range=[-3, 3], y_range=[-3, 3])
        self.add(axes)        points = []
        for t in theta_vals:
            px = float(x_func(t))
            py = float(y_func(t))
            points.append(axes.c2p(px, py))        curve = VMobject(color=PINK, stroke_width=3)
        curve.set_points_as_corners(points)        self.play(Create(curve), run_time=5, rate_func=linear)
        self.wait()

4. 效果展示说明

运行UniformRoseCurve并与BadRoseCurve对比,可以观察到:

  • 绘制过程均匀流畅:曲线以恒定速度生长,花瓣尖端和中部绘制速度一致
  • 本质区别:虽然θ值分布不均匀,但映射到平面后相邻点间距相等
  • 参数化效果:真正实现了空间上的均匀分布,而非参数上的均匀

5. 本期小结

  • 核心问题:均匀参数分布不等于均匀弧长分布
  • 解决方案:通过弧长参数化实现真正匀速绘制
  • SymPy工具
    • sp.diff求导获得弧长微元
    • sp.integrate计算弧长函数
    • sp.nsolve反解参数值
    • sp.lambdify转换数值函数
  • Manim应用:将等弧长点集传递给VMobject,配合Create动画实现匀速绘制

通过SymPy的符号计算能力,我们成功解决了Manim中参数曲线绘制速度不均的问题,实现了真正匀速的动画效果。

热门栏目