three.js Flow 曲线弯曲修改器完全指南:让网格沿曲线流动的 CurveModifier 详解

发布时间:2026/9/7 7:44:57
three.js Flow 曲线弯曲修改器完全指南:让网格沿曲线流动的 CurveModifier 详解 three.js Flow 曲线弯曲修改器完全指南让网格沿曲线流动的 CurveModifier 详解【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js导读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-flowthree.js 将其收录并演化为官方 addon 模块。从源码结构来看整个模块可以拆成三部分协同工作CPU 端曲线预处理把Curve离散采样连同其 Frenet 标架切向量、法向量、副法向量编码进一张数据纹理DataTexture供着色器查询着色器改写通过material.onBeforeCompile把标准顶点着色器替换为“沿曲线重定位顶点”的实现对象封装Flow单网格与InstancedFlow实例化网格对外暴露简单 API隐藏纹理与矩阵细节。渲染后端约束WebGL 与 WebGPUFlow 的实现依赖对材质着色器源码进行字符串级改写因此与渲染后端强相关WebGLRenderer使用 CurveModifier.js内部通过material.onBeforeCompile直接改写 GLSL 顶点着色器WebGPURenderer应导入 CurveModifierGPU.js 中的同名Flow其内部改用 TSLThree Shading Language的positionNode/normalNode以节点方式挂接重定位逻辑不再触碰 GLSL 字符串。官方文档对此有明确说明“This module can only be used with WebGLRenderer. When using WebGPURenderer, import the class fromCurveModifierGPU.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;若需要批量实例化版本则从同一文件导入InstancedFlowimport { 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.jsmoveAlongCurve( amount ) { this.uniforms.pathOffset.value amount; }它只是累加pathOffsetuniform 的值。在每帧渲染循环中持续传入一个很小的增量即可产生“流动”动画例如官方示例每帧执行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;initSplineTextureCurveModifier.js为每条曲线分配一块1024 × 4的DataTexture实际高度按TEXTURE_HEIGHT * numberOfCurves增长格式为RGBAFormat HalfFloatType数据数组按Uint16Array分配以承载半浮点。纹理环绕模式均设为RepeatWrapping过滤模式为LinearFilter。写入时updateSplineTextureCurveModifier.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 portionspinePortion (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见 getUniformsuniform默认值作用spineTexture样条数据纹理保存曲线采样与 Frenet 标架pathOffset0沿路径的时间/偏移量moveAlongCurve只改它pathSegment1路径覆盖的分数长度1 表示使用整条曲线spineOffset161网格“吸附”到曲线的起始世界 X 偏移spineLength400曲线总长由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.jsWebGLWebGPU 版源码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即可获得基于曲线驱动的低成本网格流动效果。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻