Python使用SymPy解決Manim曲線繪制速度不均的問(wèn)題
如果你想繪制一個(gè)參數(shù)曲線,比如:極坐標(biāo)玫瑰線 r = cos(5θ)。
那么思路很簡(jiǎn)單:算出曲線上的點(diǎn),用 Create 一筆畫出來(lái)。
但是,寫完代碼一渲染,問(wèn)題來(lái)了:
花瓣尖端“唰”地一下就過(guò)去了,中間部分卻慢吞吞的。整條曲線的繪制節(jié)奏忽快忽慢,看起來(lái)十分別扭。
問(wèn)題的根源:我讓參數(shù) θ\thetaθ均勻遞增,但曲線上點(diǎn)移動(dòng)的實(shí)際距離并不是均勻的。參數(shù)變化快的地方,點(diǎn)就“飛”過(guò)去;參數(shù)變化慢的地方,點(diǎn)就“爬”過(guò)去。要讓畫筆勻速移動(dòng),必須讓參數(shù)按照弧長(zhǎng)均勻分布——這就是弧長(zhǎng)參數(shù)化。
手工做這件事幾乎不可能:求弧長(zhǎng)得積分,反解參數(shù)得解方程,曲線稍微復(fù)雜一點(diǎn)就算不動(dòng)了。
好在,我們有 SymPy。
1. 痛點(diǎn)場(chǎng)景還原:一個(gè)具體的例子
先看一段“有毛病”的代碼,直觀感受一下問(wèn)題:
from manim import *
import numpy as np
class 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)
# 密度函數(shù):在 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為奇數(shù)時(shí)只需π即可畫完)
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()
運(yùn)行這個(gè)動(dòng)畫,你會(huì)看到:花瓣尖端幾幀就畫完了,而花瓣中部卻畫得很慢。

rate_func=linear 控制的是動(dòng)畫進(jìn)度的均勻,但曲線本身點(diǎn)的分布就是疏密不均的,所以視覺(jué)速度必然忽快忽慢。
2. SymPy 解決方案:弧長(zhǎng)參數(shù)化三步走
要讓繪制速度均勻,核心思路是:生成一組讓相鄰點(diǎn)之間實(shí)際距離相等的參數(shù)值。具體分三步:
- 用積分算出弧長(zhǎng)函數(shù) L(θ)——從起點(diǎn)到參數(shù) θ的弧長(zhǎng)
- 算總弧長(zhǎng),等分出一組目標(biāo)弧長(zhǎng)值 s1,s2,...s_1, s_2, ...s1?,s2?,...
- 對(duì)每個(gè)Si,反解方程L(θ)=Si?,得到對(duì)應(yīng)的 θi?
這三步,SymPy 都能幫上忙。
2.1 用 SymPy 推導(dǎo)弧長(zhǎng)函數(shù)
import sympy as sp
theta = sp.Symbol('theta', real=True)
n = 5 # 五瓣玫瑰線
# 極坐標(biāo)方程(注意這里放大了2倍,與痛點(diǎn)代碼中的 r*2 保持一致)
r = 2 * sp.cos(n * theta)
# 極坐標(biāo) → 直角坐標(biāo)(符號(hào)推導(dǎo),零誤差)
x = r * sp.cos(theta) # x = r(θ)·cos(θ)
y = r * sp.sin(theta) # y = r(θ)·sin(θ)
# 弧長(zhǎng)微元:ds/dθ = sqrt((dx/dθ)2 + (dy/dθ)2)
dx_dtheta = sp.diff(x, theta)
dy_dtheta = sp.diff(y, theta)
# 弧長(zhǎng)微元表達(dá)式(注意:這里不算積分,只保留被積函數(shù))
ds_dtheta = sp.sqrt(dx_dtheta**2 + dy_dtheta**2)
print("弧長(zhǎng)微元 ds/dθ =", sp.simplify(ds_dtheta))
# 運(yùn)行結(jié)果:
'''
弧長(zhǎng)微元 ds/dθ = 2*sqrt((2*sin(4*theta) +
3*sin(6*theta))**2 +
(2*cos(4*theta) -
3*cos(6*theta))**2)
'''
SymPy 會(huì)輸出一個(gè)橢圓積分形式的表達(dá)式——這是正常的,很多曲線的弧長(zhǎng)都沒(méi)有初等表達(dá)式。
沒(méi)關(guān)系,數(shù)值求解一樣好用。
2.2 用 nsolve 反解等弧長(zhǎng)參數(shù)點(diǎn)
import numpy as np
from scipy.integrate import quad
from scipy.optimize import bisect # 數(shù)值求根,穩(wěn)定且快
# 把弧長(zhǎng)微元轉(zhuǎn)成可數(shù)值計(jì)算的函數(shù)
ds_func = sp.lambdify(theta, ds_dtheta, 'numpy')
# 數(shù)值弧長(zhǎng)函數(shù):L(t) = ∫?^t ds
def arc_length(t):
"""計(jì)算從 0 到 t 的弧長(zhǎng)"""
result, _ = quad(ds_func, 0, t, limit=100)
return result
# 計(jì)算總弧長(zhǎng)
total_length = arc_length(2 * np.pi)
print(f"總弧長(zhǎng): {total_length:.4f}")
# 等分弧長(zhǎng),用數(shù)值求根反解對(duì)應(yīng)的 theta
N = 500
s_values = np.linspace(0, total_length, N)
theta_vals = []
# 輔助函數(shù):f(t) = L(t) - s,我們要找 f(t)=0 的根
def f(t, s):
return arc_length(t) - s
for i, s in enumerate(s_values):
if i == 0:
theta_vals.append(0.0) # s=0 對(duì)應(yīng) theta=0
continue
# 初始搜索區(qū)間:用上一個(gè) theta 作為左邊界
# 弧長(zhǎng)是單調(diào)遞增的,所以解一定在 [左邊界, 右邊界] 之間
left = theta_vals[-1] # 上一個(gè)已解出的 theta
right = left + 0.5 # 向右擴(kuò)展,足夠覆蓋下一個(gè)等分點(diǎn)
# 擴(kuò)展右邊界直到 f(right, s) > 0(確保根在區(qū)間內(nèi))
while f(right, s) < 0:
right += 0.5
# 二分法求根(穩(wěn)定、快速)
sol = bisect(f, left, right, args=(s,), xtol=1e-8)
theta_vals.append(float(sol))
theta_vals = np.array(theta_vals)
代碼要點(diǎn):
sp.lambdify把符號(hào)表達(dá)式編譯成 NumPy 函數(shù),求值快sp.nsolve數(shù)值解方程,給定一個(gè)好猜測(cè)值能顯著加速- 猜測(cè)值用“弧長(zhǎng)占比 × 總參數(shù)范圍”做線性估計(jì),足夠接近真實(shí)解
3. Manim 聯(lián)動(dòng)實(shí)戰(zhàn)
把上面的計(jì)算和 Manim 動(dòng)畫串起來(lái),就是一份完整可運(yùn)行的代碼:
from manim import *
import numpy as np
import sympy as sp
from scipy.integrate import quad
from scipy.optimize import bisect
class UniformRoseCurve(Scene):
def construct(self):
# ===== SymPy + 數(shù)值積分計(jì)算等弧長(zhǎng)參數(shù)點(diǎn) =====
theta = sp.Symbol("theta", real=True)
n = 5
# 極坐標(biāo)方程 r = 2*cos(5θ)
r = 2 * sp.cos(n * theta)
# 極坐標(biāo)轉(zhuǎn)直角坐標(biāo)
x_expr = r * sp.cos(theta)
y_expr = r * sp.sin(theta)
# 弧長(zhǎng)微元
dx = sp.diff(x_expr, theta)
dy = sp.diff(y_expr, theta)
ds_dtheta = sp.sqrt(dx**2 + dy**2)
# 數(shù)值弧長(zhǎng)函數(shù)
ds_func = sp.lambdify(theta, ds_dtheta, "numpy")
def arc_length(t):
val, _ = quad(ds_func, 0, t, limit=100)
return val
# 總弧長(zhǎng)(k為奇數(shù)時(shí)只需π即可畫完)
total_len = arc_length(np.pi)
print(f"總弧長(zhǎng): {total_len:.4f}")
# 用二分法反解等弧長(zhǎng) 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
# 擴(kuò)展右邊界直到 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)
# 計(jì)算直角坐標(biāo)點(diǎn)
x_func = sp.lambdify(theta, x_expr, "numpy")
y_func = sp.lambdify(theta, y_expr, "numpy")
# ===== Manim 動(dòng)畫 =====
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. 效果展示說(shuō)明
運(yùn)行 UniformRoseCurve,對(duì)比之前的 BadRoseCurve,差異一目了然:
- 繪制過(guò)程均勻流暢:粉色線條以恒定速度從起點(diǎn)生長(zhǎng)到終點(diǎn),花瓣尖端不再“閃現(xiàn)”,花瓣中部也不再“拖沓”。整條曲線在 10 秒內(nèi)被平穩(wěn)地畫完,節(jié)奏非常舒服。
- 同一曲線,兩種體驗(yàn):
BadRoseCurve由于刻意在花瓣中部堆了 3 倍密度,視覺(jué)上畫筆會(huì)在那里明顯減速;而弧長(zhǎng)參數(shù)化版本抹平了所有速度波動(dòng),讓rate_func=linear真正發(fā)揮作用。 - 弧長(zhǎng)參數(shù)化的本質(zhì):雖然
theta_vals的數(shù)值分布是不均勻的(花瓣尖端附近 θ 變化慢,中部變化快),但映射到平面后,相鄰點(diǎn)之間的實(shí)際距離完全相等。這就是“均勻”的真正含義——空間上的均勻,而非參數(shù)上的均勻。
5. 本期小結(jié)
- 問(wèn)題本質(zhì):均勻參數(shù) ≠ 均勻弧長(zhǎng),直接用均勻參數(shù)畫曲線必然速度不均。
- 解決思路:弧長(zhǎng)參數(shù)化——先算弧長(zhǎng)函數(shù),再反解出等弧長(zhǎng)分布的參數(shù)點(diǎn)。
- SymPy 的角色:
sp.diff→ 求導(dǎo)得弧長(zhǎng)微元sp.integrate→ 算弧長(zhǎng)函數(shù)sp.nsolve→ 反解參數(shù)值sp.lambdify→ 表達(dá)式轉(zhuǎn)數(shù)值函數(shù),高效求值
- Manim 中的用法:把等弧長(zhǎng)點(diǎn)集喂給
VMobject.set_points_as_corners(),再用Create+rate_func=linear,即可實(shí)現(xiàn)真正勻速的曲線繪制。
以上就是Python使用SymPy解決Manim曲線繪制速度不均的問(wèn)題的詳細(xì)內(nèi)容,更多關(guān)于Python SymPy解決Manim繪制速度不均的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Pygame出現(xiàn)播放背景音樂(lè)卡頓的問(wèn)題分析及解決(發(fā)生在win10更新至win11后)
Pygame是常用的游戲開發(fā)庫(kù)之一,然而在使用Pygame的過(guò)程中,卻出現(xiàn)了播放背景音樂(lè)卡頓的問(wèn)題,表現(xiàn)為咯咯咯的噪音,所以本文記錄了Pygame出現(xiàn)播放背景音樂(lè)卡頓的問(wèn)題分析及解決,需要的朋友可以參考下2024-02-02
pycharm安裝教程(下載安裝以及設(shè)置中文界面)
這篇文章主要給大家介紹了關(guān)于pycharm安裝教程,文中包括下載安裝以及設(shè)置中文界面,PyCharm是一款Python IDE,其帶有一整套可以幫助用戶在使用Python語(yǔ)言開發(fā)時(shí)提高其效率的工具,需要的朋友可以參考下2023-10-10
Python socket.error: [Errno 98] Address already in use的原因和解決
這篇文章主要介紹了Python socket.error: [Errno 98] Address already in use的原因和解決方法,在Python的socket編程中可能會(huì)經(jīng)常遇到這個(gè)問(wèn)題,需要的朋友可以參考下2014-08-08
Python利用pandas和matplotlib實(shí)現(xiàn)繪制柱狀折線圖
這篇文章主要為大家詳細(xì)介紹了如何使用?Python?中的?Pandas?和?Matplotlib?庫(kù)創(chuàng)建一個(gè)柱狀圖與折線圖結(jié)合的數(shù)據(jù)可視化圖表,感興趣的可以了解一下2023-11-11
python查看矩陣的行列號(hào)以及維數(shù)方式
這篇文章主要介紹了python查看矩陣的行列號(hào)以及維數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-05-05
Python摳圖教程之使用OpenCV實(shí)現(xiàn)背景去除
這篇文章主要給大家介紹了關(guān)于Python摳圖教程之使用OpenCV實(shí)現(xiàn)背景去除的相關(guān)資料,背景去除是在很多視覺(jué)應(yīng)用里的主要預(yù)處理步驟,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-10-10
詳解Python數(shù)據(jù)結(jié)構(gòu)與算法中的順序表
線性表在計(jì)算機(jī)中的表示可以采用多種方法,采用不同存儲(chǔ)方法的線性表也有著不同的名稱和特點(diǎn)。線性表有兩種基本的存儲(chǔ)結(jié)構(gòu):順序存儲(chǔ)結(jié)構(gòu)和鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)。本文將介紹順序存儲(chǔ)結(jié)構(gòu)的特點(diǎn)以及各種基本運(yùn)算的實(shí)現(xiàn)。需要的可以參考一下2022-01-01
matplotlib.pyplot.matshow 矩陣可視化實(shí)例
這篇文章主要介紹了matplotlib.pyplot.matshow 矩陣可視化實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-06-06

