Catmull-Rom在奇异点处的扭曲问题
引言
在计算机图形学和动画中,我们经常需要在已知点之间创建平滑的过渡。Catmull-Rom样条是一种流行的插值方法,它以简单直观的方式生成经过所有控制点的平滑曲线。本文将深入探讨Catmull-Rom插值的原理、实现和应用。
什么是Catmull-Rom样条?
Catmull-Rom样条是一种三次插值样条,由Edwin Catmull和Raphael Rom在1974年提出。它具有以下特点:
- 曲线经过所有控制点(插值性)
- 只需要四个点即可定义一段曲线
- C¹连续(一阶导数连续)
- 局部控制性:移动一个点只影响相邻曲线段
数学原理
基本公式
给定四个控制点P₀, P₁, P₂, P₃,在P₁和P₂之间的曲线段由以下参数方程定义:
P ( t ) = [ 1 t t 2 t 3 ] [ 0 1 0 0 − τ 0 τ 0 2 τ τ − 3 3 − 2 τ − τ − τ 2 − τ τ − 2 τ ] [ P 0 P 1 P 2 P 3 ] \mathbf{P}(t) = \begin{bmatrix} 1 & t & t^2 & t^3 \end{bmatrix} \begin{bmatrix} 0 & 1 & 0 & 0 \\ -\tau & 0 & \tau & 0 \\ 2\tau & \tau-3 & 3-2\tau & -\tau \\ -\tau & 2-\tau & \tau-2 & \tau \end{bmatrix} \begin{bmatrix} \mathbf{P}_0 \\ \mathbf{P}_1 \\ \mathbf{P}_2 \\ \mathbf{P}_3 \end{bmatrix} P(t)=[1tt2t3] 0−τ2τ−τ10τ−32−τ0τ3−2ττ−200−ττ P0P1P2P3
其中:
- t ∈ [0,1] 是插值参数
- τ (tau) 是张力参数,通常取0.5(默认值)
简化形式
展开后可以得到更直观的表达式:
P ( t ) = 1 2 [ − t 3 + 2 t 2 − t 3 t 3 − 5 t 2 + 2 − 3 t 3 + 4 t 2 + t t 3 − t 2 ] T [ P 0 P 1 P 2 P 3 ] \mathbf{P}(t) = \frac{1}{2} \left[ \begin{matrix} -t^3 + 2t^2 - t \\ 3t^3 - 5t^2 + 2 \\ -3t^3 + 4t^2 + t \\ t^3 - t^2 \end{matrix} \right]^T \begin{bmatrix} \mathbf{P}_0 \\ \mathbf{P}_1 \\ \mathbf{P}_2 \\ \mathbf{P}_3 \end{bmatrix} P(t)=21 −t3+2t2−t3t3−5t2+2−3t3+4t2+tt3−t2 T P0P1P2P3
C#代码实现
public static Vector2 CatmullRomInterpolate(Vector2 p0,Vector2 p1,Vector2 p2,Vector2 p3,double t)
{double t2 = t * t;double t3 = t2 * t;double x = 0.5 * ((-p0.X + 3 * p1.X - 3 * p2.X + p3.X) * t3 +(2 * p0.X - 5 * p1.X + 4 * p2.X - p3.X) * t2 +(-p0.X + p2.X) * t +(2 * p1.X));double y = 0.5 * ((-p0.Y + 3 * p1.Y - 3 * p2.Y + p3.Y) * t3 +(2 * p0.Y - 5 * p1.Y + 4 * p2.Y - p3.Y) * t2 +(-p0.Y + p2.Y) * t +(2 * p1.Y));return new Vector2((float)x, (float)y);
}public static List<Vector2> CatmullRomSmooth(List<Vector2> controlPoints,int segmentPerInterval = 10)
{List<Vector2> result = new List<Vector2>();if (controlPoints.Count < 4){return result;}Vector2 virtualFirst = new Vector2(2 * controlPoints[0].X - controlPoints[1].X,2 * controlPoints[0].Y - controlPoints[1].Y);Vector2 virtualLast = new Vector2(2 * controlPoints[controlPoints.Count - 1].X - controlPoints[controlPoints.Count - 2].X,2 * controlPoints[controlPoints.Count - 1].Y - controlPoints[controlPoints.Count - 2].Y);List<Vector2> extendedPoints = new List<Vector2>{virtualFirst,controlPoints[0]};extendedPoints.AddRange(controlPoints);extendedPoints.Add(virtualLast);for (int i = 0; i < extendedPoints.Count - 3; i++){Vector2 p0 = extendedPoints[i];Vector2 p1 = extendedPoints[i + 1];Vector2 p2 = extendedPoints[i + 2];Vector2 p3 = extendedPoints[i + 3];for (int j = 0; j <= segmentPerInterval; j++){double t = (double)j / segmentPerInterval;Vector2 interpolatedPoint = CatmullRomInterpolate(p0, p1, p2, p3, t);result.Add(interpolatedPoint);}}return result;
}
以一个DXF图像作为示例:
可以看出黄色部分做完处理平滑了很多,但是作为代价在多段线导数变化剧烈的地方出现了扭曲的现象,这是因为Catmull-Rom 样条作为三次插值样条,在控制点处保证 C1 连续性(位置和切线连续),但在尖锐拐角处:
- 算法会平滑地连接前后线段
- 切线方向在拐点处突然变化
- 导致曲线在拐点附近产生"过冲"现象
下一篇博客会着重介绍如何消除这种现象