three.js Flow 曲线弯曲修改器完全指南:让网格沿曲线流动的 CurveModifier 详解
导读:Flow 是 three.js 官方提供在 three/addons/modifiers/CurveModifier.js 中的修改器(modifier),用于让网格(Mesh)沿着任意曲线“弯曲”与“流动”,是制作管道沿轨动画、路径游走文字、赛道形变等效果的核心工具。本文将基于官方文档 docs/pages/Flow.html 梳理 Flow 的构造函数、moveAlongCurve 与 updateCurve 等 API,并深入源码与示例,剖析其背后的“样条纹理 + 顶点着色器”实现原理、WebGL/WebGPU 双后端差异,以及面向大批量对象的 InstancedFlow 实例化用法,帮助你写出一段可直接运行沿曲线弯曲的网格动画。
Flow 是什么
Flow 是一个顶点层面的曲线弯曲修改器。给定一个普通的三维网格(例如一段文字、一条管道模型),它可以克隆该网格并把它的顶点重新映射到一条曲线的局部坐标系上,从而让整段网格在三维空间里“贴上”曲线并保持法线、朝向正确。它还可以随时间推进让网格沿着曲线持续“流动”(flushing),效果类似传送带或血液在血管中流动的动画。
其源码位于 examples/jsm/modifiers/CurveModifier.js,类注释开头的原始出处来自社区项目 zz85/threejs-path-flow,three.js 将其收录并演化为官方 addon 模块。
从源码结构来看,整个模块可以拆成三部分协同工作:
- CPU 端曲线预处理:把
Curve离散采样,连同其 Frenet 标架(切向量、法向量、副法向量)编码进一张数据纹理(DataTexture),供着色器查询; - 着色器改写:通过
material.onBeforeCompile把标准顶点着色器替换为“沿曲线重定位顶点”的实现; - 对象封装:
Flow(单网格)与InstancedFlow(实例化网格)对外暴露简单 API,隐藏纹理与矩阵细节。
渲染后端约束:WebGL 与 WebGPU
Flow 的实现依赖对材质着色器源码进行字符串级改写,因此与渲染后端强相关:
- WebGLRenderer:使用 CurveModifier.js,内部通过
material.onBeforeCompile直接改写 GLSL 顶点着色器; - WebGPURenderer:应导入 CurveModifierGPU.js 中的同名
Flow,其内部改用 TSL(Three Shading Language)的positionNode/normalNode以节点方式挂接重定位逻辑,不再触碰 GLSL 字符串。
官方文档对此有明确说明:“This module can only be used with WebGLRenderer. When using WebGPURenderer, import the class from CurveModifierGPU.js.”,InstancedFlow 类同样声明“只能用于 WebGLRenderer”。这意味着若你的项目运行在 WebGPU 后端,切换曲线弯曲能力时必须同步切换 import 来源,两套 API 方法名保持一致,便于无缝替换。
导入方式
Flow 属于 addon 模块,必须显式导入(详见官方 Installation#Addons 说明),不会自动包含在核心 three 包中:
import { Flow } from 'three/addons/modifiers/CurveModifier.js';
在 WebGPU 场景下改为:
import { Flow } from 'three/addons/modifiers/CurveModifierGPU.js';
若需要批量实例化版本,则从同一文件导入 InstancedFlow:
import { InstancedFlow } from 'three/addons/modifiers/CurveModifier.js';
构造函数与参数
new Flow( mesh : Mesh, numberOfCurves : number )
构造一个新的 Flow 实例,mesh 是被克隆并沿曲线弯曲的原始网格,numberOfCurves 是为将来追加更多曲线预分配的空间数量,默认值为 1。
重要语义:Flow 并不修改你传入的网格本身。从 CurveModifier.js 的构造实现可见,它先执行 mesh.clone() 得到 obj3D,随后遍历克隆体的所有后代:
- 对每个
Mesh/InstancedMesh子节点,若material是数组(多材质),则逐份clone()一份新材质并改写其着色器;否则克隆单一材质; - 材质改写通过
modifyShader( newMaterial, uniforms, numberOfCurves )完成; - 最后在实例上保存
curveArray、curveLengthArray、object3D、splineTexture、uniforms等内部状态。
因此使用时应把 flow.object3D 加入场景而不是原来的 mesh,且原始网格不受污染,可重复用于多个 Flow。
构造函数执行后的内部产物:
| 内部属性 | 含义 |
|---|---|
object3D |
克隆并“改性”后的场景对象,加入场景使用 |
splineTexture |
存储曲线采样点及 Frenet 标架的数据纹理 |
uniforms |
供着色器使用的统一变量(见下文) |
curveArray / curveLengthArray |
各曲线槽位对应的曲线对象与总弧长 |
new InstancedFlow( count, curveCount, geometry, material )
InstancedFlow 继承自 Flow,是实例化版本,适合把大量对象沿同一条或多条曲线排布与运动。构造参数:
- count:实例元素数量;
- curveCount:要预分配的曲线槽位数量;
- geometry:实例网格使用的几何体;
- material:实例网格使用的材质。
其构造实现先内部创建一个 InstancedMesh( geometry, material, count ),并做两项关键设置:instanceMatrix.setUsage( DynamicDrawUsage )(矩阵需逐帧动态更新)与 mesh.frustumCulled = false(顶点会在着色器中被重新定位,剔除包围盒不可靠,故禁用视锥剔除),随后才调用父类构造函数完成曲线纹理与材质初始化。
核心方法
.moveAlongCurve( amount : number )
沿曲线移动网格,amount 是推进偏移量。该方法实现非常简洁(CurveModifier.js):
moveAlongCurve( amount ) {
this.uniforms.pathOffset.value += amount;
}
它只是累加 pathOffset uniform 的值。在每帧渲染循环中持续传入一个很小的增量即可产生“流动”动画,例如官方示例每帧执行 flow.moveAlongCurve( 0.001 )。
需要说明的是:moveAlongCurve 的推进量并非严格的世界空间距离,而是与 pathSegment、曲线离散采样密度共同作用在纹理纵坐标上的增量,使用时通过视觉反馈调参即可。
.updateCurve( index : number, curve : Curve )
为给定的曲线槽位 index 更新曲线,curve 是用于弯曲网格的 Curve(例如 CatmullRomCurve3、CubicBezierCurve3)。该方法内部(CurveModifier.js)做四件事:
- 越界检查:
if ( index >= this.curveArray.length ) throw Error( 'Flow: Index out of range.' ); - 计算弧长:
const curveLength = curve.getLength(); - 同步
spineLengthuniform 与curveLengthArray[index]、curveArray[index]; - 调用
updateSplineTexture( this.splineTexture, curve, index )把曲线重新编码进数据纹理。
当交互式拖拽样条控制点、曲线几何发生变化后,应重新调用该方法让弯曲结果更新,官方示例正是在 TransformControls 的 dragging-changed 事件回调里调用 flow.updateCurve( 0, curve )。
源码深潜:曲线纹理与着色器如何工作
理解 Flow 的关键,是知道曲线不是被 CPU 逐帧计算的,而是被“烘焙”进纹理由 GPU 读取。
样条数据纹理的编码
模块顶部定义了纹理布局常量:
const CHANNELS = 4;
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 4;
initSplineTexture(CurveModifier.js)为每条曲线分配一块 1024 × 4 的 DataTexture(实际高度按 TEXTURE_HEIGHT * numberOfCurves 增长),格式为 RGBAFormat + HalfFloatType,数据数组按 Uint16Array 分配以承载半浮点。纹理环绕模式均设为 RepeatWrapping,过滤模式为 LinearFilter。
写入时 updateSplineTexture(CurveModifier.js)采样 numberOfPoints = 1024 个均匀弧长点:
- 通过
curve.arcLengthDivisions与updateArcLengths()预计算弧长表,保证按“弧长”均匀采样; getSpacedPoints获取采样点坐标;computeFrenetFrames( numberOfPoints, true )计算每一点的 Frenet 标架;- 数据纹理被组织为4 个通道行:第 0 行存顶点坐标,第 1/2/3 行分别存该点的切向量 tangent、法向量 normal、副法向量 binormal,三者构成局部正交基。
顶点着色器中的弯曲重定位
改写后的顶点着色器核心逻辑(CurveModifier.js)思路如下:
- 先把顶点变换到世界空间:
worldPos = modelMatrix * vec4(position, 1.); - 依据顶点在世界空间下的 x 坐标推算出它落在曲线的哪个“进度”(spine portion):
spinePortion = (worldPos.x + spineOffset) / spineLength;当flow <= 0时不弯曲(bend = false),并且该顶点 x 权重xWeight = 1,即仍保留原始 x 偏移; - 把进度换算为纹理采样纵坐标
mt,并对textureStacks(值为 1,即TEXTURE_HEIGHT / 4)取模以支持闭合曲线循环; - 从样条纹理中取出该点的位置
spinePos与局部基向量 a/b/c,构成mat3 basis; - 最终
transformed = basis * vec3(worldPos.x * xWeight, worldPos.y, worldPos.z) + spinePos,即“网格截面相对曲线位置 + 曲线绝对位置”,顶点法线也随之用basis旋转:transformedNormal = normalMatrix * (basis * objectNormal)。
与此同时,GLSL 片段中还注入了 5 个 uniform(见 getUniforms):
| uniform | 默认值 | 作用 |
|---|---|---|
spineTexture |
样条数据纹理 | 保存曲线采样与 Frenet 标架 |
pathOffset |
0 | 沿路径的时间/偏移量,moveAlongCurve 只改它 |
pathSegment |
1 | 路径覆盖的分数长度(1 表示使用整条曲线) |
spineOffset |
161 | 网格“吸附”到曲线的起始世界 X 偏移 |
spineLength |
400 | 曲线总长,由 updateCurve 自动同步为 curve.getLength() |
其中 spineOffset、spineLength 等默认值对应社区原始示例的默认场景尺寸。当网格几何体在世界 X 方向从 0 到某长度伸展时,弯曲会以 spineOffset 为起点、沿 spineLength 跨度贴合曲线;因此把被弯曲网格设计为沿 X 轴延伸(官方示例的 TextGeometry 即如此)是与 Flow 配合的隐含约定。
WebGPU 版实现差异
CurveModifierGPU.js 在 CPU 端纹理编码、构造流程上与 WebGL 版几乎一致,区别在着色器接入方式:
- 不再改写 GLSL 字符串,而是通过
material.positionNode挂接一个 TSLFn,内部用modelWorldMatrix、positionLocal、reference( 'pathOffset', 'float', uniforms )、texture( spineTexture, ... )等 TSL 节点复刻同样的弯曲公式; - 额外声明
varyingProperty( 'vec3', 'curveNormal' )并在material.normalNode输出,从而在节点系统内保持法线弯曲一致; flowuniform 在 TSL 版中被声明为float(通过reference),与 GLSL 版的int flow取值判断略有差异,但 API 层对使用者透明。
完整示例:一段沿曲线流动的文字
官方示例 webgl_modifier_curve.html 展示了 Flow 的最小可用套路。剥离交互与字体加载,核心链路如下:
// 1. 准备曲线
const curve = new THREE.CatmullRomCurve3( handlePoints ); // handlePoints 为控制点数组
curve.curveType = 'centripetal';
curve.closed = true;
// 2. 构造将被弯曲的网格(文字几何体 / 任意网格均可)
const geometry = new TextGeometry( 'Hello three.js!', { font, size: 0.2, depth: 0.05, ... } );
geometry.rotateX( Math.PI );
const material = new THREE.MeshStandardMaterial( { color: 0x99ffff } );
const objectToCurve = new THREE.Mesh( geometry, material );
// 3. 创建 Flow 并把曲线绑定到 0 号槽位
const flow = new Flow( objectToCurve );
flow.updateCurve( 0, curve );
scene.add( flow.object3D ); // 注意加入场景的是 flow.object3D
// 4. 渲染循环中持续推进,产生流动动画
flow.moveAlongCurve( 0.001 );
该示例还额外用 TransformControls 拖拽四个绿色方块控制点来修改 CatmullRomCurve3,并在每次拖拽结束(dragging-changed 且非拖拽中)时重新执行 flow.updateCurve( 0, curve )——这也是曲线交互编辑场景中必须牢记的“曲线变了要重新 updateCurve”惯例。
多实例版本:InstancedFlow 的用法
当对象数量很大时(例如大量文字或粒子排布在曲线上),逐个创建 Mesh 会让 draw call 激增,此时应使用 InstancedFlow。官方示例 webgl_modifier_curve_instanced.html 演示了 8 个实例沿 2 条曲线分布的情形:
const numberOfInstances = 8;
const flow = new InstancedFlow( numberOfInstances, curves.length, geometry, material );
// 先为每条曲线注册槽位
curves.forEach( function ( { curve }, i ) {
flow.updateCurve( i, curve );
scene.add( flow.object3D );
} );
// 再为每个实例选择曲线并分配初始位置
for ( let i = 0; i < numberOfInstances; i ++ ) {
const curveIndex = i % curves.length;
flow.setCurve( i, curveIndex );
flow.moveIndividualAlongCurve( i, i * 1 / numberOfInstances );
flow.object3D.setColorAt( i, new THREE.Color( 0xffffff * Math.random() ) );
}
InstancedFlow 额外提供三个方法(继承自 Flow 的同时复用其曲线纹理机制):
.setCurve( index, curveNo ):让第 index 个实例使用第 curveNo 条曲线;.moveIndividualAlongCurve( index, offset ):让第 index 个实例沿它所在的曲线独立推进 offset;.writeChanges( index ):把上述“曲线长度、曲线编号、偏移量”编码进该实例的模型矩阵并标记instanceMatrix.needsUpdate = true,供着色器中的USE_INSTANCING分支读取。
在 CurveModifier.js 中 writeChanges 使用一个复用的 Matrix4.makeTranslation 把 (spineLength, whichCurve, offset) 塞进实例矩阵平移分量,而顶点着色器里对应的 #ifdef USE_INSTANCING 分支正是从 instanceMatrix[3][0..2] 取出 spineLength、曲线编号和 pathOffset,从而让每个实例拥有独立的曲线归属与相位(着色器片段)。实例仍可整体使用 flow.moveAlongCurve( amount ) 让所有实例同步前进——示例的动画循环正是这样调用的。
实战要点与注意事项
- 加入场景的对象是
flow.object3D,不是传入构造函数的原始 mesh;构造函数内部会 clone 原始对象,原始对象不会被破坏。 - 网格建议沿 X 轴延伸:弯曲映射以世界 X 坐标为“脊柱进度”基准,需要水平延展的几何(文字、长条管道)在 X 方向布局可获得理想效果;
spineOffset/spineLength两个 uniform 可在材质上手动调节弯曲起点与跨度。 - 曲线改变后必须重新
updateCurve:动态编辑控制点时,在编辑结束的回调里对受影响槽位重放新曲线;越界索引会抛出Flow: Index out of range.。 - WebGL 与 WebGPU 分别导入:前者用
CurveModifier.js,后者用CurveModifierGPU.js,实例化时注意InstancedFlow目前仅声明支持 WebGLRenderer(对应源码中无 GPU 版InstancedFlow导出)。 - 动态实例记得设置:
InstancedFlow内部已自动配置DynamicDrawUsage与frustumCulled = false;如果你手动构造等价实现,这两项缺一不可,否则实例位置不会随矩阵更新或会在镜头旋转时被错误剔除。 - 纹理资源开销:每条曲线固定占据约
1024 × 4的 half-float 数据纹理,numberOfCurves决定预分配槽位数;大量曲线的场景请按需分配,避免过度预留。
相关资源
- 修改器源码:examples/jsm/modifiers/CurveModifier.js(WebGL)
- WebGPU 版源码:examples/jsm/modifiers/CurveModifierGPU.js
- 官方文档:Flow.html、InstancedFlow.html
- 官方示例:webgl_modifier_curve.html、webgl_modifier_curve_instanced.html
- 同目录其他修改器可对照参考:
EdgeSplitModifier、SimplifyModifier、TessellateModifier(均位于 examples/jsm/modifiers 目录)
结合官方示例直接运行验证,或在自身工程中以“克隆对象 + updateCurve + moveAlongCurve”三步接入 Flow,即可获得基于曲线驱动的低成本网格流动效果。
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 StartedRust0623
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