首页
/ three.js 插值工具模块 Interpolations 深入解析:Catmull-Rom 样条与 Bézier 曲线的底层实现

three.js 插值工具模块 Interpolations 深入解析:Catmull-Rom 样条与 Bézier 曲线的底层实现

2026-09-08 20:09:12作者:戚魁泉Nursing

Interpolations 是 three.js 中一组面向曲线求值的内置标量插值函数集合,位于 src/extras/core/Interpolations.js,提供 CatmullRomCubicBezierQuadraticBezier 三个核心方法。它并不被当作独立类直接暴露给使用者,而是作为底层标量插值引擎,被 2D/3D 的 Bézier 曲线与样条曲线类(CubicBezierCurveQuadraticBezierCurveCubicBezierCurve3QuadraticBezierCurve3SplineCurve 等)内部调用,逐维求出曲线上的点。阅读本文后,你将理解这三个函数的参数语义、完整数学公式与源码实现、它们与具体曲线类之间的调用链,并能熟练地把插值函数用于构建平滑轨迹、轮廓、挤出与管线几何体。

Interpolations 在 three.js 中的定位

一个“只服务内部”的工具模块

从源码注释(src/extras/core/Interpolations.js)可以明确看到它的定位:

Interpolations contains spline and Bézier functions internally used by concrete curve classes. (Interpolations 包含被具体曲线类内部使用的样条与 Bézier 函数。)

其公式出处标注为维基百科的 Bézier 曲线条目,也就是说该模块是数学公式的可直接执行翻译,不维护任何状态,不做内存缓存,也不耦合渲染。三个函数全部是纯函数:相同的 t 与相同控制点输入,必然得到相同输出。这一设计让它们可以在每帧、每个顶点上被高频安全调用。

模块整体结构(src/extras/core/Interpolations.js)非常简单:

  • 1 个样条函数 CatmullRom(t, p0, p1, p2, p3)
  • 1 个三次 Bézier 函数 CubicBezier(t, p0, p1, p2, p3),内部由 4 个 Bernstein 基函数分量 CubicBezierP0~CubicBezierP3 求和得到
  • 1 个二次 Bézier 函数 QuadraticBezier(t, p0, p1, p2),内部由 3 个 Bernstein 基函数分量 QuadraticBezierP0~QuadraticBezierP2 求和得到

文件末尾统一导出:

export { CatmullRom, QuadraticBezier, CubicBezier };

标量插值:它只算“一维的数值”

需要注意的是,这三个函数操作的是单个数值number),而不是 Vector2/Vector3。曲线类拿到二维/三维控制点后,会把每个坐标轴拆开,分别调用插值函数。例如二维三次 Bézier 曲线内部把 xy 分量分开插值,三维则再对 z 分量做一次,详见下文“调用链”一节。

三个函数的参数语义与完整公式

官方文档 docs/pages/module-Interpolations.html.md 对三个方法逐一说明了参数含义,现整理如下。三者共享的核心参数是插值因子 t,它表示目标点沿曲线(或样条段)的相对位置,取值区间为 [0, 1]t = 0 位于段起点,t = 1 位于段终点。控制点 p0~p3 的几何含义依曲线类型而不同。

.CatmullRom( t, p0, p1, p2, p3 ):Catmull-Rom 样条段

参数 类型 含义
t number 插值因子,取值 [0, 1],决定返回点在当前样条段内的位置
p0 number 第一个控制点(当前段的前一个点,用作切向约束)
p1 number 第二个控制点(当前段的起点
p2 number 第三个控制点(当前段的终点
p3 number 第四个控制点(当前段的后一个点,用作切向约束)
返回 number Catmull-Rom 样条上的计算结果点

Catmull-Rom 的核心思想是:要求曲线穿过 p1p2,并在两端处的切线与“相邻点连线”方向一致。three.js 的实现采用经典的中心差分形式,先计算两个切向量 v0v1

const v0 = ( p2 - p0 ) * 0.5;   // 起点 p1 处的切线方向,取 p0→p2 中点差分
const v1 = ( p3 - p1 ) * 0.5;   // 终点 p2 处的切线方向,取 p1→p3 中点差分

再代入三次 Hermite 多项式,得到结果(源码见 src/extras/core/Interpolations.js):

const t2 = t * t;
const t3 = t * t2;
return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3
     + ( - 3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2
     + v0 * t + p1;

等价地写成教材式公式为:

CatmullRom(t) = (2·p1 − 2·p2 + v0 + v1)·t³
              + (−3·p1 + 3·p2 − 2·v0 − v1)·t²
              + v0·t + p1

可以验证边界性质:t = 0 时恰返回 p1t = 1 时恰返回 p2,即样条精确穿过内部两个控制点(interpolating spline),这是与“逼近型”Bézier 曲线的本质区别。

.CubicBezier( t, p0, p1, p2, p3 ):三次 Bézier 曲线

参数 类型 含义
t number 插值因子,取值 [0, 1]
p0 number 第一个控制点(曲线起点,被精确穿过)
p1 number 第二个控制点(仅引导方向,曲线一般不穿过)
p2 number 第三个控制点(仅引导方向,曲线一般不穿过)
p3 number 第四个控制点(曲线终点,被精确穿过)
返回 number 三次 Bézier 曲线上的计算结果点

实现把完整公式拆成 4 个独立的 Bernstein 基函数分量(src/extras/core/Interpolations.js),每个分量只负责“一个控制点乘以它在 t 处的权重”:

function CubicBezierP0( t, p ) {          // B₀ = (1−t)³
	const k = 1 - t;
	return k * k * k * p;
}
function CubicBezierP1( t, p ) {          // B₁ = 3(1−t)²t
	const k = 1 - t;
	return 3 * k * k * t * p;
}
function CubicBezierP2( t, p ) {          // B₂ = 3(1−t)t²
	return 3 * ( 1 - t ) * t * t * p;
}
function CubicBezierP3( t, p ) {          // B₃ = t³
	return t * t * t * p;
}

function CubicBezier( t, p0, p1, p2, p3 ) {
	return CubicBezierP0( t, p0 ) + CubicBezierP1( t, p1 )
	     + CubicBezierP2( t, p2 ) + CubicBezierP3( t, p3 );
}

等价公式:

CubicBezier(t) = (1−t)³·p0 + 3(1−t)²·t·p1 + 3(1−t)·t²·p2 + t³·p3

由于四个 Bernstein 基函数恒有 B0+B1+B2+B3 = 1,三次 Bézier 曲线必然穿过 p0(t=0)与 p3(t=1),曲线始终落在控制点的凸包内;p1 决定起点切线方向(沿 p1−p0),p2 决定终点切线方向(沿 p3−p2)。这是它被广泛用于字体轮廓、动画缓动与矢量路径的数学基础。

.QuadraticBezier( t, p0, p1, p2 ):二次 Bézier 曲线

参数 类型 含义
t number 插值因子,取值 [0, 1]
p0 number 第一个控制点(曲线起点
p1 number 第二个控制点(仅引导方向
p2 number 第三个控制点(曲线终点
返回 number 二次 Bézier 曲线上的计算结果点

实现同样按 Bernstein 基拆分(src/extras/core/Interpolations.js):

function QuadraticBezierP0( t, p ) {          // B₀ = (1−t)²
	const k = 1 - t;
	return k * k * p;
}
function QuadraticBezierP1( t, p ) {          // B₁ = 2(1−t)t
	return 2 * ( 1 - t ) * t * p;
}
function QuadraticBezierP2( t, p ) {          // B₂ = t²
	return t * t * p;
}

function QuadraticBezier( t, p0, p1, p2 ) {
	return QuadraticBezierP0( t, p0 ) + QuadraticBezierP1( t, p1 )
	     + QuadraticBezierP2( t, p2 );
}

等价公式:

QuadraticBezier(t) = (1−t)²·p0 + 2(1−t)·t·p1 + t²·p2

从结构可以清晰看出模块的设计模式:每个 Bézier 函数都是一个 N 次多项式,被拆成 N+1 个 Bernstein 基函数分量累加。这种写法除了代码可读性好,还便于将来单独复用每个基权重(例如计算切线/导数时直接对基函数求导)。

谁在调用它们:源码级调用链

通过仓库内的 import 语句可以精确还原调用关系(见 src/extras/curves 目录下的各个曲线类):

插值函数 导入方曲线类 维度 调用位置
CatmullRom SplineCurve 2D(x、y 分开插值) SplineCurve.js#L83-L84
CubicBezier CubicBezierCurve 2D CubicBezierCurve.js#L96-L97
CubicBezier CubicBezierCurve3 3D CubicBezierCurve3.js#L79-L81
QuadraticBezier QuadraticBezierCurve 2D QuadraticBezierCurve.js#L87-L88
QuadraticBezier QuadraticBezierCurve3 3D QuadraticBezierCurve3.js#L71-L73

2D 三次 Bézier:CubicBezierCurve

CubicBezierCurve 为例,它的 getPoint(t) 把 4 个二维控制点 v0…v3xy 分量分别送入三次插值再组装成 Vector2

getPoint( t, optionalTarget = new Vector2() ) {
	...
	return point.set(
		CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),   // x 分量
		CubicBezier( t, v0.y, v1.y, v2.y, v3.y )    // y 分量
	);
}

CubicBezierCurve3(三维版本,src/extras/curves/CubicBezierCurve3.js#L72-L82)则在 xy 之外再对 z 分量插值一次。这种“同一标量函数、逐维调用”的模式贯穿整个曲线体系。

2D Catmull-Rom 样条:SplineCurve

SplineCurve 是文档中 CatmullRom 唯一的直接消费者。给定 N 个二维点后,它把每个样条段对应一组 4 个相邻点(p0~p3),用插值因子换算段内位置:

const p = ( points.length - 1 ) * t;   // t∈[0,1] 映射到总段数
const intPoint = Math.floor( p );
const weight = p - intPoint;           // 段内插值因子,等价于 CatmullRom 的 t

// 选取当前段前后各两个点(越界处用端点收拢)
const p0 = points[ intPoint === 0 ? intPoint : intPoint - 1 ];
const p1 = points[ intPoint ];
const p2 = points[ intPoint > points.length - 2 ? points.length - 1 : intPoint + 1 ];
const p3 = points[ intPoint > points.length - 3 ? points.length - 1 : intPoint + 2 ];

point.set(
	CatmullRom( weight, p0.x, p1.x, p2.x, p3.x ),
	CatmullRom( weight, p0.y, p1.y, p2.y, p3.y )
);

可以看到:段内权重 weight 就是传给 CatmullRomt,首末段缺少的相邻点直接用自身端点补齐,从而保证曲线在整段上连续且不越界访问数组。

注意:CatmullRomCurve3 走的是另一条专用实现

这里有一个容易混淆的细节:三维的 CatmullRomCurve3 并没有 import 本模块的 CatmullRom,而是在文件内实现了带缓存的 CubicPolyCatmullRomCurve3.js#L4-L76),先解出三次多项式的 4 个系数再求值,以便支持 centripetal(向心,默认)、chordal(弦长)与 catmullrom(带 tension,默认 0.5)三种参数化类型。相比之下,Interpolations.CatmullRom 相当于 tension 固定为 0.5 的均匀参数化版本,因为它的中心差分系数 0.5 恰是 tension 的默认值。若需要可调节张力或避免尖点/自交的三维样条,应选用 CatmullRomCurve3 并设置其 curveType 参数。

从标量到场景:Interpolations 支撑的完整链路

借助 Curve 基类生成离散点集

所有导入这些函数的曲线类都继承自 Curve,基类在 getPoint(t) 之上封装了两条常用的采样接口:

  • getPoints( divisions ):按参数 t 均匀divisions 个点,适合表现曲线形状本身;
  • getSpacedPoints( divisions ):按弧长均匀取点(内部先做弧长表,再反向映射),适合放置沿轨道的物体、粒子或相机动画,保证速度一致。

例如用 SplineCurve 画一条正弦形态的 2D 折线再交给 Line 渲染(代码模式见 SplineCurve.js#L8-L25):

import * as THREE from 'three';

// 创建一条类正弦波(二维样条点)
const curve = new THREE.SplineCurve( [
	new THREE.Vector2( -10, 0 ),
	new THREE.Vector2( -5, 5 ),
	new THREE.Vector2( 0, 0 ),
	new THREE.Vector2( 5, -5 ),
	new THREE.Vector2( 10, 0 )
] );

// getPoints 内部会沿曲线逐段调用 CatmullRom
const points = curve.getPoints( 50 );
const geometry = new THREE.BufferGeometry().setFromPoints( points );

const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const splineObject = new THREE.Line( geometry, material );
scene.add( splineObject );

这里的每一次 CatmullRom( weight, p0.x, … ) 都是标量层面的插值,最终拼合成曲线上一个可见的顶点。完整的交互式示例可参考 examples/webgl_geometry_spline_editor.html(样条编辑)、examples/webgl_geometry_extrude_splines.html(沿样条挤出)。

供 ExtrudeGeometry、TubeGeometry 等上层几何体复用

Bézier 与样条曲线对象被 three.js 上层几何体大量引用,例如 ExtrudeGeometry 用曲线(含 SplineCurve/Bézier 曲线构造的 Shape)定义挤出截面与斜角轮廓,TubeGeometry 需要沿任意曲线(含 CatmullRomCurve3)管道化。只要曲线类正确实现 getPoint,就自然接入 Curve 基类的弧长采样、切线计算与 TubeGeometry 的 Frenet 框架管线——这正是 Interpolations 这类底层标量函数“一改俱动”的价值所在。仓库的 examples/webgl_modifier_curve.html 展示了沿贝塞尔曲线路径生成大量实例的实际效果。

测试情况与验证方式

仓库为插值模块预留了单元测试挂载点 test/unit/src/extras/core/Interpolations.tests.js,以 QUnit 组织了 Extras > Core > Interpolations 模块。目前该文件仅为空壳骨架(相关 import 与用例被注释),真正的行为验证更多落在其上层消费者上——例如对 CubicBezierCurveSplineCurveCatmullRomCurve3 等的取样、getPoint 端点命中、getTangent 与弧长采样测试。读者如果要在自己的项目中验证这三个纯函数的正确性,可自行断言以下关键性质:

  • CatmullRom(0, …, p1, …) === p1CatmullRom(1, …, …, p2, …) === p2(穿过内部点);
  • CubicBezier(0, p0, …, …) === p0CubicBezier(1, …, …, p3) === p3(穿过首尾控制点);
  • QuadraticBezier(0, p0, …) === p0QuadraticBezier(1, …, p2) === p2

边界、注意事项与选型建议

  • t 的取值约定:三个函数的 t 都应在 [0, 1] 内。虽然多项式在区间外也有定义(可实现延展),但 three.js 曲线体系内部约定 t ∈ [0, 1](见各曲线类 getPoint 的 JSDoc 注释),区间外行为不做保证。
  • 函数为内部模块Interpolations 的 API 文档明确标注“(inner)”,其导出经由 src/Three.js 等入口聚合后,通常不直接暴露为 THREE.CatmullRom 这样的顶层符号。绝大多数场景下你应当使用具体曲线类(SplineCurveCubicBezierCurve(3)QuadraticBezierCurve(3)CatmullRomCurve3),而不是直接调用这些标量函数。
  • 样条插值 vs 逼近CatmullRom插值型(穿过 p1p2),Bézier 系列只穿过首尾控制点,中间控制点只“牵引”方向。理解这一点是合理布置控制点的前提。
  • 二维还是三维:2D 曲线类输出 Vector2(多用于 Shape/ExtrudeGeometry 截面与文字字形轮廓),3D 曲线类输出 Vector3(多用于路径、相机漫游、TubeGeometry 管道路径)。
  • 如需张力控制或均匀弧长SplineCurve/Interpolations.CatmullRom 使用固定中心差分;需要调节 curveTypecentripetal/chordal/catmullrom)与 tension 时,请改选 CatmullRomCurve3,它内置了重复点保护(dt < 1e-4 时的容错处理,见 CatmullRomCurve3.js#L234-L237)。

小结

Interpolations 是 three.js 曲线子系统中最纯粹的一块基石:它把 Catmull-Rom 样条、三次与二次 Bézier 的数学公式收敛为三个零依赖、零状态、逐坐标调用的标量函数,再由 SplineCurveCubicBezierCurve(3)QuadraticBezierCurve(3) 等曲线类封装成 2D/3D 的可采样曲线,最终支撑 ShapeExtrudeGeometryTubeGeometry 乃至相机动画与示例场景中的各类曲线应用。理解这一层实现,有助于你在遇到曲线形变不符合预期时,直接定位到是参数化方式、控制点布局还是插值因子换算的问题。

关键文件导航

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391