three.js TubePainter 实战指南:沿点序列绘制动态管状笔画的 API 原理与 XR 画笔应用
three.js TubePainter 实战指南沿点序列绘制动态管状笔画的 API 原理与 XR 画笔应用【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsTubePainter 是 three.js 提供的一个 misc杂项附加模块用于沿着一串三维点连续挤出出管状网格模拟真实画笔在空间中留下带粗细、带颜色渐变笔迹的效果它在官方 XR虚拟现实/增强现实绘画示例中承担核心绘制职责。阅读本文后你将掌握 TubePainter 的全部公开 APImoveTo/lineTo/setColor/setSize/update及mesh属性的用法、其内部动态几何缓冲与管线生成的工作原理并能够把同样的能力复用到桌面鼠标绘画、触控笔迹、粒子轨迹等场景中。TubePainter 是什么功能定位与适用场景根据本仓库 API 文档 docs/pages/TubePainter.html 与源码文件头部注释见 examples/jsm/misc/TubePainter.js的描述This module can be used to paint tube-like meshes along a sequence of points. This module is used in a XR painter demo.即TubePainter 沿一序列点生成管状网格与 three.js 内置的 TubeGeometry 不同它并不一次性根据既有曲线生成静态几何而是面向交互式连续绘制设计——每一帧随着手写笔 / 手柄 / 鼠标位置变化追加一小段管道已绘制的部分保持不动。因此它天然适配XR 空间画笔手柄按下扳机移动笔迹跟随手柄轨迹延伸官方示例 examples/webxr_xr_paint.html桌面自由绘画鼠标 / 触控在平面或曲面上拖拽出立体橡皮泥笔迹可视化轨迹把动态物体的运动轨迹渲染成有体积的光带。快速上手创建一支画笔并加入场景TubePainter 是一个addon附加模块不在核心库默认导出中必须显式引入。在本仓库的 npm 包导出结构中three/addons/*被映射到examples/jsm/*见 package.json 的exports字段因此以 npm 方式安装后可直接使用import { TubePainter } from three/addons/misc/TubePainter.js;最基础的使用流程只有两步——创建实例把它的画笔网格加入场景const painter new TubePainter(); scene.add( painter.mesh );此后调用painter.moveTo(起点)落笔再逐帧调用painter.lineTo(新位置)与painter.update()即可不断延长笔迹。若希望在浏览器中不经过打包工具直接运行可参照仓库 examples 目录下现有页面顶部的 importmap 写法见 examples/webxr_xr_paint.html把three映射到本地构建产物、把three/addons/映射到examples/jsm/目录即可让import { TubePainter } from three/addons/misc/TubePainter.js在原生 ES Module 下生效。构造函数与公开接口总览构造函数不接收任何参数const painter new TubePainter();实例暴露一个属性和五个方法下表对照源码逐一说明成员类型 / 签名作用源码位置meshMesh被画出来的管状网格必须手动加入场景才会显示examples/jsm/misc/TubePainter.jsmoveTo( position )Vector3将当前绘制笔尖移动到指定位置落笔不产生几何examples/jsm/misc/TubePainter.jslineTo( position )Vector3从当前笔尖向目标位置画一段笔画延长管道examples/jsm/misc/TubePainter.jssetColor( color )Color设置后续新画管段的顶点颜色examples/jsm/misc/TubePainter.jssetSize( size )number设置后续新画管段的粗细examples/jsm/misc/TubePainter.jsupdate()—把新写入缓冲区的几何数据上传到 GPU 并让其可见examples/jsm/misc/TubePainter.js.mesh : Meshpainter.mesh被绘制的管状网格。它由内部的BufferGeometry与MeshStandardMaterial组合而成材质开启了顶点着色vertexColors: true从而每条新画的笔画都能携带独立颜色。该网格的包围体不会随绘制自动刷新因此源码刻意将mesh.frustumCulled falseexamples/jsm/misc/TubePainter.js避免因视锥剔除误判把正在延长的笔迹裁掉。使用方需要把mesh加入场景或其它 Object3D 子树后才会被渲染。.moveTo( position : Vector3 )painter.moveTo( new THREE.Vector3( 0, 1.6, - 1 ) );把当前绘画位置移动到position。它只改变落笔参考点不产生任何几何体相当于把画笔抬起来移到新位置。从源码看examples/jsm/misc/TubePainter.jsmoveTo内部会重置三项笔画状态记录新起点point2复位法线参考lastNormal (0,1,0)标记isFirstSegment true下一段将重新计算初始坐标系清空上一笔的笔尖端盖记录endCapStartIndex null、endCapVertexCount 0准备在新起点处重新生成封口。.lineTo( position : Vector3 )painter.lineTo( new THREE.Vector3( x, y, z ) );从当前笔尖向position画出一段笔画并延长管道。这是 XR 手柄边移动边绘制时被逐帧调用的核心方法。其内部执行顺序见 examples/jsm/misc/TubePainter.js计算目标点与当前点的方向向量若两点重合长度为零则直接返回根据方向计算管道在该段的局部坐标系旋转最小化标架详见下文框架算法小节若这是新落笔后的第一段先为起点添加半球形封盖并立即在笔尖处预留一段末端封盖记录其顶点区间用于原地更新在上一截面与当前截面之间生成一圈管壁三角形调用updateEndCap把末端封盖原位改写到最新笔尖处使笔头始终是封闭的圆弧形而不是漏出中空截面将当前点、坐标系、颜色、粗细滚动保存为上一状态供下一段衔接。注意lineTo只负责向内部几何缓冲追加顶点要让渲染结果刷新还需要紧接着调用update()官方 demo 中两者总是在同一帧内先后出现。.setColor( color : Color )painter.setColor( new THREE.Color( 0xff3366 ) ); // 或按 HSL 动态取色 painter.setColor( new THREE.Color().setHSL( hue, 1.0, 0.5 ) );设置后续新绘制管段使用的颜色。它不会回溯改写已经画好的笔迹只影响此后追加的顶点。若在连续绘制中逐帧改变颜色相邻管段的顶点色不同因材质启用了顶点着色管壁上会呈现自然的颜色渐变过渡。.setSize( size : number )painter.setSize( 1.0 );设置后续新绘制管段的粗细。源码中管段横截面半径与size的关系为radius 0.01 * size见 examples/jsm/misc/TubePainter.js也就是说size 1时半径约为 0.01 个世界单位size 5时约为 0.05。与setColor相同它只影响之后追加的管段若绘制过程中粗细发生改变相邻截面尺寸不同会得到类似书法笔触的渐粗渐细效果。.update()painter.lineTo( target ); painter.update();lineTo/moveTo等调用只修改 CPU 侧的类型化数组update()负责把这些改动真正交给 GPU。它内部只对自上次 update 以来新增的顶点区间打上脏标记addUpdateRange并把needsUpdate置为true从而让渲染器按需上传这部分数据详见下文动态缓冲上传机制。从源码看实现原理理解 TubePainter 的关键在于它在构造时一次性预分配了超大容量的顶点缓冲之后每次画一笔只是在已有数组的尾部继续写数据并通过BufferGeometry.drawRange控制实际参与渲染的顶点数量逐渐扩大。以下逐层拆解 examples/jsm/misc/TubePainter.js 的实现。一次性预分配 动态绘制用法源码开头定义了常量与三个顶点属性examples/jsm/misc/TubePainter.jsconst BUFFER_SIZE 1000000 * 3; const positions new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 ); positions.usage DynamicDrawUsage; const normals new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 ); normals.usage DynamicDrawUsage; const colors new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 ); colors.usage DynamicDrawUsage; const geometry new BufferGeometry(); geometry.setAttribute( position, positions ); geometry.setAttribute( normal, normals ); geometry.setAttribute( color, colors ); geometry.drawRange.count 0;每个属性数组预分配300 万个Float32约合每属性 12 MB三个属性合计约 36 MB最多可容纳约 100 万个顶点三个属性都被标记为DynamicDrawUsage提示 GPU 驱动这些缓冲会频繁更新应优先放置在适合动态上传的存储初始drawRange.count 0即虽然分配了巨量显存缓冲但一开始一个顶点都不画每画一段几何顶点数增长代码只在数组尾部追加写入写入位置由不断递增的count变量维护。这一设计使 TubePainter 可以支撑长时间、高帧率的连续绘画而无需反复realloc或重建 BufferGeometry代价是内存一次性占用较高且单个实例存在约 100 万顶点的容量上限——源码中没有任何自动扩容逻辑达到上限后继续lineTo会超出预分配数组边界实践中若需要更长的笔迹应创建多个 TubePainter 实例分段管理。管壁几何横截面圆环与两截面之间搭桥每段管道的截面由一个圆环近似。getPointsexamples/jsm/misc/TubePainter.js在局部 X–Y 平面内生成均匀分布在圆周上的 15 个点sides 15圆的半径即上文提到的0.01 * size。当从旧截面推进到新截面时strokeexamples/jsm/misc/TubePainter.js把两个截面圆环上对应的 15 组顶点两两配对每个配对一个四边形拆成两个三角形写入缓冲。因此每画一小段大约新增15 × 2 × 3 90个顶点两个截面被相邻两段共享实际每段净增量与其相邻几何有关此处为粗略数量级。粗略估算若以每帧一小段、每段约 90 顶点计100 万顶点的容量大致可支撑上万次lineTo调用足以满足典型 XR 绘画会话。笔尖封口半球端盖与原位更新技巧若管道两端不做处理会露出中空圆环截面观感上像是被切断的水管。TubePainter 用addCapexamples/jsm/misc/TubePainter.js为两端补上圆弧形的半球封盖起始封盖新落笔后的第一次lineTo在起点处生成isEndCap false凸面朝外末端封盖紧接在笔尖处预先生成isEndCap true并记录它在缓冲区中的起始索引与顶点数endCapStartIndex/endCapVertexCountexamples/jsm/misc/TubePainter.js每次lineTo再通过updateEndCapexamples/jsm/misc/TubePainter.js原地改写这笔尖端盖的顶点使其始终跟住不断移动的笔尖。因为端盖顶点始终位于预分配数组的同一区间改写时只需对该区间调用addUpdateRange不会造成几何无限膨胀——已画出的笔杆顶点数只增不减而笔尖那顶帽子一直在原位被刷新。框架算法沿路径旋转最小化标架由源码推断要让管道在转弯处不拧麻花每一段都需要一个稳定的局部坐标系。源码中calculateRMFexamples/jsm/misc/TubePainter.js通过lastNormal、prevDirection、rotationAxis等状态变量沿路径逐段递推地计算横截面朝向从实现结构看采用的是**旋转最小化标架Rotation Minimizing FrameRMF也称平行传输法线**的思路首段根据当前前进方向相对世界轴的角度选择参考向量Y 轴优先方向接近 Y 轴时退化为 X 轴再施以 Gram–Schmidt 正交化得到法线与副法线后续段用上一段法线绕两段方向的叉积轴旋转实现法线沿路径最小旋转地搬运避免笔迹扭转平滑每段法线再向上一段法线插值 30%smoothFactor 0.3见 examples/jsm/misc/TubePainter.js让笔迹转弯更柔和。最终由matrix1.makeBasis( side, normal, -direction )组装出局部坐标系矩阵把截面圆环从局部空间变换到世界空间examples/jsm/misc/TubePainter.js。颜色与粗细的跨段渐变TubePainter 内部维护了当前值与上一段值两套状态color1/color2、size1/size2、matrix1/matrix2。stroke在同一个四边形网格的三角形顶点上分别写入两组颜色值见 examples/jsm/misc/TubePainter.js配合逐帧变化的setColor相邻两段之间便形成了平滑的顶点色渐变同理当笔刷尺寸在两段之间发生变化时两端截面半径不同管道呈现自然的粗细过渡。这也解释了官方 XR demo 为什么能在同一支笔上同时实现彩虹渐变色与可伸缩笔刷。动态缓冲上传机制update()的实现如下examples/jsm/misc/TubePainter.jslet count 0; function update() { const start count; const end geometry.drawRange.count; if ( start end ) return; positions.addUpdateRange( start * 3, ( end - start ) * 3 ); positions.needsUpdate true; // normals / colors 同理... count end; }count闭包变量记录上次已提交的顶点号drawRange.count记录当前几何实际顶点数每次update只把[count, end)这一新增区间通过addUpdateRange登记为待上传再置needsUpdate true渲染时由 WebGLRenderer 读取该更新范围做部分缓冲上传若没有新顶点start end则直接返回避免无谓的 GPU 开销。这种增量标记 部分上传机制保证了连续绘画时每帧仅上传新增的那一小段数据是整支画笔能实时跟手的关键。一个可直接运行的桌面绘画示例TubePainter 并不绑定 XR——把官方 demo 中手柄位置替换成鼠标射线与地面的交点就得到一个立即可玩的桌面版本。下面示例演示了moveTo落笔、按住鼠标拖动画线、实时换色、结束后抬笔的完整闭环!DOCTYPE html html langzh-CN head meta charsetutf-8 titleTubePainter 桌面绘画/title style html, body { margin: 0; overflow: hidden; } #info { position: absolute; top: 10px; left: 10px; color: #fff; font-family: sans-serif; } /style /head body div idinfo按住鼠标在地面上拖动绘画颜色随时间自动变化/div script typeimportmap { imports: { three: ../build/three.module.js, three/addons/: ./jsm/ } } /script script typemodule import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; import { TubePainter } from three/addons/misc/TubePainter.js; const renderer new THREE.WebGLRenderer( { antialias: true } ); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); const scene new THREE.Scene(); scene.background new THREE.Color( 0x111122 ); const camera new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 ); camera.position.set( 2, 3, 5 ); const controls new OrbitControls( camera, renderer.domElement ); controls.target.set( 0, 1, 0 ); scene.add( new THREE.GridHelper( 6, 12 ) ); scene.add( new THREE.HemisphereLight( 0xffffff, 0x444466, 2 ) ); const painter new TubePainter(); painter.setSize( 1.2 ); scene.add( painter.mesh ); // 把鼠标指针投射到 y 0 的地面平面上 const raycaster new THREE.Raycaster(); const pointer new THREE.Vector2(); const groundPlane new THREE.Plane( new THREE.Vector3( 0, 1, 0 ), 0 ); const target new THREE.Vector3(); let isDrawing false; function pickTarget() { raycaster.setFromCamera( pointer, camera ); return raycaster.ray.intersectPlane( groundPlane, target ) ! null; } renderer.domElement.addEventListener( pointerdown, ( e ) { pointer.set( ( e.clientX / innerWidth ) * 2 - 1, -( e.clientY / innerHeight ) * 2 1 ); if ( ! pickTarget() ) return; isDrawing true; painter.moveTo( target ); // 落笔 painter.setColor( new THREE.Color().setHSL( Math.random(), 1, 0.5 ) ); } ); renderer.domElement.addEventListener( pointermove, ( e ) { pointer.set( ( e.clientX / innerWidth ) * 2 - 1, -( e.clientY / innerHeight ) * 2 1 ); if ( ! isDrawing || ! pickTarget() ) return; painter.setColor( new THREE.Color().setHSL( ( performance.now() * 0.0005 ) % 1, 1, 0.5 ) ); painter.lineTo( target ); // 画一笔 painter.update(); // 提交新顶点 } ); window.addEventListener( pointerup, () ( isDrawing false ) ); renderer.setAnimationLoop( () renderer.render( scene, camera ) ); /script /body /html说明若你在已安装three的 npm 项目Vite 等中运行可去掉 importmap改回import ... from three/addons/misc/TubePainter.js即可若要在本仓库examples/目录下离线运行则保留 importmap 并保证核心库构建产物就位参见 package.json 中定义的构建脚本。XR 画笔官方示例逐段解读文档开头提到的XR painter demo正是仓库中的 examples/webxr_xr_paint.html它与 TubePainter 组合使用的方式极具参考价值核心流程如下。1. 为左右手各创建一支画笔。每个手柄控制器实例对应一个独立 TubePainter这样左右手可以同时作画、互不干扰examples/webxr_xr_paint.htmlconst painter1 new TubePainter(); scene.add( painter1.mesh ); const painter2 new TubePainter(); scene.add( painter2.mesh );2. 用select事件模拟按下/松开扳机。按下瞬间先同步手柄的世界矩阵再从手柄前端名为pivot的小标记物体取出世界坐标作为落笔点调用painter.moveTo( cursor )松开时仅把状态位复位examples/webxr_xr_paint.html。3. 用squeeze握紧事件调节笔刷粗细。握住手柄上下移动时根据手柄位移增量缩放笔尖标记并同步把缩放值传给painter.setSize()同时限制最小值为0.1examples/webxr_xr_paint.html、[L180-L188]。4. 动画循环里边移动边画。这是最有代表性的模式——每帧处理每个手柄若处于按压状态就依据当前时间生成连续变化的 HSL 色调用setColor上色把笔尖pivot的世界坐标作为目标调用lineTo最后立刻update()提交examples/webxr_xr_paint.htmlif ( userData.isSelecting true ) { const hue ( performance.now() * 0.001 ) % 1; color.setHSL( hue, 1.0, 0.5 ); painter.setColor( color ); painter.lineTo( cursor ); painter.update(); }配合renderer.setAnimationLoop( animate )与renderer.xr.enabled true这段代码构成了完整的VR 彩虹画笔笔迹颜色随时间流动、粗细可握持调节、笔头始终是封闭圆头。pivot只是一个很小的二十面体网格examples/webxr_xr_paint.html用于在控制器前端提供可视化的笔尖位置并作为lineTo的世界坐标来源。使用建议与注意事项容量有上限单实例预分配约 100 万顶点写入超过数组长度会越界超长笔迹应拆分为多个 TubePainter 实例或周期性封笔换新。先moveTo再lineTo新落笔必须先用moveTo设置起点否则首次lineTo会从原点附近开始每次moveTo后第一条lineTo才会生成起始封盖。lineTo之后必须update()前者只写 CPU 缓冲后者才触发 GPU 上传官方示例中两者成对出现。颜色 / 粗细是面向未来的setColor与setSize只影响随后追加的管段修改前已画好的笔迹不受影响适合做逐段渐变而非整体改色。关闭视锥剔除内部已将mesh.frustumCulled false请勿在外部重新开启否则笔迹延伸出包围体后可能被误剔除。材质为 MeshStandardMaterial需要光照环境才能呈现立体感示例中搭配了半球光与平行光若做纯色扁平风格可另行更换材质并保持vertexColors: true。使用前确保三点共线之外的状态正确连续调用lineTo时若目标点与当前点重合内部会直接跳过不会产生零长度段。相关文件速查用途仓库路径TubePainter 完整源码本文全部行号引用均指向此文件examples/jsm/misc/TubePainter.jsXR 画笔官方示例含左右手、粗细与颜色控制的完整集成examples/webxr_xr_paint.html本 API 参考文档源Markdowndocs/pages/TubePainter.html.md本 API 参考文档HTMLdocs/pages/TubePainter.htmladdon 聚合导出TubePainter 被统一收录于此examples/jsm/Addons.jsnpm 包导出映射three/addons/*→examples/jsm/*package.json【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考