three.js LineCurve3 完全指南:三维线段曲线的构造、采样与序列化
LineCurve3 是 three.js(JavaScript 3D Library)中表示三维空间直线段的曲线类,它继承自抽象基类 Curve,为 3D 场景中的"从 A 点到 B 点"的直线路径提供了统一的插值、采样、切线计算与 JSON 序列化能力。本文以官方文档 LineCurve3 文档 为主线,结合仓库源码与单元测试,深入讲解其构造方式、全部属性与方法、底层实现原理,以及它在组合路径与动画轨迹中的典型用法。
LineCurve3 是什么
- 继承关系:
Curve→LineCurve3 - 核心描述:一条表示三维(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对应起点v1,t = 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)。需要注意两个实现细节:
- 对
t === 1做了精确端点特判,直接复制v2。这是为了避免浮点累加误差导致采样终点与声明的终点v2不完全一致,从而影响路径闭合、边界判断等对精度敏感的逻辑; - 其余情况先算
v2 - v1的方向向量,再multiplyScalar( t )缩放后加上v1,t严格等比映射在直线段上。
基本调用:
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 曲线),TubeGeometry、ExtrudeGeometry等依赖它构建立管截面。用LineCurve3作为TubeGeometry的path,就能得到"直管道"形态的几何体。
取点示例:
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。
退化情形提示:当
v1与v2重合(零长度线段)时,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)、arcLengthDivisions 与 type,本类在此基础上追加 v1、v2 两个数组字段(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 选择 LineCurve 或 LineCurve3:
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 Curve为true; - 类型标志:断言
isLineCurve3 === true、type === '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 个点且与预期逐点相等。
这些测试从数值层面锁定了文档所描述的数学行为,是二次开发时修改该类后必须回归的关卡。
源码与延伸阅读
本类相关文件索引(均位于当前仓库):
- 类实现:src/extras/curves/LineCurve3.js
- 2D 同族实现:src/extras/curves/LineCurve.js
- 曲线目录统一出口:src/extras/curves/Curves.js(集中 re-export 全部
LineCurve3等曲线类型) - 抽象基类:src/extras/core/Curve.js 与文档 Curve 文档
- 组合路径与自动闭合:src/extras/core/CurvePath.js
- 单元测试:test/unit/src/extras/curves/LineCurve3.tests.js
- 官方参考文档(本文骨架来源):docs/pages/LineCurve3.html.md
小结:LineCurve3 是 three.js 曲线体系中最基础、也最容易把握全貌的 3D 曲线实现。掌握它的构造参数、getPoint 的线性插值语义、针对匀速运动重写的 getPointAt、解析化的 getTangent,以及随 Curve 继承而来的弧长、取点、Frenet 标架与 JSON 序列化能力,你就能在场景中轻松搭建"点 A 到点 B"的直线运动、路径闭合与工程化持久化,并为理解 CatmullRomCurve3、CubicBezierCurve3 等更复杂曲线打下坚实基础。
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 StartedRust0626
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