用于相机动画的 Overhauser(Catmull-Rom)样条






4.85/5 (24投票s)
从游戏开发者的角度介绍 Overhauser 样条曲线,附带 C++ 示例代码

引言
许多人对游戏中逼真的相机动画或多媒体演示印象深刻。通常被称为相机插值的数学原理其实相当简单。在本文中,我将重点介绍一种简单的算法,该算法使用一类称为Overhauser或Catmull-Rom的样条曲线,并展示它们如何以及为何优于其他类似的方法。
数学是你的朋友
你可能会因此讨厌我,但数学确实很美妙。在本节中,我们将回顾矢量微积分的知识,这将帮助我们更好地理解示例代码。
让我们从基础开始:一条经过其控制点的曲线被称为插值这些点。贝塞尔曲线仅插值其 4 个控制点中的 2 个,而 B 样条曲线则不插值指定的任何控制点(曲线围绕这些点平滑地通过)。Catmull-Rom 样条曲线,也称为 Overhauser 样条曲线,属于一类称为Hermite 样条的曲线。它们是均匀有理三次多项式曲线,可以在 N 个控制点之间进行插值,并且正好通过 N-2 个控制点(除了第一个和最后一个)。它们是均匀的,因为控制点(也称为节点)在曲线参数 (t
) 方面以相等的间隔间隔开。插值以分段方式进行:在每对点之间定义一个新的三次曲线。
Catmull-Rom 样条曲线的参数方程由下式给出
其中矢量 V 和 T 以及矩阵 M 为
我们可以直接使用此方程,并使用矢量和矩阵乘法来编写我们的解决方案。虽然可行,但这可能效率不高。让我们稍微简化一下方程。我鼓励您仔细检查我的数学 - 这很有趣。通过将水平矢量 T 与矩阵 M 相乘,并考虑垂直矢量,我们得到
其中 b1
...b4
是 t
的三次多项式
图 3A 显示了最终方程的成员。P1
...P4
是控制点。在 3D 中,Pn
是齐次或非齐次矢量(3 或 4 个坐标)。在 2D 中,它们是 2 坐标矢量。
这一切到底是什么意思?嗯,它的意思是,如果您知道 N 个中间位置以及相机在 N 个时刻的角度/轴对,您就可以通过使用上述 Eq 3A 在 N-2 个位置和角度/轴对之间进行插值来生成准确平滑的相机动画。相机将通过所有中间的 N-2 个点。[注意:如果您将起始点和结束点加倍,相机将通过所有 N 个位置。]
编码实现
首先,我们需要一个类来为我们的控制点 Pn
提供抽象。我们将编写一个最小的 3D 矢量类,只包含几个操作。您可以根据需要扩展它。另请注意,随附的示例应用程序将这些矢量的 Z 坐标置零,并主要将其用于绘制 2D 曲线。但是,该包完全能够计算 3D 样条曲线!让我们回顾一下 3D 矢量类 vec3
/// Minimal 3-dimensional vector abstraction
class vec3
{
public:
// Constructors
vec3() : x(0), y(0), z(0)
{}
vec3(float vx, float vy, float vz)
{
x = vx;
y = vy;
z = vz;
}
vec3(const vec3& v)
{
x = v.x;
y = v.y;
z = v.z;
}
// Destructor
~vec3() {}
// A minimal set of vector operations
vec3 operator * (float mult) const // result = this * arg
{
return vec3(x * mult, y * mult, z * mult);
}
vec3 operator + (const vec3& v) const // result = this + arg
{
return vec3(x + v.x, y + v.y, z + v.z);
}
vec3 operator - (const vec3& v) const // result = this - arg
{
return vec3(x - v.x, y - v.y, z - v.z);
}
float x, y, z;
};
相当简单。现在我们将引入一个新类来抽象我们的样条曲线。
#include "vec3.hpp"
#include
class CRSpline
{
public:
// Constructors and destructor
CRSpline();
CRSpline(const CRSpline&);
~CRSpline();
// Operations
void AddSplinePoint(const vec3& v);
vec3 GetInterpolatedSplinePoint(float t); // t = 0...1; 0=vp[0] ... 1=vp[max]
int GetNumPoints();
vec3& GetNthPoint(int n);
// Static method for computing the Catmull-Rom parametric equation
// given a time (t) and a vector quadruple (p1,p2,p3,p4).
static vec3 Eq(float t, const vec3& p1, const vec3& p2,
const vec3& p3, const vec3& p4);
private:
std::vector<vec3> vp;
float delta_t;
};
这同样相当直观:CRSpline
类本质上是多个控制点(表示为 std::vector
)的容器。它有一个 static
成员函数,用于为给定的参数 t
和四个控制点 P1
...P4
求解样条方程。该函数返回一个 3 坐标矢量,这是给定 4 个控制点之间,给定 t
值的插值结果。
AddSplinePoint
和 GetInterpolatedSplinePoint
方法允许我们指定 2D/3D 曲线的控制点并获取平滑曲线。让我们快速看一下后者,它包含最后一点棘手的逻辑
vec3 CRSpline::GetInterpolatedSplinePoint(float t)
{
// Find out in which interval we are on the spline
int p = (int)(t / delta_t);
// Compute local control point indices
#define BOUNDS(pp) { if (pp < 0) pp = 0;
else if (pp >= (int)vp.size()-1) pp = vp.size() - 1; }
int p0 = p - 1; BOUNDS(p0);
int p1 = p; BOUNDS(p1);
int p2 = p + 1; BOUNDS(p2);
int p3 = p + 2; BOUNDS(p3);
// Relative (local) time
float lt = (t - delta_t*(float)p) / delta_t;
// Interpolate
return CRSpline::Eq(lt, vp[p0], vp[p1], vp[p2], vp[p3]);
}
如上面的代码所示,GetInterpolatedSplinePoint
函数将样条曲线分成 4 点段,相对于局部段转换参数 t
,然后使用静态方程求解器获取最终结果。该函数假定 t
的范围从 0
到 1
,其中 0
代表样条曲线的“开始”(第一个控制点),1
代表样条曲线的“结束”(最后一个控制点)。如果这不合适,您可以自己制定时间方案;但请记住调整上面 p
和 lt
的计算。
任何 2D 或 3D 矢量序列都可以以类似的方式进行插值。例如,相机的角度/轴,场景中各种移动对象的 the positions and orientation 等。与使用 b 样条曲线或贝塞尔曲线相比,这种方法的优点是生成的曲线触及所有的控制点(再次,请确保加倍第一个和最后一个控制点以实现此目的)。
Using the Code
在这里,我包含了一个使用该库的非常原始的应用程序,用 Borland Dev Studio 4 实现。它基本上实例化了 CRSpline
类并用伪随机控制点填充它,然后使用 BDS 的 TCanvas
接口在常规对话框的画布上绘制样条曲线。

希望我这段简短的数学讲解没有让您感到无聊。请享用代码。
参考文献
- 计算机图形学:原理与实践;Foley, van Dam, Feiner, Hughes;Addison-Wesley, 1997
- Catmull, E. 和 R. Rom,“一类局部插值样条曲线”;计算机辅助几何设计;Academic Press, San Francisco, 1974
历史
- 2008年11月7日:初次发布