Python-Hermite基函数

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 的本质是:你只给几个关键点,它自动算出经过这些点、且切线连续的平滑曲线。这条曲线可以:

  1. 按参数 t 采样任意位置 (getPoint(t))
  2. 按 t 求切线方向 (getTangent(t))------决定"朝向"
  3. 按弧长均匀采样 (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/物理判定都由这条曲线派生。同样这套方法还能做轨道、河流、相机路径、管道等一切"沿一条平滑线展开"的东西。


相关推荐
空~自由1 小时前
Python 正则 . * ? .* .*? (.*?) 通俗讲解
开发语言·python
Web3&Basketball1 小时前
DeepSeek V4.1 Flash 实测:Token 砍半、Search API 跑通
数据库·人工智能·python·大模型·api调用·deepseek·推理优化
墨子夕1 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(第一篇)
java·python·设计模式
程序 代码狂人2 小时前
经典 SQL 面试题:连续登录问题的两种经典解法
java·开发语言·算法
Linux实战笔记2 小时前
凌晨 3 点接口超时,排查发现 asyncio 协程里混用了同步 requests 库
python·asyncio·aiohttp·事件循环阻塞·协程调试
weixin_BYSJ19872 小时前
【计算机毕设】房屋租赁系统02647
java·vue.js·spring boot·python·django·课程设计
在世修行2 小时前
智学文枢:数据归谁管、变了怎么通知
python·桌面应用·状态管理·signal·store·pyside
星夜离尘562 小时前
MATLAB 只剩批次摘要怎么合并方差:用 n、mean、M2 保留批间差异
开发语言·matlab