three.js CurvePath:将多条曲线组合为单一路径的 API 参考与源码解析
CurvePath 是 three.js 曲线系统(Curve system)中的核心组合类:它把多条子曲线(直线、贝塞尔曲线、椭圆弧、样条曲线等)串联成一条路径,同时完整保留父类 Curve 的插值 API,使得整条路径可以像单条曲线一样按参数 t 采样、计算弧长、均匀取点。本文基于官方 API 文档 CurvePath.html.md 与源码 CurvePath.js,逐条讲解其属性、方法与底层实现,帮助你在生成挤出几何体(ExtrudeGeometry)、自定义描边、运动路径等场景中正确使用该类。
一、类的定位与继承关系
官方文档明确给出的继承链为:
Curve → CurvePath
源码中这一点直接体现为类声明(CurvePath.js#L10):
class CurvePath extends Curve {
constructor() {
super();
this.type = 'CurvePath';
// 定义路径的曲线数组
this.curves = [];
// 是否由线段自动闭合路径
this.autoClose = false;
}
CurvePath 在库内的地位可以从导出文件 Three.Core.js 得到印证——它与 Curve、Path 等一起被打包进核心导出(export { CurvePath } from './extras/core/CurvePath.js')。更重要的是,它是整个 2D 路径工具链的基座:Path.js 中的 Path 类直接继承自 CurvePath,而 Shape、ShapePath 又构建在 Path 之上,最终服务于 ShapeUtils 和 ExtrudeGeometry 等几何生成流程。因此理解 CurvePath,就理解了 three.js 中「点集 → 路径 → 形状 → 实体几何」这条链路的起点。
单元测试 CurvePath.tests.js 也验证了继承与实例化行为:
QUnit.test( 'Extending', ( assert ) => {
const object = new CurvePath();
assert.strictEqual(
object instanceof Curve, true,
'CurvePath extends from Curve'
);
} );
二、构造器
const path = new THREE.CurvePath();
new CurvePath() 不需要任何参数。它初始化一个空的 curves 数组和一个 type = 'CurvePath' 的序列化标识,并调用父类 Curve 的构造器。父类构造器(Curve.js#L15-L57)还顺带初始化了两个对 CurvePath 同样重要的成员:
arcLengthDivisions = 200:父类数值积分弧长时的分段数。虽然CurvePath的长度直接由子曲线长度累加得出(见下文),但当子曲线自身参与计算时,该值仍影响精度;needsUpdate = false/cacheArcLengths = null:父类弧长缓存控制开关。
CurvePath 自身则追加了两个专属属性,构成其全部公开状态。
三、属性详解
3.1 .curves : Array<Curve>
定义路径的曲线数组,是 CurvePath 唯一真正的数据载体。所有操作(add、closePath、getCurveLengths、toJSON…)本质上都是在读写这个数组。
一个典型用法是把直线和贝塞尔曲线混排(各曲线类定义在 src/extras/curves/ 目录,由 Curves.js 汇总导出):
const path = new THREE.CurvePath();
path.add( new THREE.LineCurve( new THREE.Vector2( 0, 0 ), new THREE.Vector2( 1, 0 ) ) );
path.add( new THREE.CubicBezierCurve(
new THREE.Vector2( 1, 0 ),
new THREE.Vector2( 2, 0.5 ),
new THREE.Vector2( 2, -0.5 ),
new THREE.Vector2( 1, -1 )
) );
path.add( new THREE.QuadraticBezierCurve(
new THREE.Vector2( 1, -1 ),
new THREE.Vector2( 0.5, -1.5 ),
new THREE.Vector2( 0, 0 )
) );
注意:2D 曲线(如 CubicBezierCurve)与 3D 曲线(如 CubicBezierCurve3)返回的采样点分别是 Vector2 和 Vector3。从源码实现看,CurvePath 并不强制子曲线维度一致,采样结果的维度完全取决于当前命中的那条子曲线,因此在 2D/3D 曲线混用时调用方需要自行处理维度差异。
3.2 .autoClose : boolean
默认值 false。为 true 时,采样方法会在返回的点序列末尾自动补上起点,使路径在几何意义上闭合。它影响 getSpacedPoints 与 getPoints 两处输出(CurvePath.js#L189-L193、L227-L231):
// getSpacedPoints 中的处理
if ( this.autoClose ) {
points.push( points[ 0 ] );
}
需要区分两种「闭合」语义:
autoClose = true只是在采样结果上补点,curves数组不变,路径的getLength()也不包含隐含的闭合线段;- 真正修改路径几何的是
closePath()方法,它会向curves数组中追加一条连接终点与起点的线曲线,闭合部分随后被计入弧长与采样。
autoClose 的典型场景是 Path/Shape 构建轮廓供 ExtrudeGeometry 使用时,轮廓必须首尾相接才能形成封闭截面。
四、方法详解
4.1 .add( curve )
add( curve ) {
this.curves.push( curve );
}
实现极其直接:把曲线推入 curves 数组(CurvePath.js#L45-L49)。它没有返回值,也不做参数校验,因此传入的必须是 Curve 体系内的曲线对象(即实现了 getPoint 等接口的实例)。
实践提示:
add后如果之前已经调用过getLength()/getCurveLengths(),内部长度缓存会失效吗?答案是不会自动失效——缓存仅在cacheLengths.length === curves.length时复用(见 4.4 节),数组长度变化后缓存自然失效。但若修改的是已存在子曲线的形状参数,则必须手动调用updateArcLengths()(见 4.5 节)。
4.2 .closePath() : CurvePath
closePath() {
// Add a line curve if start and end of lines are not connected
const startPoint = this.curves[ 0 ].getPoint( 0 );
const endPoint = this.curves[ this.curves.length - 1 ].getPoint( 1 );
if ( ! startPoint.equals( endPoint ) ) {
const lineType = ( startPoint.isVector2 === true ) ? 'LineCurve' : 'LineCurve3';
this.curves.push( new Curves lineType );
}
return this;
}
方法逻辑(CurvePath.js#L56-L71)分三步:
- 取首条曲线的起点
curves[0].getPoint(0)与末条曲线的终点curves[last].getPoint(1); - 若两点不相等,按起点向量类型选择
LineCurve(2D)或LineCurve3(3D),构造一条从终点指回起点的线曲线; - 推入
curves数组并返回this,支持链式调用。
两个值得注意的实现细节:
- 如果路径本身已经首尾相连(起点恰好等于终点),方法不会追加任何曲线,直接返回;
- 闭合判定使用
equals精确比较Vector2/Vector3,浮点场景下两条曲线端点存在微小误差时仍会追加一条极短线段——这通常无害,但说明「已经闭合」的判断对调用方的数据精度敏感。
4.3 .getPoint( t, optionalTarget ) : Vector2 | Vector3
这是 CurvePath 对父类抽象方法 Curve#getPoint 的核心重写(CurvePath.js#L81-L120)。参数说明:
- t : number — 沿整条路径的位置参数,取值
[0, 1]; - optionalTarget : Vector2 | Vector3 — 可选,结果写入该向量以复用内存;
- 返回 : 2D 或 3D 向量,取决于当前命中的子曲线定义。
实现遵循源码注释中写明的四步算法:
getPoint( t, optionalTarget ) {
// 1. Length of each sub path have to be known
// 2. Locate and identify type of curve
// 3. Get t for the curve
// 4. Return curve.getPointAt(t')
const d = t * this.getLength();
const curveLengths = this.getCurveLengths();
let i = 0;
while ( i < curveLengths.length ) {
if ( curveLengths[ i ] >= d ) {
const diff = curveLengths[ i ] - d;
const curve = this.curves[ i ];
const segmentLength = curve.getLength();
const u = segmentLength === 0 ? 0 : 1 - diff / segmentLength;
return curve.getPointAt( u, optionalTarget );
}
i ++;
}
return null;
}
其工作机理是基于弧长的全局参数化:
d = t * this.getLength():把全局参数t换算成沿路径的目标弧长d;getCurveLengths()返回各子曲线长度的前缀和数组,线性扫描找到第一个curveLengths[i] >= d的分段,确定d落在第i条子曲线上;diff = curveLengths[i] - d是该子曲线内部尚未走完的剩余弧长,因此局部参数u = 1 - diff / segmentLength恰好把全局进度换算为该子曲线的局部进度(segmentLength为 0 时直接取u = 0防止除零);- 调用
curve.getPointAt( u )而非getPoint( u )——这一步很关键:getPointAt会在子曲线内部再做一次弧长重参数化,从而保证沿整条路径的采样是弧长均匀的,而不是各子曲线参数空间的拼接。
一个容易忽略的边界情况:若 t * totalLength 因浮点误差略大于前缀和末尾(理论上 t <= 1 时不会越过末位),循环走完会返回 null。实际调用中把 t 限制在 [0, 1] 内即可规避。
4.4 .getCurveLengths() : Array<number>
返回各子曲线长度的累积数组(CurvePath.js#L148-L177):
getCurveLengths() {
// cacheLengths must be recalculated when curve count changes
if ( this.cacheLengths && this.cacheLengths.length === this.curves.length ) {
return this.cacheLengths;
}
const lengths = [];
let sums = 0;
for ( let i = 0, l = this.curves.length; i < l; i ++ ) {
sums += this.curves[ i ].getLength();
lengths.push( sums );
}
this.cacheLengths = lengths;
return lengths;
}
要点:
- 返回数组的第
i项是「从路径起点到第i条子曲线终点」的累计弧长,末位元素即整条路径总长; - 缓存策略是按数组长度匹配:
cacheLengths.length === curves.length时直接复用。因此add一条曲线后长度变化会使旧缓存作废; - 源码注释特意说明不能覆盖父类的
getLengths(),因为Curve#getUtoTmapping依赖它,二者各司其职。
4.5 getLength() 与 updateArcLengths()
文档页面未单列、但源码中存在且与 getPoint 强相关的两个方法:
getLength() {
// We cannot use the default THREE.Curve getPoint() with getLength() because in
// THREE.Curve, getLength() depends on getPoint() but in THREE.CurvePath
// getPoint() depends on getLength
const lens = this.getCurveLengths();
return lens[ lens.length - 1 ];
}
updateArcLengths() {
// cacheLengths must be recalculated.
this.needsUpdate = true;
this.cacheLengths = null;
this.getCurveLengths();
}
源码注释点明了一个设计上的鸡生蛋问题:父类 Curve 中 getLength() 依赖 getPoint()(数值积分采样),而 CurvePath 中 getPoint() 又依赖 getLength()(用于换算弧长)。CurvePath 的解法是让 getLength() 直接取子曲线前缀和的末位值,绕开对 getPoint() 的依赖,从而打破循环。
相应地,updateArcLengths() 被重写为「置位 needsUpdate、清空 cacheLengths、重算一次 getCurveLengths()」。父类 Curve.js#L186-L197 的文档注释也明确要求:当某条子曲线的参数被修改后,除了对该子曲线调用 updateArcLengths(),还必须对所属的 CurvePath 调用一次,否则路径级缓存与全局 t → 弧长的换算会基于过期数据。
4.6 采样序列:getPoints() 与 getSpacedPoints()
CurvePath 重写了这两个方法(继承自父类的同名方法仅对单条曲线做均匀 t 采样):
getSpacedPoints( divisions = 40 ) {
const points = [];
for ( let i = 0; i <= divisions; i ++ ) {
points.push( this.getPoint( i / divisions ) );
}
if ( this.autoClose ) {
points.push( points[ 0 ] );
}
return points;
}
getSpacedPoints(divisions = 40):沿整条路径按弧长均匀取divisions + 1个点(内部走getPoint,即弧长参数化);autoClose为true时末尾补一个起点。注意默认分段数是40,与父类Curve#getSpacedPoints的默认值5不同;getPoints(divisions = 12)(CurvePath.js#L199-L235):按子曲线分段采样,并根据曲线类型自适应调整每条子曲线的分辨率——
const resolution = curve.isEllipseCurve ? divisions * 2
: ( curve.isLineCurve || curve.isLineCurve3 ) ? 1
: curve.isSplineCurve ? divisions * curve.points.length
: divisions;
即椭圆/圆弧曲线用 2×divisions(圆弧曲率高),直线只用 1 段(两段端点即可精确表示),样条曲线按 divisions × 控制点数 加密,其余默认 divisions。此外它会跳过与前一点重合的相邻重复点(if ( last && last.equals( point ) ) continue;),避免路径拼接点产生冗余顶点——这对后续生成 Line/ShapeGeometry 的顶点数据是实际收益。
4.7 序列化:copy() / toJSON() / fromJSON()
toJSON() {
const data = super.toJSON();
data.autoClose = this.autoClose;
data.curves = [];
for ( let i = 0, l = this.curves.length; i < l; i ++ ) {
const curve = this.curves[ i ];
data.curves.push( curve.toJSON() );
}
return data;
}
fromJSON( json ) {
super.fromJSON( json );
this.autoClose = json.autoClose;
this.curves = [];
for ( let i = 0, l = json.curves.length; i < l; i ++ ) {
const curve = json.curves[ i ];
this.curves.push( new Curves[ curve.type ]().fromJSON( curve ) );
}
return this;
}
toJSON输出{ type: 'CurvePath', autoClose, curves: [...] },每条子曲线以自身toJSON结果内嵌;fromJSON依据每条子曲线数据中的type字段(如'CubicBezierCurve')从Curves命名空间动态实例化对应类再反序列化——这也是为什么子曲线必须属于 Curves.js 汇总导出体系;copy(source)对每条子曲线执行clone()做深拷贝,并复制autoClose。
这套机制让 CurvePath 可以作为场景/对象图的一部分参与 JSON 序列化与对象复制,例如通过 Object3D 的序列化链路间接保存自定义曲线几何。
五、在曲线家族中的位置:Path、Shape 与 ShapePath
CurvePath 是「裸」的组合容器,只负责拼接与弧长换算;面向具体建模需求时,three.js 提供了功能更强的子类:
| 类 | 源码 | 相对 CurvePath 的增量 |
|---|---|---|
Path |
Path.js | 提供类 Canvas 2D 的便捷 API:moveTo、lineTo、quadraticCurveTo、bezierCurveTo、splineThru、absarc 等,内部自动把坐标增量转换为具体曲线对象并 add 进 curves |
Shape |
Shape.js | 继承 Path,支持 holes 孔洞,是 ShapeGeometry/ExtrudeGeometry 的标准输入 |
ShapePath |
src/extras/core/ShapePath.js | 维护轮廓与孔洞的空间归属关系,用于解析复杂形状集合 |
Path.js 源码注释中的示例展示了这条继承链的完整用法(Path 即带便捷 API 的 CurvePath):
const path = new THREE.Path();
path.lineTo( 0, 0.8 );
path.quadraticCurveTo( 0, 1, 0.2, 1 );
path.lineTo( 1, 1 );
const points = path.getPoints();
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const material = new THREE.LineBasicMaterial( { color: 0xffffff } );
const line = new THREE.Line( geometry, material );
scene.add( line );
选型建议:需要 Canvas 风格的增量式绘制 API 或最终生成形状几何时优先用 Path/Shape;需要手动控制曲线序列、混合 2D/3D 曲线、做运行时拼路与弧长查询(如沿路径移动对象、路径变形动画)时,直接使用 CurvePath 最轻量。
六、完整实战示例
下面示例组合直线与两条贝塞尔曲线,演示采样、弧长查询与闭合,全部 API 均与上文源码一致:
import * as THREE from 'three';
// 1. 组合路径
const path = new THREE.CurvePath();
path.add( new THREE.LineCurve(
new THREE.Vector2( 0, 0 ),
new THREE.Vector2( 1, 0 )
) );
path.add( new THREE.QuadraticBezierCurve(
new THREE.Vector2( 1, 0 ),
new THREE.Vector2( 1.5, 1.5 ),
new THREE.Vector2( 0.5, 1 )
) );
path.add( new THREE.CubicBezierCurve(
new THREE.Vector2( 0.5, 1 ),
new THREE.Vector2( -0.5, 0.5 ),
new THREE.Vector2( -0.5, -0.5 ),
new THREE.Vector2( 0, 0 )
) );
// 2. 弧长信息
console.log( path.getLength() ); // 路径总长
console.log( path.getCurveLengths() ); // [L1, L1+L2, L1+L2+L3]
// 3. 全局弧长均匀采样
const points = path.getSpacedPoints( 64 );
console.log( points[ 0 ].x, points[ 0 ].y ); // (0, 0)
const mid = path.getPoint( 0.5 ); // 路径弧长中点位置
// 4. 修改子曲线参数后必须刷新缓存
path.curves[ 1 ].control = new THREE.Vector2( 2, 2 );
path.updateArcLengths();
// 若该曲线同时作为独立 Curve 使用,还应对其调用 updateArcLengths()
// 5. 渲染为折线
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const line = new THREE.Line( geometry,
new THREE.LineBasicMaterial( { color: 0xffffff } ) );
scene.add( line );
// 6. 生成闭合轮廓(追加一条回起点的线曲线)
path.closePath();
console.log( path.curves.length ); // 4
注意第 3 步中 getSpacedPoints 的均匀性是弧长意义上的:即使某条子曲线参数空间变化很快(曲线局部被「拉直」或「打弯」),采样点在整个路径上的物理间距依然均等——这正是 CurvePath#getPoint 内部先做弧长换算、再调用 getPointAt 的目的。
七、API 速查表
| 成员 | 签名 | 说明 |
|---|---|---|
| 构造器 | new CurvePath() |
初始化空路径,curves = []、autoClose = false |
.curves |
Array<Curve> |
定义路径的子曲线数组 |
.autoClose |
boolean,默认 false |
采样序列末尾是否自动补起点形成闭合 |
.add() |
add( curve ) : void |
追加一条曲线 |
.closePath() |
closePath() : CurvePath |
若首尾未连接则追加闭合线曲线;返回 this |
.getCurveLengths() |
getCurveLengths() : Array<number> |
子曲线累计弧长数组(带缓存) |
.getPoint() |
getPoint( t, optionalTarget ) : Vector2 | Vector3 |
按全局弧长参数 t ∈ [0,1] 取路径上一点 |
.getLength() |
getLength() : number |
路径总弧长,等于 getCurveLengths() 末位值 |
.updateArcLengths() |
updateArcLengths() : void |
子曲线参数变化后必须调用以失效缓存 |
.getSpacedPoints() |
getSpacedPoints( divisions = 40 ) |
弧长均匀采样,autoClose 时补闭合点 |
.getPoints() |
getPoints( divisions = 12 ) |
按子曲线自适应分辨率采样,去重相邻点 |
.copy() / .toJSON() / .fromJSON() |
— | 深拷贝与 JSON 序列化/反序列化 |
八、关键参考路径
| 内容 | 相对路径 |
|---|---|
| 官方 API 文档(本文依据) | docs/pages/CurvePath.html.md |
| CurvePath 实现 | src/extras/core/CurvePath.js |
| 父类 Curve(弧长缓存、UtoT 映射) | src/extras/core/Curve.js |
| 便捷子类 Path | src/extras/core/Path.js |
| 形状类 Shape / ShapePath | src/extras/core/Shape.js、src/extras/core/ShapePath.js |
| 具体曲线类汇总 | src/extras/curves/Curves.js |
| 单元测试 | test/unit/src/extras/core/CurvePath.tests.js |
| 核心导出 | src/Three.Core.js |
小结
CurvePath 的设计核心可以概括为一句话:用前缀和弧长数组把多段异质曲线统一到一个全局弧长参数空间里。getCurveLengths() 建立分段索引,getPoint(t) 通过「全局弧长 → 定位子曲线 → 换算局部参数 → getPointAt」四级换算实现全路径均匀采样,closePath() 与 autoClose 分别提供「几何闭合」与「采样闭合」两种手段,而 getPoints() 的按类型自适应分辨率与 copy/toJSON 的递归序列化则补齐了工程落地所需的能力。由于 Path、Shape 均构建于其上,掌握 CurvePath 也就掌握了 three.js 从曲线到形状几何的整条数据链路。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00