首页
/ three.js LineCurve3 完全指南:三维线段曲线的构造、采样与序列化

three.js LineCurve3 完全指南:三维线段曲线的构造、采样与序列化

2026-09-07 14:10:10作者:毕习沙Eudora

LineCurve3 是 three.js(JavaScript 3D Library)中表示三维空间直线段的曲线类,它继承自抽象基类 Curve,为 3D 场景中的"从 A 点到 B 点"的直线路径提供了统一的插值、采样、切线计算与 JSON 序列化能力。本文以官方文档 LineCurve3 文档 为主线,结合仓库源码与单元测试,深入讲解其构造方式、全部属性与方法、底层实现原理,以及它在组合路径与动画轨迹中的典型用法。

LineCurve3 是什么

  • 继承关系CurveLineCurve3
  • 核心描述:一条表示三维(3D)线段(line segment)的曲线。

与 2D 版本的 LineCurve(对应 Vector2 端点)相对,LineCurve3 使用两个 Vector3 定义端点。它是所有 three.js 曲线中最简单的 3D 形态:没有弯曲、没有曲率,切线方向恒定,采样公式为一次线性插值。这种"简单性"反而让它成为理解 Curve 抽象基类机制的最佳样例——它恰好覆盖了基类中几乎每一个抽象方法,我们可以在源码中对照查看每条曲线公共接口的最小实现形态。

构造函数

new LineCurve3( v1 : Vector3, v2 : Vector3 )

构造一条新的线段曲线。

参数 类型 含义 默认值
v1 Vector3 起点(start point) new Vector3()(原点)
v2 Vector3 终点(end point) new Vector3()(原点)

源码 可以看到两个参数均可省略:

constructor( v1 = new Vector3(), v2 = new Vector3() ) {

    super();

    this.isLineCurve3 = true;
    this.type = 'LineCurve3';
    this.v1 = v1;
    this.v2 = v2;

}

文档勘误提示:官方文档(.md 与 .html 两份)在属性列表中把 v2 的类型标注为 Vector2,但查看源码可以确认 LineCurve3.js 顶部仅导入了 Vector3,两个端点实际都是 Vector3 实例,文档中的 Vector2 属于笔误,实际类型应以源码为准。

基本示例:

import * as THREE from 'three';

// 一条从原点指向 (10, 10, 10) 的三维线段
const curve = new THREE.LineCurve3(
    new THREE.Vector3( 0, 0, 0 ),
    new THREE.Vector3( 10, 10, 10 )
);

LineCurve3 会调用基类构造函数,因此它也同时获得基类 Curve 的如下属性:type(本类中被覆盖为 'LineCurve3')、arcLengthDivisions(默认 200,弧长累加细分精度)、needsUpdate(曲线参数变更标记)与内部弧长缓存。这些细节可查阅 Curve 基类源码Curve 文档

属性

.isLineCurve3 : boolean(只读)

用于类型检测的标志位。默认值为 true

在源码中它在构造器内被直接置为 true(见 LineCurve3.js)。由于 JavaScript 没有真正的运行时类型系统,three.js 统一采用这种 is* 布尔标志做类型判断。单元测试也用它验证实例身份(见下方"单元测试验证"一节)。基类 CurvePath 中就通过 curve.isLineCurve || curve.isLineCurve3 来识别直线段。

.v1 : Vector3

起点。可在构造后随时修改:

const curve = new THREE.LineCurve3();
curve.v1.set( - 5, 0, - 5 );
curve.v2.set( 5, 2, 5 );

.v2 : Vector3

终点。注意修改 v1/v2 后,若此前已调用过弧长相关方法,建议在基于弧长的采样(getPointAt/getSpacedPoints)前调用 updateArcLengths() 使缓存失效,基类文档对此有明确说明(见 Curve 文档updateArcLengths 一节)。

方法详解

.getPoint( t : number, optionalTarget : Vector3 ) : Vector3

返回直线上某一点的位置。

  • t:插值因子,表示在线段上的位置,取值范围 [0, 1]t = 0 对应起点 v1t = 1 对应终点 v2
  • optionalTarget:可选的写入目标向量。传入后结果直接写入该对象并返回它,从而避免在动画循环中频繁创建新 Vector3 造成 GC 压力;
  • 重写自Curve#getPoint(基类中该方法是抽象占位,仅 warn 提示未实现,见 Curve.js)。

源码实现(LineCurve3.js):

getPoint( t, optionalTarget = new Vector3() ) {

    const point = optionalTarget;

    if ( t === 1 ) {

        point.copy( this.v2 );

    } else {

        point.copy( this.v2 ).sub( this.v1 );
        point.multiplyScalar( t ).add( this.v1 );

    }

    return point;

}

实现等价于经典的线性插值公式 P(t) = v1 + t * (v2 - v1)。需要注意两个实现细节:

  1. t === 1 做了精确端点特判,直接复制 v2。这是为了避免浮点累加误差导致采样终点与声明的终点 v2 不完全一致,从而影响路径闭合、边界判断等对精度敏感的逻辑;
  2. 其余情况先算 v2 - v1 的方向向量,再 multiplyScalar( t ) 缩放后加上 v1t 严格等比映射在直线段上。

基本调用:

const mid = curve.getPoint( 0.5 );              // 线段中点 (5, 5, 5)
const reused = new THREE.Vector3();
curve.getPoint( 0.25, reused );                 // 写入既有对象,避免分配

.getPointAt( u : number, optionalTarget : Vector3 ) : Vector3(重写)

按"弧长比例"返回曲线上的点。对一般曲线,基类会先经过 getUtoTmapping 做弧长重参数化(见 Curve.js),但 LineCurve3匀速直线,参数 t 与弧长天然成正比,因此源码直接做了等价重写(LineCurve3.js):

// Line curve is linear, so we can overwrite default getPointAt
getPointAt( u, optionalTarget ) {

    return this.getPoint( u, optionalTarget );

}

这一点对动画非常重要:getPointAt(t) 表示"沿线段行走了全程的 t 比例",即匀速运动语义,配合基于时间的驱动即可实现稳定的线性运动。

继承自 Curve 的采样方法

LineCurve3 未显式声明,但可直接使用的基类采样方法包括(完整文档见 Curve 文档):

  • .getPoints( divisions = 5 ) : Array<Vector3>:通过 getPoint 均匀取 divisions 段,返回 divisions + 1 个点(含首尾端点),适合"按参数均匀"而非"按弧长均匀"的布线;
  • .getSpacedPoints( divisions = 5 ) : Array<Vector3>:通过 getPointAt 取点,返回全程弧长等间距的 divisions + 1 个点;
  • .getLength() : number:返回总弧长。对直线段即两端点欧氏距离 |v2 - v1|
  • .getLengths( divisions = arcLengthDivisions ) : Array<number>:返回累积分段长度数组,配合弧长缓存使用;
  • .getUtoTmapping( u, distance = null ) : number:弧长与参数 t 的映射工具,基类中以二分搜索 + 线性插值实现(见 Curve.js);
  • .computeFrenetFrames( segments, closed = false ) : Object:生成切向/法向/副法向的 Frenet 标架(要求 3D 曲线),TubeGeometryExtrudeGeometry 等依赖它构建立管截面。用 LineCurve3 作为 TubeGeometrypath,就能得到"直管道"形态的几何体。

取点示例:

const points = curve.getPoints( 4 );
// 共 5 个点:(0,0,0) (2.5,2.5,2.5) (5,5,5) (7.5,7.5,7.5) (10,10,10)

const spaced = curve.getSpacedPoints( 4 );      // 等弧长采样,结果与上面一致(直线等速)

.getTangent( t : number, optionalTarget : Vector3 ) : Vector3.getTangentAt( u : number, optionalTarget : Vector3 ) : Vector3

返回线段上该位置的单位切向量。直线段的切线方向处处恒定,源码直接给出解析结果而非基类默认的"微小增量差分近似"(LineCurve3.js):

getTangent( t, optionalTarget = new Vector3() ) {

    return optionalTarget.subVectors( this.v2, this.v1 ).normalize();

}

即恒等于归一化后的方向 (v2 - v1) / |v2 - v1|,与 t 取值无关。这对物体沿直线运动时的朝向设置、TubeGeometry/computeFrenetFrames 的标架构建都很有价值。getTangentAt 在基类中会先做 u → t 的弧长映射(见 Curve.js),而本类与 getPointAt 同理、直接透传 u

退化情形提示:当 v1v2 重合(零长度线段)时,subVectors 得到零向量,normalize() 将得到零向量而非单位向量。若需要稳定行为,应先判断两点距离是否大于 Number.EPSILON

.copy( source : LineCurve3 ) : LineCurve3

从另一条曲线复制状态。基类版本只复制 arcLengthDivisions,本类补充复制两个端点(LineCurve3.js):

copy( source ) {

    super.copy( source );
    this.v1.copy( source.v1 );
    this.v2.copy( source.v2 );
    return this;

}

配合基类的 .clone()(内部即 new this.constructor().copy( this ),见 Curve.js)即可深拷贝整条曲线。

.toJSON() : Object.fromJSON( json ) : LineCurve3

序列化/反序列化。基类的 toJSON 记录 metadata(当前仓库版本为 4.7)、arcLengthDivisionstype,本类在此基础上追加 v1v2 两个数组字段(LineCurve3.js):

toJSON() {

    const data = super.toJSON();
    data.v1 = this.v1.toArray();
    data.v2 = this.v2.toArray();
    return data;

}

生成的 JSON 形如:

{
    "metadata": {
        "version": 4.7,
        "type": "Curve",
        "generator": "Curve.toJSON"
    },
    "arcLengthDivisions": 200,
    "type": "LineCurve3",
    "v1": [0, 0, 0],
    "v2": [10, 10, 10]
}

fromJSON 通过 v1.fromArray( json.v1 )v2.fromArray( json.v2 ) 还原端点,且 type 字段可在 CurvePath 等组合场景中用于正确重建对应曲线类型。由此,LineCurve3 可以无缝接入基于 ObjectLoader 的工程化场景持久化流程(场景中曲线的序列化格式约定见 Curve 文档toJSON 一节)。

实战应用

1. 物体沿线段匀速移动

利用 getPointAt(t) 的匀速语义,在每个动画帧按时间比例取点:

const curve = new THREE.LineCurve3(
    new THREE.Vector3( - 5, 1, - 5 ),
    new THREE.Vector3( 5, 1, 5 )
);

const object = new THREE.Mesh(
    new THREE.SphereGeometry( 0.25 ),
    new THREE.MeshStandardMaterial()
);
scene.add( object );

const tmp = new THREE.Vector3();
const clock = new THREE.Clock();
const duration = 4; // 全程 4 秒

function tick() {

    const t = ( clock.getElapsedTime() % duration ) / duration;
    curve.getPointAt( t, tmp );
    object.position.copy( tmp );

    // 需要朝向时,可结合 getTangentAt 让物体沿运动方向对齐
    object.lookAt( tmp.clone().add( curve.getTangentAt( t ) ) );

    requestAnimationFrame( tick );

}
tick();

2. 作为曲线路径的一段并自动闭合

CurvePath 允许把多条曲线首尾拼接成一条复合路径,例如"直线 → 贝塞尔 → 直线"的折线/轨道。其中 closePath() 会在首尾不重合时自动补一条直线段闭合路径,其类型依据首点是否为 Vector2 选择 LineCurveLineCurve3

import { CurvePath, LineCurve3, CatmullRomCurve3, Vector3 } from 'three';

const path = new CurvePath();

path.add( new LineCurve3( new Vector3( 0, 0, 0 ), new Vector3( 10, 0, 0 ) ) );
path.add( new CatmullRomCurve3( [
    new Vector3( 10, 0, 0 ),
    new Vector3( 15, 5, 0 ),
    new Vector3( 20, 0, 0 )
] ) );

path.closePath(); // 若终点与起点不重合,内部自动追加 LineCurve3 闭合

此外,CurvePath 还会对直线段类曲线做 1 段弧长离散优化(见 CurvePath.js),因为直线的弧长不需要 200 段累加即可精确表示——这正是仓库对"简单曲线"的工程化细节。

3. 可视化线段本身

把采样点交给 Line/LineSegments 即可直接渲染出这条三维线段:

const pts = curve.getPoints( 16 );        // 直线其实 2 个点就够,此处仅演示采样
const geometry = new THREE.BufferGeometry().setFromPoints( pts );
const line = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0x00ffff } ) );
scene.add( line );

单元测试验证

仓库在 test/unit/src/extras/curves/LineCurve3.tests.js 中提供了覆盖本类的完整 QUnit 测试,可作为 API 行为的事实依据:

  • 继承关系:断言 instanceof Curvetrue
  • 类型标志:断言 isLineCurve3 === truetype === 'LineCurve3'
  • 采样正确性:对 (0,0,0) → (-8,5,-7) 验证 getPointAt(0/0.3/0.5/1) 的精确结果;对 (0,0,0) → (10,10,10) 验证 getPoints() 默认输出 6 个等距点,对第二组 (10,10,10) → (-10,10,-10) 验证仅 x/z 变化的采样;
  • 长度计算getLength() 应等于 Math.sqrt(300)getLengths(5) 返回 [0, √12, √48, √108, √192, √300],印证了"直线弧长 = 欧氏距离、按段均匀累加"的公式;
  • 切线getTangent(0.5)getTangentAt(0.5) 三个分量均应为 √(1/3)(即单位方向沿对角线);
  • Frenet 标架computeFrenetFrames(1, false) 生成单位切向/法向/副法向的正确性;
  • 弧长映射getUtoTmapping(0, 0) 返回 0、getUtoTmapping(0, length) 返回 1;
  • 等弧长采样getSpacedPoints(4) 输出 5 个点且与预期逐点相等。

这些测试从数值层面锁定了文档所描述的数学行为,是二次开发时修改该类后必须回归的关卡。

源码与延伸阅读

本类相关文件索引(均位于当前仓库):

小结LineCurve3 是 three.js 曲线体系中最基础、也最容易把握全貌的 3D 曲线实现。掌握它的构造参数、getPoint 的线性插值语义、针对匀速运动重写的 getPointAt、解析化的 getTangent,以及随 Curve 继承而来的弧长、取点、Frenet 标架与 JSON 序列化能力,你就能在场景中轻松搭建"点 A 到点 B"的直线运动、路径闭合与工程化持久化,并为理解 CatmullRomCurve3CubicBezierCurve3 等更复杂曲线打下坚实基础。

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