three.js 插值工具模块 Interpolations 深入解析:Catmull-Rom 样条与 Bézier 曲线的底层实现
Interpolations 是 three.js 中一组面向曲线求值的内置标量插值函数集合,位于 src/extras/core/Interpolations.js,提供 CatmullRom、CubicBezier 与 QuadraticBezier 三个核心方法。它并不被当作独立类直接暴露给使用者,而是作为底层标量插值引擎,被 2D/3D 的 Bézier 曲线与样条曲线类(CubicBezierCurve、QuadraticBezierCurve、CubicBezierCurve3、QuadraticBezierCurve3、SplineCurve 等)内部调用,逐维求出曲线上的点。阅读本文后,你将理解这三个函数的参数语义、完整数学公式与源码实现、它们与具体曲线类之间的调用链,并能熟练地把插值函数用于构建平滑轨迹、轮廓、挤出与管线几何体。
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 曲线内部把 x 与 y 分量分开插值,三维则再对 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 的核心思想是:要求曲线穿过 p1 与 p2,并在两端处的切线与“相邻点连线”方向一致。three.js 的实现采用经典的中心差分形式,先计算两个切向量 v0、v1:
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 时恰返回 p1,t = 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…v3 的 x、y 分量分别送入三次插值再组装成 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)则在 x、y 之外再对 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 就是传给 CatmullRom 的 t,首末段缺少的相邻点直接用自身端点补齐,从而保证曲线在整段上连续且不越界访问数组。
注意:CatmullRomCurve3 走的是另一条专用实现
这里有一个容易混淆的细节:三维的 CatmullRomCurve3 并没有 import 本模块的 CatmullRom,而是在文件内实现了带缓存的 CubicPoly(CatmullRomCurve3.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 与用例被注释),真正的行为验证更多落在其上层消费者上——例如对 CubicBezierCurve、SplineCurve、CatmullRomCurve3 等的取样、getPoint 端点命中、getTangent 与弧长采样测试。读者如果要在自己的项目中验证这三个纯函数的正确性,可自行断言以下关键性质:
CatmullRom(0, …, p1, …) === p1且CatmullRom(1, …, …, p2, …) === p2(穿过内部点);CubicBezier(0, p0, …, …) === p0、CubicBezier(1, …, …, p3) === p3(穿过首尾控制点);QuadraticBezier(0, p0, …) === p0、QuadraticBezier(1, …, p2) === p2。
边界、注意事项与选型建议
- t 的取值约定:三个函数的
t都应在[0, 1]内。虽然多项式在区间外也有定义(可实现延展),但 three.js 曲线体系内部约定t ∈ [0, 1](见各曲线类getPoint的 JSDoc 注释),区间外行为不做保证。 - 函数为内部模块:
Interpolations的 API 文档明确标注“(inner)”,其导出经由 src/Three.js 等入口聚合后,通常不直接暴露为THREE.CatmullRom这样的顶层符号。绝大多数场景下你应当使用具体曲线类(SplineCurve、CubicBezierCurve(3)、QuadraticBezierCurve(3)、CatmullRomCurve3),而不是直接调用这些标量函数。 - 样条插值 vs 逼近:
CatmullRom是插值型(穿过p1、p2),Bézier 系列只穿过首尾控制点,中间控制点只“牵引”方向。理解这一点是合理布置控制点的前提。 - 二维还是三维:2D 曲线类输出
Vector2(多用于Shape/ExtrudeGeometry截面与文字字形轮廓),3D 曲线类输出Vector3(多用于路径、相机漫游、TubeGeometry管道路径)。 - 如需张力控制或均匀弧长:
SplineCurve/Interpolations.CatmullRom使用固定中心差分;需要调节curveType(centripetal/chordal/catmullrom)与tension时,请改选 CatmullRomCurve3,它内置了重复点保护(dt < 1e-4时的容错处理,见 CatmullRomCurve3.js#L234-L237)。
小结
Interpolations 是 three.js 曲线子系统中最纯粹的一块基石:它把 Catmull-Rom 样条、三次与二次 Bézier 的数学公式收敛为三个零依赖、零状态、逐坐标调用的标量函数,再由 SplineCurve、CubicBezierCurve(3)、QuadraticBezierCurve(3) 等曲线类封装成 2D/3D 的可采样曲线,最终支撑 Shape、ExtrudeGeometry、TubeGeometry 乃至相机动画与示例场景中的各类曲线应用。理解这一层实现,有助于你在遇到曲线形变不符合预期时,直接定位到是参数化方式、控制点布局还是插值因子换算的问题。
关键文件导航
- 本模块实现:src/extras/core/Interpolations.js
- API 参考文档:docs/pages/module-Interpolations.html.md(对应页面 docs/pages/Interpolations.html)
- 曲线基类与消费者:src/extras/core/Curve.js、src/extras/curves/SplineCurve.js、src/extras/curves/CubicBezierCurve.js、src/extras/curves/QuadraticBezierCurve.js
- 可对比的同类三维样条实现:src/extras/curves/CatmullRomCurve3.js
- 单元测试挂载点:test/unit/src/extras/core/Interpolations.tests.js
- 可运行参考示例:examples/webgl_geometry_spline_editor.html、examples/webgl_geometry_extrude_splines.html
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00