首页
/ Three.js KnotCurve 完全解析:参数曲线实现、采样原理与样条挤出实战

Three.js KnotCurve 完全解析:参数曲线实现、采样原理与样条挤出实战

2026-09-07 10:23:50作者:温玫谨Lighthearted

KnotCurve(绳结曲线)是 Three.js 中一类预置的三维解析曲线,它以封闭的参数方程直接在三维空间勾勒出环扣交错的"绳结/环面缠绕"轨迹。本文以官方文档页 KnotCurve 参考文档 为主线,结合其真实源码实现与官方示例,讲解它的构造方式、getPoint 采样语义、继承自 Curve 的弧长与切向工具,并给出基于 TubeGeometry 的管线挤出与沿曲线相机漫游的完整实战方案。

本文面向需要在 three.js 中快速获得一条现成封闭三维曲线、并将其用于生成绳结管道几何体、镜头路径或粒子轨迹的开发者。阅读完成后,你将掌握 KnotCurve 的数学本质、正确导入与调用姿势,以及把任意曲线接入挤出/相机系统的方法。


一、KnotCurve 是什么

KnotCurve 属于 Three.js 的 addon(扩展)曲线,它并不是内核类,而是与 HeartCurve、GrannyKnot、VivianiCurve、TrefoilKnot、HelixCurve 等一批"参数化趣味曲线"一起被打包在文件 examples/jsm/curves/CurveExtras.js 中(见该文件 679-694 行的导出列表)。

  • 类继承关系KnotCurve extends Curve,即直接继承自核心曲线抽象基类 Curve。因此它天然拥有 Curve 提供的一整套方法:getPointsgetSpacedPointsgetLengthgetLengthsgetTangentgetTangentAtcomputeFrenetFrames 等(下文第五节展开)。
  • 本质:一条无参数构造、无公共状态的解析曲线。其每个点的坐标都由 t 代入固定参数方程算出,是一种最纯粹的"数学曲线",不依赖控制点。
  • 曲线形态:从源码公式看(下文第二节),轨迹点同时满足
    x² + ( √(y²+z²) − R )² = s² ,其中 R=10,s=50
    
    这是圆环面(torus)的标准方程,因此 KnotCurve 实际是缠绕在某一大半径圆环面上的封闭曲线,闭合于三维空间中,形似一个"立体绳圈"。

判定一条曲线是否"闭合"看端点即可:KnotCurve 中 getPoint(0)getPoint(1) 均返回点 (0, 60, 0)(当 cos t = 1y = R + s = 60),所以它天然适合与 TubeGeometry(..., closed=true) 这类闭合几何体配合。


二、曲线背后的参数方程(源码级解读)

官方文档仅说明 KnotCurve "a knot curve",其具体几何形态由源码决定。查看 CurveExtras.js 中 KnotCurve 的实现,其核心代码如下:

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

    const point = optionalTarget;

    t *= 2 * Math.PI;              // 把 [0,1] 映射到 [0, 2π)

    const R = 10;                  // 内部常量:圆环大半径
    const s = 50;                  // 内部常量:圆环管径 / 缠绕幅度

    const x = s * Math.sin( t );
    const y = Math.cos( t ) * ( R + s * Math.cos( t ) );
    const z = Math.sin( t ) * ( R + s * Math.cos( t ) );

    return point.set( x, y, z );

}

2.1 逐段理解公式

分量 表达式 说明
自变量 t *= 2 * Math.PI 归一化参数 t∈[0,1] 被放大到角度域 [0, 2π),跑满整整一圈
x s * Math.sin(t) s = 50,水平摆动幅度,
y cos(t) * (R + s·cos(t)) 与 z 共同构成"圆环绕行"分量
z sin(t) * (R + s·cos(t)) 同上,两个分量通过正余弦搭配产生绳结的立体交叠
常量 R = 10s = 50 均为硬编码常量,不可通过构造参数修改

2.2 为什么它看起来像"绳结"

x = s·sin(t)√(y²+z²) = |R + s·cos(t)|,可将三点坐标统一成

x = s · sin t
√(y² + z²) = R + s · cos t   (取主值域讨论)

这正是圆环面参数化:x 充当环面"管截面"上的缠绕角,(y,z) 平面内到原点的距离绕大圆半径 R 波动。由于 R=10 远小于 s=50,缠绕半径大于环面大半径,轨迹在空间中的投影会反复"穿越"自身附近区域,从而产生视觉上类似打结、缠绕的立体效果。

坐标量级可直接估算:|x| ≤ s = 50(R + s·cos t) 取值区间为 [R−s, R+s] = [-40, 60],再乘上 cos t / sin t(范围都在 [−1,1]),故 yz 的绝对值均以约 60 为界。实际渲染时通常需要把对象整体缩小(示例中相机远在 z=500 处、网格缩放到 scale=4 才显得协调)。


三、导入与安装

文档明确指出:KnotCurve 是 addon,必须显式导入,不会被 three 主包自动引入。

import { KnotCurve } from 'three/addons/curves/CurveExtras.js';

由于 CurveExtras.js 采用具名多导出(GrannyKnot、HeartCurve、KnotCurve、TorusKnot、CinquefoilKnot 等共 14 个类),实际开发中常见两种用法:

方式一:具名导入单个类

import { KnotCurve } from 'three/addons/curves/CurveExtras.js';

const curve = new KnotCurve();

方式二:命名空间整体导入(官方示例的做法)

官方示例 webgl_geometry_extrude_splines.html 中这样写:

import * as Curves from 'three/addons/curves/CurveExtras.js';

随后在曲线字典中创建实例:

const splines = {
    GrannyKnot: new Curves.GrannyKnot(),
    HeartCurve: new Curves.HeartCurve( 3.5 ),
    VivianiCurve: new Curves.VivianiCurve( 70 ),
    KnotCurve: new Curves.KnotCurve(),   // 无参构造
    TrefoilKnot: new Curves.TrefoilKnot(),
    // ...
};

两种方式等价,可视代码风格选择。Import map 是让 three/addons/ 前缀可用的关键:上述官方示例在页面顶部通过 <script type="importmap">"three/addons/" 映射到本仓库的 ./jsm/示例 importmap)。若使用 npm + 打包器,three/addons/*three 包的 examples 目录自动提供,无需额外配置。


四、构造函数与 getPoint 采样

4.1 构造函数

new KnotCurve()

不接受任何参数。这是它与 CurveExtras 中其他曲线的一个显著差异——如 HeartCurve(scale)VivianiCurve(scale)TorusKnot(scale) 等都接受 scale 缩放参数并暴露为实例属性,而 KnotCurve 的实现把 R=10s=50 直接写在 getPoint 内部,因此实例本身不可调尺寸。若需整体缩放,请在几何体/网格层面处理(示例采用 mesh.scale,见 示例 setScale)。

4.2 .getPoint( t, optionalTarget ) : Vector3

这是 KnotCurve 覆盖 Curve#getPoint 的核心采样方法,签名如下:

参数 类型 说明
t number 插值因子,表示曲线上的位置,必须位于 [0,1]01 对应同一空间点(曲线闭合)
optionalTarget Vector3 可选。结果写入的目标向量。若不传,源码会使用默认值 new Vector3() 新建
  • 返回:曲线上对应位置的三维向量。
  • OverridesCurve#getPoint,即把基类中"未实现"的抽象方法落为具体参数方程。
  • 性能提示:源码 getPoint( t, optionalTarget = new Vector3() ) 展示了 three.js 经典的"可选目标复用"模式——高频批量采样时(如生成数千个点),可传入同一个 Vector3 反复接收结果,避免频繁 GC 分配:
const curve = new KnotCurve();
const tmp = new THREE.Vector3();

for ( let i = 0; i <= 500; i++ ) {
    curve.getPoint( i / 500, tmp );   // 复用 tmp,无额外分配
    // 使用 tmp.x / tmp.y / tmp.z
}

注意语义:t参数均匀而非弧长均匀的采样位置。KnotCurve 的方程中自变量与三角函数耦合,参数增量并不对应等弧长步进;若需要等距采样,应使用继承来的 getPointAt(u)(见下节),它会把 u 先经弧长查表转换为参数 t 再调用 getPoint


五、继承自 Curve 基类的实用能力

KnotCurve 的文档以 Inheritance: Curve → 开头,意味着你可以无成本地使用 Curve 基类(src/extras/core/Curve.js) 的全部工具。这些方法对 KnotCurve 全部可用且多数已通用实现

方法 作用 说明
getPoints(divisions=5) 参数均匀采样 采样 divisions+1 个点,默认 6 个点较稀疏,生成光滑曲线建议传 100~300
getPointAt(u, target) 弧长均匀采样 先经 getUtoTmapping 把 u 映射为参数 t 再取点,保证等距(见 getUtoTmapping 源码
getSpacedPoints(divisions=5) 等距取点序列 getPointAt 组合的便捷版
getLength() 曲线总长 通过累计弦长逼近弧长
getTangent(t) 切向量 默认用 t±0.0001 两点的差分归一化求得
getTangentAt(u) 弧长均匀处的切向量 相机 lookAt/方向跟随常用
computeFrenetFrames(segments, closed) Frenet 标架 输出 tangents/normals/binormals,被 TubeGeometry 内部用于确定管道截面朝向
arcLengthDivisions 弧长细分精度 默认 200,曲线"非常大"时可调大以保证弧长换算精度

弧长相关方法都依赖基类内部的 cacheArcLengths 缓存与 needsUpdate 标志;KnotCurve 无可变参数,故不存在"改参数后需 updateArcLengths()"的问题——该机制是为 CatmullRomCurve3 等可变参数曲线设计的(updateArcLengths 源码)。

调试时可直接在控制台快速验证:

import { KnotCurve } from 'three/addons/curves/CurveExtras.js';

const curve = new KnotCurve();
console.log( '长度 ≈', curve.getLength() );
console.log( '起点', curve.getPoint( 0 ) );
console.log( '终点', curve.getPoint( 1 ) );       // 与起点相同 → 闭合
console.log( '采样数', curve.getPoints( 200 ).length ); // 201 个点

六、实战:用 TubeGeometry 把 KnotCurve 挤出成绳结管道

KnotCurve 最典型的落地场景,是作为 TubeGeometry 的路径轴生成"绳结状的立体管道"。官方示例 webgl_geometry_extrude_splines.html 把 KnotCurve 与其他 13 条曲线统一注册成 spline 字典,任选其一重建管道。

关键代码如下(与示例一致):

import * as THREE from 'three';
import * as Curves from 'three/addons/curves/CurveExtras.js';

// 1. 创建曲线实例(也可从 splines 字典里切换)
const extrudePath = new Curves.KnotCurve();

// 2. 沿曲线生成管道几何体
const tubeGeometry = new THREE.TubeGeometry(
    extrudePath,   // path:任意 Curve 子类均可
    100,           // tubularSegments:沿路径的细分段数,越大越光滑
    2,             // radius:管道半径
    3,             // radialSegments:截面圆周细分(示例 GUI 允许 2~12)
    true           // closed:曲线闭合则必须为 true
);

// 3. 挂材质、加线框辅助、入场景
const material = new THREE.MeshLambertMaterial( { color: 0xff00ff } );
const mesh = new THREE.Mesh( tubeGeometry, material );
mesh.scale.set( 4, 4, 4 );              // 整体缩小以适配场景尺度
scene.add( mesh );

使用要点:

  • TubeGeometry 内部会调用 path.getPointAt 做等弧长布点、path.computeFrenetFrames( tubularSegments, closed ) 计算每个截面处的法线/副法线,从而保证管道不扭曲。由于这些基础能力全部来自 Curve 基类,任何自定义曲线都只需实现 getPoint 即可无缝接入 TubeGeometryTubeGeometry 与 Frenet 帧的协作关系见 Curve 源码注释)。
  • closed=true 与 KnotCurve 的闭合性必须一致,否则首尾截面朝向会出现"扭曲闭合"瑕疵。
  • 示例通过 GUI 可把 extrusionSegments 在 50~500、radiusSegments 在 2~12 之间实时调节,对应 TubeGeometry 的细分参数,读者可在浏览器中直观感受分段数对绳结表面平滑度的影响。

七、进阶:沿 KnotCurve 的相机漫游

KnotCurve 还能当作"第一人称过山车路径"。官方示例在 render() 循环中(示例 302-350 行)演示了完整套路:

const looptime = 20 * 1000;                       // 20 秒绕一圈
const t = ( Date.now() % looptime ) / looptime;   // 循环参数

// 弧长均匀取点,保证相机匀速
tubeGeometry.parameters.path.getPointAt( t, position );
position.multiplyScalar( params.scale );

// 帧间插值得到连续 binormal(Frenet 帧插值)
const segments = tubeGeometry.tangents.length;
const pickt = t * segments;
const pick = Math.floor( pickt );
const pickNext = ( pick + 1 ) % segments;
binormal.subVectors( tubeGeometry.binormals[ pickNext ], tubeGeometry.binormals[ pick ] )
         .multiplyScalar( pickt - pick )
         .add( tubeGeometry.binormals[ pick ] );

// 切线方向决定"前进方向"
tubeGeometry.parameters.path.getTangentAt( t, direction );
normal.copy( binormal ).cross( direction );
position.add( normal.clone().multiplyScalar( offset ) );   // 沿法线偏移,制造第一视角

splineCamera.position.copy( position );
splineCamera.matrix.lookAt( splineCamera.position, lookAt, normal );
splineCamera.quaternion.setFromRotationMatrix( splineCamera.matrix );

其中 tubeGeometry.parameters.path 指回创建几何体时传入的曲线对象。整段代码展示了本主题的核心能力组合:getPointAt(等弧长取位)+ getTangentAt(切线)+ Frenet 帧(法线/副法线插值) 三者协作即可在任意解析曲线上实现稳定的"贴地飞行"相机。


八、注意事项与常见误区

  1. 不要试图传构造参数缩放:KnotCurve 的 RsgetPoint 内部常量,new KnotCurve(scale) 不会生效。整体缩放请用 mesh.scalegeometry.scale(...),或自行子类化后在 getPoint 结果上 multiplyScalar
  2. 坐标系尺度较大:曲线点横跨约 ±60 个单位。在默认 0.01~1000 的近平/远裁剪下没问题,但若场景单位偏小,需注意缩放,否则可能超出相机视野。
  3. t 必须落在 [0,1]:文档与源码都强调该约束。虽然 sin/cos 对超出范围的参数仍能求值,但语义不再成立(闭合曲线会周期性重叠),请在采样循环中使用 i/divisions 这类归一化索引。
  4. 参数均匀 ≠ 弧长均匀:需要等速动画或等距布线时用 getPointAt / getSpacedPoints,不要直接用 getPoint 按固定参数间隔铺点。
  5. 导入路径问题:务必走 three/addons/curves/CurveExtras.js(addon 路径),而非 three 主模块;主包不导出该曲线。

小结

KnotCurve 是 three.js addon 体系中"零配置、拿来即用"的典型封闭三维解析曲线:它继承 Curve 基类、以硬编码的圆环参数方程(R=10, s=50)生成闭合绳结轨迹,通过 getPoint(t, optionalTarget) 采样,配合基类的弧长、切线与 Frenet 帧工具,可快速驱动 TubeGeometry 挤出管道、相机路径与粒子轨迹。若要继续深入,可直接阅读:

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