首页
/ Three.js 深度解析 CubicBezierCurve3:三次贝塞尔曲线的 API、源码实现与曲线族方法

Three.js 深度解析 CubicBezierCurve3:三次贝塞尔曲线的 API、源码实现与曲线族方法

2026-09-06 17:08:40作者:侯霆垣

本文以官方文档 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()。这一设计使得 CubicBezierCurve3QuadraticBezierCurve3CatmullRomCurve3LineCurve3 等曲线可以互换地传入 TubeGeometryExtrudeGeometry 等任何接受 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()(如单元测试中的 ExtendingInstancing 用例所示)是合法的,会得到一条退化的零长度曲线;此时应通过赋值 curve.v0/v1/v2/v3copy() 补全控制点。

三、属性

3.1 .isCubicBezierCurve3 : boolean(readonly)

类型测试标志位,默认 true。常用于 instanceof 之外的快速类型判断:

if ( object.isCubicBezierCurve3 ) { /* ... */ }

3.2 控制点属性 .v0 / .v1 / .v2 / .v3 : Vector3

四个 Vector3 引用,分别对应起点、控制点一、控制点二与终点。直接引用赋值意味着:

  1. 修改共享引用会反向影响曲线——若传入的 Vector3 被多处引用,原地修改(vector.x = 1)会同步改变曲线形状;
  2. 修改控制点之后,若依赖弧长缓存的方法(getPointAtgetSpacedPoints 等)仍需精确结果,建议设置 curve.needsUpdate = true 或调用 curve.updateArcLengths(),这一点由父类 CurveneedsUpdate 机制保障(见下节)。

另外构造函数中还设置了 this.type = 'CubicBezierCurve3',该字段由父类 Curve 引入,用于序列化/反序列化场景中的类型识别(ObjectLoader 等按 type 还原具体类)。

四、核心方法 .getPoint( t, optionalTarget )

4.1 文档签名

.getPoint( t : number, optionalTarget : Vector3 ) : Vector3
  • t:插值因子,表示曲线上的位置,取值范围必须为 [0,1]
  • optionalTarget:可选的目标向量,结果将写入该向量并返回,便于在渲染循环中复用、避免每帧产生垃圾对象;
  • 返回:曲线上的位置(Vector3);
  • OverridesCurve#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 必为 v0t = 1 必为 v3v1v2 不在线上的中间段,只通过权重牵引曲线走向。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() 采样并累加相邻距离,得到累计弧长数组;结果缓存在 cacheArcLengthsneedsUpdatefalse 且分段数匹配时直接复用;
  • getLength():返回弧长数组末元素;
  • getUtoTmapping( u, distance ):将"按弧长均匀"的因子 u(或给定距离)反解为"按参数均匀"的 t,内部采用二分查找 + 段内线性插值实现。

测试用例 getUtoTmapping 验证了边界与中间值:getUtoTmapping( 0, 0 ) 返回 0getUtoTmapping( 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 = trueupdateArcLengths())。

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/getTangentAtt = 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 时对闭环做额外旋转载荷以消除首尾法线不连续的扭曲。

此方法是 TubeGeometryExtrudeGeometry 沿 3D 曲线生成截面的直接依赖,测试用例 computeFrenetFramescurve.computeFrenetFrames( 2, false ) 的三组向量逐分量断言,可作为复现基准。

5.5 序列化与拷贝:copy / clone / toJSON / fromJSON

CubicBezierCurve3 覆写了 copytoJSONfromJSON,完整继承文档与源码:

// 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.version4.7,四个控制点以 toArray() 的数组形式([x,y,z])序列化;
  • fromJSON 使用 fromArray 将数组还原回向量,返回 this 以支持链式调用;
  • clone() 继承自 Curve,内部通过 new this.constructor().copy( this ) 实现,因此克隆结果自动是 CubicBezierCurve3 类型。

序列化能力使曲线可以进入场景 JSON 体系(ObjectLoadertype 字段还原)。但需要注意一个边界:从 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 逐分量断言

七、典型应用场景

  1. 管状/挤出几何TubeGeometry 的第一个参数即为 path : Curve(默认值为 QuadraticBezierCurve3,见 src/geometries/TubeGeometry.js 注释),把 CubicBezierCurve3 传进去即可得到沿三次曲线扫掠的管道;ExtrudeGeometryextrudePath 同理。两者内部均调用 computeFrenetFrames 求截面方向。
  2. 运动轨迹与路径动画:利用 getPointAt( u ) 可以按"匀速"(弧长参数)沿曲线移动物体,比直接用 getPoint 更符合物理直觉;getTangentAt 可同时给出朝向。
  3. 曲线组合:多个 CubicBezierCurve3 可加入 CurvePath 拼出长路径。仓库的 manual/resources/threejs-primitives.js 中即出现 shape.add( new THREE.CubicBezierCurve3( ...points.slice( i, i + 4 ) ) ) 的用法,展示了每 4 个连续点构造一段三次贝塞尔并追加到路径/形状上的惯用写法。
  4. 平滑样条逼近:由于三次贝塞尔可由任意两端的切向量确定(v1 = v0 + m0/3v2 = v3 − m1/3 的思路),常用作把带切线信息的点序列转成 C1 连续曲线的中间表示;此点属于常见数学实践,仓库源码未直接给出该转换函数,此处为方法提示而非仓库事实。

八、完整示例:生成曲线、均匀采样并挤出管道

以下示例综合了本文涉及的 API(导入路径与仓库导出结构一致,CubicBezierCurve3src/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()

九、要点小结

  • CubicBezierCurve3Curve 家族的三维三次贝塞尔实现:构造函数接收 v0/v1/v2/v3 四个 Vector3(均有零向量默认值),type 固定为 'CubicBezierCurve3'
  • 唯一的覆写核心是 getPoint( t, optionalTarget ),它把三维问题拆成 x/y/z 三条标量 Bernstein 多项式,公式实现在 src/extras/core/Interpolations.js
  • t 按参数均匀,ugetPointAt/getSpacedPoints/getTangentAt)按弧长均匀,二者经 getUtoTmapping 的二分查找 + 段内插值换算,弧长缓存受 arcLengthDivisions(默认 200)与 needsUpdate 控制;
  • 序列化(toJSON/fromJSON,metadata 版本 4.7)与 copy/clone 使该类可无缝接入 ObjectLoader 与场景 JSON 体系;
  • computeFrenetFrames 是它与 TubeGeometryExtrudeGeometry 联动的关键桥梁;
  • 全部数值结论均可在 test/unit/src/extras/curves/CubicBezierCurve3.tests.js 中找到精确断言,建议以该测试文件作为回归基准。

参考文件src/extras/curves/CubicBezierCurve3.jssrc/extras/core/Curve.jssrc/extras/core/Interpolations.jssrc/extras/curves/Curves.jstest/unit/src/extras/curves/CubicBezierCurve3.tests.jssrc/geometries/TubeGeometry.jssrc/geometries/ExtrudeGeometry.jsmanual/resources/threejs-primitives.js

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