Three.js 深度解析 CubicBezierCurve3:三次贝塞尔曲线的 API、源码实现与曲线族方法
本文以官方文档 CubicBezierCurve3 页面为骨架,系统讲解 Three.js 中三维三次贝塞尔曲线类的构造函数、全部属性与方法,并结合 src/extras/curves/CubicBezierCurve3.js 源码与 src/extras/core/Interpolations.js 中的 Bernstein 基函数实现,剖析 getPoint() 的逐轴插值原理;同时覆盖从父类 Curve 继承而来的弧长映射、切线、Frenet 标架等关键能力。读完后你既能直接使用该类生成任意三维曲线点,也能理解其采样、序列化与在 TubeGeometry 等几何体中的底层调用关系。
一、类定位与继承关系
CubicBezierCurve3 是一条三维三次贝塞尔曲线(A curve representing a 3D Cubic Bezier curve),继承自抽象基类 Curve:
Curve → CubicBezierCurve3
从源码结构看,类定义位于 src/extras/curves/CubicBezierCurve3.js,并经由 src/extras/curves/Curves.js 统一导出:
export { CubicBezierCurve3 } from './CubicBezierCurve3.js';
Curve 是所有解析曲线的抽象基类,提供插值、弧长、切线等通用能力,而子类只需实现 getPoint()。这一设计使得 CubicBezierCurve3 与 QuadraticBezierCurve3、CatmullRomCurve3、LineCurve3 等曲线可以互换地传入 TubeGeometry、ExtrudeGeometry 等任何接受 Curve 的参数位置。
二、构造函数
官方文档定义的构造函数签名:
new CubicBezierCurve3( v0 : Vector3, v1 : Vector3, v2 : Vector3, v3 : Vector3 )
四个参数含义(完整继承自文档并对照源码注释):
| 参数 | 类型 | 含义 |
|---|---|---|
v0 |
Vector3 |
起点(The start point) |
v1 |
Vector3 |
第一个控制点(The first control point) |
v2 |
Vector3 |
第二个控制点(The second control point) |
v3 |
Vector3 |
终点(The end point) |
一个值得注意的实现细节:源码中四个参数都带有默认值,允许零参实例化:
// src/extras/curves/CubicBezierCurve3.js
constructor( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3(), v3 = new Vector3() ) {
super();
this.isCubicBezierCurve3 = true;
this.type = 'CubicBezierCurve3';
this.v0 = v0;
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
}
这意味着 new CubicBezierCurve3()(如单元测试中的 Extending、Instancing 用例所示)是合法的,会得到一条退化的零长度曲线;此时应通过赋值 curve.v0/v1/v2/v3 或 copy() 补全控制点。
三、属性
3.1 .isCubicBezierCurve3 : boolean(readonly)
类型测试标志位,默认 true。常用于 instanceof 之外的快速类型判断:
if ( object.isCubicBezierCurve3 ) { /* ... */ }
3.2 控制点属性 .v0 / .v1 / .v2 / .v3 : Vector3
四个 Vector3 引用,分别对应起点、控制点一、控制点二与终点。直接引用赋值意味着:
- 修改共享引用会反向影响曲线——若传入的
Vector3被多处引用,原地修改(vector.x = 1)会同步改变曲线形状; - 修改控制点之后,若依赖弧长缓存的方法(
getPointAt、getSpacedPoints等)仍需精确结果,建议设置curve.needsUpdate = true或调用curve.updateArcLengths(),这一点由父类Curve的needsUpdate机制保障(见下节)。
另外构造函数中还设置了 this.type = 'CubicBezierCurve3',该字段由父类 Curve 引入,用于序列化/反序列化场景中的类型识别(ObjectLoader 等按 type 还原具体类)。
四、核心方法 .getPoint( t, optionalTarget )
4.1 文档签名
.getPoint( t : number, optionalTarget : Vector3 ) : Vector3
- t:插值因子,表示曲线上的位置,取值范围必须为
[0,1]; - optionalTarget:可选的目标向量,结果将写入该向量并返回,便于在渲染循环中复用、避免每帧产生垃圾对象;
- 返回:曲线上的位置(
Vector3); - Overrides:Curve#getPoint。
4.2 源码实现:逐轴调用 Bernstein 多项式
// src/extras/curves/CubicBezierCurve3.js
getPoint( t, optionalTarget = new Vector3() ) {
const point = optionalTarget;
const v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;
point.set(
CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),
CubicBezier( t, v0.y, v1.y, v2.y, v3.y ),
CubicBezier( t, v0.z, v1.z, v2.z, v3.z )
);
return point;
}
关键点:三维曲线被拆解为三条独立的标量三次贝塞尔曲线,x、y、z 分量各自独立求值后组合。其中 CubicBezier 来自 src/extras/core/Interpolations.js:
function CubicBezierP0( t, p ) { const k = 1 - t; return k * k * k * p; }
function CubicBezierP1( t, p ) { const k = 1 - t; return 3 * k * k * t * p; }
function CubicBezierP2( t, p ) { return 3 * ( 1 - t ) * t * t * p; }
function CubicBezierP3( t, p ) { 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 );
}
这正是经典的 Bernstein 基函数展开,等价于数学形式:
B(t) = (1-t)³·P0 + 3(1-t)²·t·P1 + 3(1-t)·t²·P2 + t³·P3, t ∈ [0,1]
因此几何性质一目了然:t = 0 必为 v0,t = 1 必为 v3;v1、v2 不在线上的中间段,只通过权重牵引曲线走向。Interpolations.js 的模块注释同时表明公式来源为维基百科的 Bézier curve 条目(此处不作为外部链接给出,仅作出处说明)。
4.3 数值验证(来自单元测试)
test/unit/src/extras/curves/CubicBezierCurve3.tests.js 中构造了如下测试曲线:
curve = new CubicBezierCurve3(
new Vector3( - 10, 0, 2 ),
new Vector3( - 5, 15, 4 ),
new Vector3( 20, 15, - 5 ),
new Vector3( 10, 0, 10 )
);
Simple curve 用例验证了均匀采样 4 段(5 个点)的结果,可用于手工核对实现正确性:
t = 0.00 → ( -10, 0, 2 )
t = 0.25 → ( -3.359375, 8.4375, 1.984375 )
t = 0.50 → ( 5.625, 11.25, 1.125 )
t = 0.75 → ( 11.796875, 8.4375, 2.703125 )
t = 1.00 → ( 10, 0, 10 )
测试还验证了一个重要的对称性质:将四个控制点倒序构造新曲线(new CubicBezierCurve3( curve.v3, curve.v2, curve.v1, curve.v0 )),其 getPoints() 结果恰为原曲线点集的逆序。这一性质可用于曲线反转(reversal)场景。
五、从 Curve 基类继承的关键能力
CubicBezierCurve3 除覆写 getPoint() 外,其余能力全部继承自 src/extras/core/Curve.js。以下方法对该类同样可用,且均有单元测试覆盖。
5.1 弧长系统:getLengths / getLength / getUtoTmapping
Curve 的构造函数定义了三个弧长相关字段:
this.arcLengthDivisions = 200; // 弧长缓存的分段数,默认 200
this.needsUpdate = false; // 曲线参数变化后置 true 以失效缓存
this.cacheArcLengths = null; // 弧长缓存
getLengths( divisions = this.arcLengthDivisions ):以getPoint()采样并累加相邻距离,得到累计弧长数组;结果缓存在cacheArcLengths,needsUpdate为false且分段数匹配时直接复用;getLength():返回弧长数组末元素;getUtoTmapping( u, distance ):将"按弧长均匀"的因子u(或给定距离)反解为"按参数均匀"的t,内部采用二分查找 + 段内线性插值实现。
测试用例 getUtoTmapping 验证了边界与中间值:getUtoTmapping( 0, 0 ) 返回 0,getUtoTmapping( 0, curve.getLength() ) 返回 1,中间某点 getUtoTmapping( 0.5, 1 ) 期望值约为 0.021163245321323316。同一测试中曲线总长期望为 39.58103024989427,累计分段 [0, 10.737…, 20.190…, 27.154…, 38.453…] 与 getLengths(4) 的输出吻合。
实践含义:若曲线很长或曲率剧烈,把
arcLengthDivisions调大可以提升getPointAt/getSpacedPoints的精度;修改控制点后记得触发弧长缓存更新(needsUpdate = true或updateArcLengths())。
5.2 等距采样:getPointAt / getPoints / getSpacedPoints
getPointAt( u, optionalTarget ):先经getUtoTmapping( u )换算再调用getPoint( t ),因此u与弧长成正比,采样沿曲线等距分布;getPoints( divisions = 5 ):按参数t均匀采样,返回divisions + 1个点;getSpacedPoints( divisions = 5 ):按弧长均匀采样,同样返回divisions + 1个点。
两者区别在于:getPoints 点间距随速度(参数化速率)变化,getSpacedPoints 间距恒定。测试用例 getPointAt 断言了 u = 0 / 0.3 / 0.5 / 1 的具体坐标,getSpacedPoints 断言了默认 5 分段下 6 个点(首末为 (-10,0,2) 与 (10,0,10))。
5.3 切线:getTangent / getTangentAt
getTangent( t ) 采用有限差分:取 t ± 0.0001(并在 [0,1] 内截断)两点求差归一化:
// src/extras/core/Curve.js
getTangent( t, optionalTarget ) {
const delta = 0.0001;
// ... t1 = max(0, t - delta), t2 = min(1, t + delta)
const pt1 = this.getPoint( t1 );
const pt2 = this.getPoint( t2 );
// tangent = normalize( pt2 - pt1 )
}
getTangentAt( u ) 则先做 u → t 弧长换算再取切线,保证等距语义。测试用例 getTangent/getTangentAt 对 t = 0, 0.25, 0.5, 0.75, 1 五个采样点断言了精确的切线分量,例如起点切线约为 (0.3138715439944244, 0.9411440474105875, 0.12542940601858074)。
5.4 Frenet 标架:computeFrenetFrames( segments, closed )
该 3D 专属方法生成切线 tangents、法线 normals、副法线 binormals 三组向量数组,实现参考了 Indiana 大学技术报告 TR425 的算法(见 src/extras/core/Curve.js 中注释):先逐段求切线,再选一个与首切线正交的初始法线(取切线 xyz 绝对值最小的分量对应的坐标轴方向构造),随后沿曲线用旋转矩阵做平行输运(parallel transport),closed = true 时对闭环做额外旋转载荷以消除首尾法线不连续的扭曲。
此方法是 TubeGeometry 与 ExtrudeGeometry 沿 3D 曲线生成截面的直接依赖,测试用例 computeFrenetFrames 对 curve.computeFrenetFrames( 2, false ) 的三组向量逐分量断言,可作为复现基准。
5.5 序列化与拷贝:copy / clone / toJSON / fromJSON
CubicBezierCurve3 覆写了 copy、toJSON、fromJSON,完整继承文档与源码:
// src/extras/curves/CubicBezierCurve3.js
copy( source ) {
super.copy( source ); // 父类拷贝 arcLengthDivisions
this.v0.copy( source.v0 );
this.v1.copy( source.v1 );
this.v2.copy( source.v2 );
this.v3.copy( source.v3 );
return this;
}
toJSON() {
const data = super.toJSON(); // 含 metadata(version 4.7, type 'Curve') + arcLengthDivisions
data.v0 = this.v0.toArray();
data.v1 = this.v1.toArray();
data.v2 = this.v2.toArray();
data.v3 = this.v3.toArray();
return data;
}
fromJSON( json ) {
super.fromJSON( json );
this.v0.fromArray( json.v0 );
this.v1.fromArray( json.v1 );
this.v2.fromArray( json.v2 );
this.v3.fromArray( json.v3 );
return this;
}
要点:
toJSON输出的type为'CubicBezierCurve3',metadata.version为4.7,四个控制点以toArray()的数组形式([x,y,z])序列化;fromJSON使用fromArray将数组还原回向量,返回this以支持链式调用;clone()继承自Curve,内部通过new this.constructor().copy( this )实现,因此克隆结果自动是CubicBezierCurve3类型。
序列化能力使曲线可以进入场景 JSON 体系(ObjectLoader 按 type 字段还原)。但需要注意一个边界:从 src/geometries/TubeGeometry.js 的源码注释可以推断,内置曲线(如 QuadraticBezierCurve3)才能被几何体序列化完整还原,用户自定义曲线或 CurvePath 组合不会被反序列化——涉及 CubicBezierCurve3 作为 path 时同理,需确认走的是内置曲线反序列化路径。
六、单元测试覆盖清单
test/unit/src/extras/curves/CubicBezierCurve3.tests.js 是验证本文所有结论的权威依据,覆盖如下断言:
| 用例 | 验证内容 |
|---|---|
Extending |
实例 instanceof Curve 为真 |
Instancing |
可零参实例化 |
type |
object.type === 'CubicBezierCurve3' |
isCubicBezierCurve3 |
标志位为 true |
Simple curve |
getPoints(4) 精确坐标 + 控制点倒序曲线点集逆序的对称性 |
getLength/getLengths |
总长 ≈ 39.58103024989427,分段累计弧长精确匹配 |
getPointAt |
u = 0/0.3/0.5/1 处等弧长采样坐标 |
getTangent/getTangentAt |
5 个采样点切线分量 |
getUtoTmapping |
端点映射为 0/1,中间值 ≈ 0.021163245321323316 |
getSpacedPoints |
默认 5 分段的等距采样点 |
computeFrenetFrames |
segments = 2 时 tangent/normal/binormal 逐分量断言 |
七、典型应用场景
- 管状/挤出几何:
TubeGeometry的第一个参数即为path : Curve(默认值为QuadraticBezierCurve3,见 src/geometries/TubeGeometry.js 注释),把CubicBezierCurve3传进去即可得到沿三次曲线扫掠的管道;ExtrudeGeometry的extrudePath同理。两者内部均调用computeFrenetFrames求截面方向。 - 运动轨迹与路径动画:利用
getPointAt( u )可以按"匀速"(弧长参数)沿曲线移动物体,比直接用getPoint更符合物理直觉;getTangentAt可同时给出朝向。 - 曲线组合:多个
CubicBezierCurve3可加入CurvePath拼出长路径。仓库的 manual/resources/threejs-primitives.js 中即出现shape.add( new THREE.CubicBezierCurve3( ...points.slice( i, i + 4 ) ) )的用法,展示了每 4 个连续点构造一段三次贝塞尔并追加到路径/形状上的惯用写法。 - 平滑样条逼近:由于三次贝塞尔可由任意两端的切向量确定(
v1 = v0 + m0/3,v2 = v3 − m1/3的思路),常用作把带切线信息的点序列转成 C1 连续曲线的中间表示;此点属于常见数学实践,仓库源码未直接给出该转换函数,此处为方法提示而非仓库事实。
八、完整示例:生成曲线、均匀采样并挤出管道
以下示例综合了本文涉及的 API(导入路径与仓库导出结构一致,CubicBezierCurve3 由 src/extras/curves/Curves.js 导出):
import * as THREE from 'three';
// 1. 构造三维三次贝塞尔曲线(四个 Vector3:起点、控制点 x2、终点)
const curve = new THREE.CubicBezierCurve3(
new THREE.Vector3( - 10, 0, 2 ),
new THREE.Vector3( - 5, 15, 4 ),
new THREE.Vector3( 20, 15, - 5 ),
new THREE.Vector3( 10, 0, 10 )
);
// 2. 类型判断
console.log( curve.isCubicBezierCurve3 ); // true
console.log( curve.type ); // 'CubicBezierCurve3'
// 3. 参数采样 vs 等弧长采样
const pParam = curve.getPoint( 0.5, new THREE.Vector3() ); // t 均匀
const pArc = curve.getPointAt( 0.5, new THREE.Vector3() ); // 弧长均匀(u→t 换算)
// 4. 弧长信息
const totalLength = curve.getLength(); // 测试基准 ≈ 39.58103024989427
const lengths = curve.getLengths( 4 ); // 累计弧长,5 个元素
// 5. 切线(可用于物体朝向)
const tangent = curve.getTangentAt( 0.25, new THREE.Vector3() );
// 6. 等距采样点列(divisions 个分段 → divisions + 1 个点)
const spacedPoints = curve.getSpacedPoints( 5 );
// 7. 沿曲线生成管状几何(Frenet 标架由 TubeGeometry 内部计算)
const tubeGeometry = new THREE.TubeGeometry( curve, 64, 0.5, 8, false );
const tube = new THREE.Mesh( tubeGeometry, new THREE.MeshStandardMaterial( { color: 0x2196f3 } ) );
scene.add( tube );
// 8. 修改控制点后的缓存更新
curve.v1.set( - 5, 20, 4 );
curve.needsUpdate = true; // 或 curve.updateArcLengths()
若需持久化,可用 curve.toJSON() / curve.fromJSON( json ) 完成序列化往返;跨对象拷贝则用 curveA.copy( curveB ) 或 curveB.clone()。
九、要点小结
CubicBezierCurve3是Curve家族的三维三次贝塞尔实现:构造函数接收v0/v1/v2/v3四个Vector3(均有零向量默认值),type固定为'CubicBezierCurve3';- 唯一的覆写核心是
getPoint( t, optionalTarget ),它把三维问题拆成 x/y/z 三条标量 Bernstein 多项式,公式实现在 src/extras/core/Interpolations.js; t按参数均匀,u(getPointAt/getSpacedPoints/getTangentAt)按弧长均匀,二者经getUtoTmapping的二分查找 + 段内插值换算,弧长缓存受arcLengthDivisions(默认 200)与needsUpdate控制;- 序列化(
toJSON/fromJSON,metadata 版本 4.7)与copy/clone使该类可无缝接入ObjectLoader与场景 JSON 体系; computeFrenetFrames是它与TubeGeometry、ExtrudeGeometry联动的关键桥梁;- 全部数值结论均可在 test/unit/src/extras/curves/CubicBezierCurve3.tests.js 中找到精确断言,建议以该测试文件作为回归基准。
参考文件:src/extras/curves/CubicBezierCurve3.js、src/extras/core/Curve.js、src/extras/core/Interpolations.js、src/extras/curves/Curves.js、test/unit/src/extras/curves/CubicBezierCurve3.tests.js、src/geometries/TubeGeometry.js、src/geometries/ExtrudeGeometry.js、manual/resources/threejs-primitives.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 StartedRust0625
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