Python图像绘制
1-Hermite图形绘制
Hermite/Catmull-Rom 的本质是:你只给几个关键点,它自动算出经过这些点、且切线连续的平滑曲线
1-Hermite非闭合区间

python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.font_manager import findfont, FontProperties
# ---------- 中文字体 ----------
def setup_chinese_font():
for name in ["Microsoft YaHei", "SimHei", "PingFang SC",
"Hiragino Sans GB", "Noto Sans CJK SC",
"Source Han Sans SC", "WenQuanYi Micro Hei"]:
try:
findfont(FontProperties(family=name), fallback_to_default=False)
matplotlib.rcParams["font.sans-serif"] = [name]
matplotlib.rcParams["axes.unicode_minus"] = False
return name
except Exception:
continue
return None
setup_chinese_font()
def catmull_rom_segment(p0, p1, p2, p3, tension=0.5, n=40):
"""
绘制 p1 -> p2 之间的一段 Catmull-Rom 曲线(三次 Hermite 形式)。
p0, p1, p2, p3: 形如 (x, y) 的点
tension: 切线缩放系数,标准 Catmull-Rom 取 0.5
返回该段的 (xs, ys)
"""
p0, p1, p2, p3 = map(np.asarray, (p0, p1, p2, p3))
# 端点切线(中心差分)
v0 = tension * (p2 - p0) # p1 处切线
v1 = tension * (p3 - p1) # p2 处切线
t = np.linspace(0.0, 1.0, n).reshape(-1, 1) # 列向量,方便广播
# 三次 Hermite 基函数组合
point = (
(2 * p1 - 2 * p2 + v0 + v1) * t ** 3
+ (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t ** 2
+ v0 * t
+ p1
)
return point[:, 0], point[:, 1]
def catmull_rom_chain(points, tension=0.5, n=40, closed=False):
"""
对一串控制点逐段绘制 Catmull-Rom 曲线。
points: [(x, y), ...]
closed: 是否首尾相接(闭合曲线)
"""
pts = list(map(np.asarray, points))
if closed:
# 闭合时首尾循环取邻居
ext = [pts[-1]] + pts + [pts[0], pts[1]]
else:
# 开放曲线:两端复制端点作为"虚拟邻居"
ext = [pts[0]] + pts + [pts[-1]]
xs, ys = [], []
for i in range(len(ext) - 3):
x, y = catmull_rom_segment(ext[i], ext[i + 1], ext[i + 2], ext[i + 3],
tension=tension, n=n)
xs.append(x);
ys.append(y)
return np.concatenate(xs), np.concatenate(ys)
# ---------- 示例数据 ----------
# 一组有点起伏、带急转弯的点,方便观察过冲现象
points = [
(0.0, 0.0),
(1.0, 2.0),
(2.0, -1.0),
(3.5, 1.5),
(4.5, 0.0),
(6.0, 2.5),
(7.0, 0.5),
]
pts = np.array(points)
# ---------- 绘制对比 ----------
fig, axes = plt.subplots(1, 3, figsize=(16, 4.5), sharey=True)
tensions = [0.0, 0.5, 1.0]
titles = ["tension = 0.0 (趋近折线)", "tension = 0.5 (标准 Catmull-Rom)",
"tension = 1.0 (过冲明显)"]
for ax, ten, title in zip(axes, tensions, titles):
# 开放曲线
xs, ys = catmull_rom_chain(points, tension=ten, n=60, closed=False)
ax.plot(xs, ys, '-', lw=2, label='Catmull-Rom')
# 控制点与控制多边形
ax.plot(pts[:, 0], pts[:, 1], 'o', ms=7, color='crimson', label='控制点')
ax.plot(pts[:, 0], pts[:, 1], '--', color='gray', lw=1, alpha=0.6,
label='控制多边形')
ax.set_title(title)
ax.grid(True, alpha=0.3)
ax.legend(fontsize=8)
ax.set_aspect('equal', adjustable='datalim')
plt.tight_layout()
plt.savefig('catmull_rom.png', dpi=130)
plt.show()
2-Hermite闭合区间

python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.font_manager import findfont, FontProperties
# ---------- 中文字体 ----------
def setup_chinese_font():
for name in ["Microsoft YaHei", "SimHei", "PingFang SC",
"Hiragino Sans GB", "Noto Sans CJK SC",
"Source Han Sans SC", "WenQuanYi Micro Hei"]:
try:
findfont(FontProperties(family=name), fallback_to_default=False)
matplotlib.rcParams["font.sans-serif"] = [name]
matplotlib.rcParams["axes.unicode_minus"] = False
return name
except Exception:
continue
return None
setup_chinese_font()
def catmull_rom_segment(p0, p1, p2, p3, tension=0.5, n=40):
"""
绘制 p1 -> p2 之间的一段 Catmull-Rom 曲线(三次 Hermite 形式)。
p0, p1, p2, p3: 形如 (x, y) 的点
tension: 切线缩放系数,标准 Catmull-Rom 取 0.5
返回该段的 (xs, ys)
"""
p0, p1, p2, p3 = map(np.asarray, (p0, p1, p2, p3))
# 端点切线(中心差分)
v0 = tension * (p2 - p0) # p1 处切线
v1 = tension * (p3 - p1) # p2 处切线
t = np.linspace(0.0, 1.0, n).reshape(-1, 1) # 列向量,方便广播
# 三次 Hermite 基函数组合
point = (
(2 * p1 - 2 * p2 + v0 + v1) * t ** 3
+ (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t ** 2
+ v0 * t
+ p1
)
return point[:, 0], point[:, 1]
def catmull_rom_chain(points, tension=0.5, n=40, closed=False):
"""
对一串控制点逐段绘制 Catmull-Rom 曲线。
points: [(x, y), ...]
closed: 是否首尾相接(闭合曲线)
"""
pts = list(map(np.asarray, points))
if closed:
# 闭合时首尾循环取邻居
ext = [pts[-1]] + pts + [pts[0], pts[1]]
else:
# 开放曲线:两端复制端点作为"虚拟邻居"
ext = [pts[0]] + pts + [pts[-1]]
xs, ys = [], []
for i in range(len(ext) - 3):
x, y = catmull_rom_segment(ext[i], ext[i + 1], ext[i + 2], ext[i + 3],
tension=tension, n=n)
xs.append(x);
ys.append(y)
return np.concatenate(xs), np.concatenate(ys)
# ---------- 闭合示例数据 1:近似手绘的心形/花瓣状闭合曲线 ----------
points_closed_1 = [
(2.0, 0.0),
(1.2, 1.3),
(0.0, 1.8),
(-1.2, 1.3),
(-2.0, 0.0),
(-1.2, -1.3),
(0.0, -1.8),
(1.2, -1.3),
]
# ---------- 闭合示例数据 2:不规则闭合多边形(观察过冲) ----------
points_closed_2 = [
(0.0, 0.0),
(3.0, 0.5),
(4.0, 2.5),
(2.0, 4.0),
(-1.0, 3.0),
(-1.5, 1.0),
]
# ---------- 闭合示例数据 3:正五边形顶点(规整、对称) ----------
angles = np.linspace(0, 2 * np.pi, 6)[:-1] + np.pi / 2 # 5 个顶点
points_closed_3 = [(np.cos(a), np.sin(a)) for a in angles]
datasets = [
("闭合曲线 1(近似花瓣)", points_closed_1),
("闭合曲线 2(不规则多边形)", points_closed_2),
("闭合曲线 3(正五边形顶点)", points_closed_3),
]
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
for ax, (title, pts_list) in zip(axes, datasets):
pts = np.array(pts_list)
# 闭合绘制:把最后一段接回起点
xs, ys = catmull_rom_chain(pts_list, tension=0.5, n=80, closed=True)
ax.plot(xs, ys, '-', lw=2, label='Catmull-Rom (closed)')
# 控制点 + 控制多边形(闭合,首尾相连)
ax.plot(np.append(pts[:, 0], pts[0, 0]),
np.append(pts[:, 1], pts[0, 1]),
'o--', ms=7, color='crimson', lw=1, alpha=0.8, label='控制多边形')
ax.set_title(title)
ax.grid(True, alpha=0.3)
ax.legend(fontsize=8)
ax.set_aspect('equal', adjustable='datalim')
plt.tight_layout()
plt.savefig('catmull_rom_closed.png', dpi=130)
plt.show()
3-Hermite绘制赛车数据


python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.font_manager import findfont, FontProperties
# ---------- 中文字体 ----------
def setup_chinese_font():
for name in ["Microsoft YaHei", "SimHei", "PingFang SC",
"Hiragino Sans GB", "Noto Sans CJK SC",
"Source Han Sans SC", "WenQuanYi Micro Hei"]:
try:
findfont(FontProperties(family=name), fallback_to_default=False)
matplotlib.rcParams["font.sans-serif"] = [name]
matplotlib.rcParams["axes.unicode_minus"] = False
return name
except Exception:
continue
return None
setup_chinese_font()
# ---------- Catmull-Rom(带逐段返回,便于着色) ----------
def catmull_rom_segment(p0, p1, p2, p3, tension=0.5, n=60):
p0, p1, p2, p3 = map(np.asarray, (p0, p1, p2, p3))
v0 = tension * (p2 - p0)
v1 = tension * (p3 - p1)
t = np.linspace(0.0, 1.0, n).reshape(-1, 1)
point = ((2 * p1 - 2 * p2 + v0 + v1) * t ** 3
+ (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t ** 2
+ v0 * t + p1)
return point[:, 0], point[:, 1]
def catmull_rom_closed(points_xy, tensions, n=60):
"""
闭合 Catmull-Rom。返回每段的 (xs, ys, 段标号)。
tensions: 与 points_xy 等长,每个点作为 p1 时的 tension。
"""
pts = list(map(np.asarray, points_xy))
m = len(pts)
segs = []
for i in range(m):
p0 = pts[(i - 1) % m]
p1 = pts[i]
p2 = pts[(i + 1) % m]
p3 = pts[(i + 2) % m]
x, y = catmull_rom_segment(p0, p1, p2, p3,
tension=tensions[i], n=n)
segs.append((x, y, i))
return segs
# ---------- 你的数据 ----------
data = [
[-260, 180, 0], [-60, 185, 2], [90, 175, 6], [210, 130, 10],
[260, 20, 12], [230, -90, 8], [110, -140, 4], [10, -100, 2],
[-90, -150, 0], [-220, -170, 4], [-300, -80, 8], [-320, 60, 10],
]
data = np.array(data, dtype=float)
xy = data[:, :2]
w = data[:, 2] # 第三维:权重/强度
# tension = 0.5 固定 + 第三维按比例缩放(0 -> 0.2, 12 -> 0.8)
tension_per_point = 0.2 + 0.6 * (w - w.min()) / (w.max() - w.min() + 1e-9)
# ---------- 绘制 ----------
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
# 左图:全部 tension=0.5,第三维用颜色/大小表示
ax = axes[0]
segs = catmull_rom_closed(xy, np.full(len(xy), 0.5), n=80)
cmap = plt.cm.viridis
norm = plt.Normalize(w.min(), w.max())
for x, y, i in segs:
ax.plot(x, y, '-', lw=2.5, color=cmap(norm(w[i])))
ax.scatter(xy[:, 0], xy[:, 1], c=w, cmap=cmap, norm=norm,
s=60 + 20 * w, edgecolors='k', zorder=5)
for i, (x, y, wi) in enumerate(zip(xy[:, 0], xy[:, 1], w)):
ax.annotate(f"p{i}\n({x:.0f},{y:.0f}), w={wi:.0f}",
(x, y), textcoords="offset points", xytext=(8, 8),
fontsize=8, alpha=0.85)
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm);
sm.set_array([])
fig.colorbar(sm, ax=ax, label="第三维(权重/强度)", shrink=0.8)
ax.set_title("闭合 Catmull-Rom(所有点 tension=0.5)", fontsize=12)
ax.set_xlabel("X");
ax.set_ylabel("Y")
ax.grid(True, alpha=0.3);
ax.set_aspect('equal', adjustable='datalim')
# 右图:第三维映射为每点 tension,观察形状变化
ax = axes[1]
segs2 = catmull_rom_closed(xy, tension_per_point, n=80)
for x, y, i in segs2:
ax.plot(x, y, '-', lw=2.5, color=cmap(norm(w[i])))
ax.scatter(xy[:, 0], xy[:, 1], c=w, cmap=cmap, norm=norm,
s=60 + 20 * w, edgecolors='k', zorder=5)
for i, (x, y, tval) in enumerate(zip(xy[:, 0], xy[:, 1], tension_per_point)):
ax.annotate(f"p{i}\nt={tval:.2f}", (x, y),
textcoords="offset points", xytext=(8, 8),
fontsize=8, alpha=0.85)
sm2 = plt.cm.ScalarMappable(cmap=cmap, norm=norm);
sm2.set_array([])
fig.colorbar(sm2, ax=ax, label="第三维(权重/强度)", shrink=0.8)
ax.set_title("第三维映射为每点 tension(0.2~0.8)", fontsize=12)
ax.set_xlabel("X");
ax.set_ylabel("Y")
ax.grid(True, alpha=0.3);
ax.set_aspect('equal', adjustable='datalim')
fig.suptitle("闭合 Catmull-Rom 样条:用户数据可视化", fontsize=14)
plt.tight_layout()
plt.savefig('closed_catmull_rom_user_data.png', dpi=130)
plt.show()
2-Hermite是什么?
在 Three.js 的曲线体系中,CubicPoly.initCatmullRom 并非一个独立的标准 API,但你提到的 "Hermite 基函数" 和 CubicPoly.initCatmullRom 很可能指向 Three.js 内部用于实现 CatmullRomCurve3 的三次多项式插值实现(源码位于 src/extras/curves/CubicBezierCurve 相关的 CubicPoly 工具类,或旧版本 CurveUtils 中的实现)。
背景:Catmull-Rom 与 Hermite 的关系
Catmull-Rom 样条本质上是一种 Hermite 样条 。Hermite 插值需要每个点的位置和切线(导数),而 Catmull-Rom 的巧妙之处在于:它自动从相邻点估算切线,无需用户手动指定。具体地,对于点 P1 处的切线,通常取:
m1=P2−P02m_1 = \frac{P_2 - P_0}{2}m1=2P2−P0
即前后相邻点差的一半(可带张力参数)。有了每段起止点的位置和切线,就可以用 三次 Hermite 基函数 在该段内插值。
三次 Hermite 基函数
给定区间 t∈0,1t \in 0,1t∈0,1,段起点 P0P_0P0、终点 P1P_1P1、起点切线 m0m_0m0、终点切线 m1m_1m1,插值结果为:
P(t)=h00(t)P0+h10(t)m0+h01(t)P1+h11(t)m1P(t) = h_{00}(t)P_0 + h_{10}(t)m_0 + h_{01}(t)P_1 + h_{11}(t)m_1P(t)=h00(t)P0+h10(t)m0+h01(t)P1+h11(t)m1
四个基函数为:
- h00(t)=2t3−3t2+1h_{00}(t) = 2t^3 - 3t^2 + 1h00(t)=2t3−3t2+1
- h10(t)=t3−2t2+th_{10}(t) = t^3 - 2t^2 + th10(t)=t3−2t2+t
- h01(t)=−2t3+3t2h_{01}(t) = -2t^3 + 3t^2h01(t)=−2t3+3t2
- h11(t)=t3−t2h_{11}(t) = t^3 - t^2h11(t)=t3−t2
这些多项式满足端点条件:h00(0)=1h_{00}(0)=1h00(0)=1、h10′(0)=1h_{10}'(0)=1h10′(0)=1 等,保证曲线经过控制点且切线匹配。
CubicPoly.initCatmullRom 的作用
在 Three.js 的实现中,CubicPoly 是一个轻量的三次多项式容器(保存 c0, c1, c2, c3 系数)。initCatmullRom 做的事情是:把上述 Hermite 形式展开成标准多项式系数形式,即把
P(t)=c0+c1t+c2t2+c3t3P(t) = c_0 + c_1 t + c_2 t^2 + c_3 t^3P(t)=c0+c1t+c2t2+c3t3
的系数预先算好并缓存。这样在采样时只需做几次乘加(Horner 法),比每次重复计算基函数更高效。展开后系数为:
- c0=P0c_0 = P_0c0=P0
- c1=m0c_1 = m_0c1=m0
- c2=−3P0+3P1−2m0−m1c_2 = -3P_0 + 3P_1 - 2m_0 - m_1c2=−3P0+3P1−2m0−m1
- c3=2P0−2P1+m0+m1c_3 = 2P_0 - 2P_1 + m_0 + m_1c3=2P0−2P1+m0+m1
其中切线按 Catmull-Rom 规则从相邻点算出(可乘以张力/向心参数)。此外,Three.js 还提供 initNonuniformCatmullRom,用于向心/弦长参数化(centripetal/chordal),通过调整切线权重避免尖点和自交,这在实际建模中更常用。
小结
- Hermite 基函数是三次插值的数学基础,用位置+切线描述一段曲线。
- Catmull-Rom 是 Hermite 的特例,切线由邻点自动估算。
CubicPoly.initCatmullRom是把 Hermite/Catmull-Rom 插值预展开为多项式系数的优化实现,供CatmullRomCurve3等曲线快速采样使用。
3-Hermite做什么?
Hermite 基函数 / Catmull-Rom 能用来做什么?
简单说:能,而且非常适合做跑道。但它的能力远不止于此。下面按"用途分类"讲清楚,最后专门说跑道怎么做。
一、核心能力:由少量控制点生成平滑曲线
Hermite/Catmull-Rom 的本质是:你只给几个关键点,它自动算出经过这些点、且切线连续的平滑曲线。这条曲线可以:
- 按参数 t 采样任意位置 (
getPoint(t)) - 按 t 求切线方向 (
getTangent(t))------决定"朝向" - 按弧长均匀采样 (
getSpacedPoints/getPointAt)------决定"匀速"
这三点是所有应用的基础。
二、典型用途
1. 路径 / 轨迹(最直接)
- 物体沿曲线移动 :赛车、NPC、摄像机、飞行器沿预设路线走。用
getPointAt(u)保证匀速,用getTangentAt(u)让车头朝向切线方向。 - 相机运镜:过场动画里相机沿平滑路径飞过场景。
- 导弹/子弹轨迹:曲线追踪目标。
2. 道路 / 跑道 / 赛道生成 ✅
这正是你问的场景,见下文详述。
3. 管道 / 轨道 / 线缆
- 沿曲线用
TubeGeometry生成管道、过山车轨道、电缆、藤蔓。 - 用曲线做扫掠(sweep)的脊线。
4. 地形与河流
- 河流中心线、道路网、山脊线,再用宽度偏移生成两侧边界。
5. 动画与运动插值
- 关键帧之间的平滑过渡(位置、甚至颜色/数值曲线)。
- UI 元素的缓动路径。
6. 程序化建模
- 挤出(extrude)沿曲线的截面,生成栏杆、围墙、装饰带。
三、专门说:游戏跑道怎么做
Catmull-Rom 做跑道是非常经典且合适的方案,流程大致如下:
步骤 1:定义跑道中心线
用一组控制点描述赛道走向(弯道处多放点,直道少放点):
js
const points = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(50, 0, 20),
new THREE.Vector3(80, 0, 80), // 弯道
new THREE.Vector3(40, 0, 140),
// ...
];
const curve = new THREE.CatmullRomCurve3(points, true); // true = 闭合赛道
步骤 2:沿曲线生成路面网格
对每个采样点,取位置 + 切线 + 法线/副法线 ,构造"左边界点"和"右边界点"(沿横向偏移半个路宽),然后把左右边界连成三角带(triangle strip)。这就是路面。用 curve.computeFrenetFrames() 可以拿到每个点的坐标系,避免路面翻转。
步骤 3:派生其他元素
- 边线 / 护栏 :左右边界再向外偏移,用
TubeGeometry或扫掠生成。 - 路面纹理 UV:沿弧长方向的 u 用累计长度归一化,横向 v 用 0~1,贴图不会拉伸。
- 检查点 / 起终点线:在特定 t 处取点和切线,放置门框或触发区。
步骤 4:物理与 AI
- 车辆约束:把车的位置投影到曲线上,得到"赛道进度"(用于排名、圈数判定)。
- AI 寻路:NPC 沿中心线行驶,向左右偏移做超车。
- 边界检测:计算车到中心线的横向距离,超出路宽即"出界"。
为什么 Catmull-Rom 适合跑道
- 曲线过控制点:你摆的点就是赛道真实经过的位置,直观可控。
- C1 连续:切线连续,车行驶不会突然"折角",视觉和物理都平滑。
- 参数化可调 :向心参数化(
initNonuniformCatmullRom)能避免急弯处出现尖点/自交,这对赛道很重要。 - 计算便宜:预展开成多项式系数后,采样只是几次乘加,实时性能好。
注意事项
- 闭合赛道 :构造时传
closed = true,首尾切线自动衔接。 - 弯道采样密度 :急弯处
getSpacedPoints采样要密一些,否则路面多边形化明显。 - 路面不扭曲 :别直接用
getTangent手工叉乘算横向,弯道+坡度时容易翻转,优先用computeFrenetFrames或自己维护 up 向量做正交化。 - 坡度:控制点带 y 值即可做上下坡,但注意 Frenet 框架在垂直段会退化,跑道一般坡度平缓问题不大。
四、一句话总结
Hermite 基函数 / Catmull-Rom 是"用少量点生成平滑可采样曲线 "的工具;跑道只是它最典型的应用之一------中心线用曲线描述,路面/护栏/UV/物理判定都由这条曲线派生。同样这套方法还能做轨道、河流、相机路径、管道等一切"沿一条平滑线展开"的东西。