拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Three.js 中 RollerCoasterLiftersGeometry 深度解析:沿曲线程序化生成过山车支撑结构

Three.js 中 RollerCoasterLiftersGeometry 深度解析沿曲线程序化生成过山车支撑结构【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsRollerCoasterLiftersGeometry是 three.js 官方 addons 中用于程序化生成过山车“支撑结构lifters”的工具类给它一条轨道曲线和分段数它就会沿曲线挤出横梁与斜撑柱并在弯道处自动做出与列车一致的内倾banking。读完本文你能独立把该类接入自己的场景理解curve/divisions两个构造参数的真实语义、内部挤出与 banking 角度的计算方式以及高/低两段支撑横梁立柱 vs 单斜撑的分支逻辑。类层级、导入与文件位置官方文档对它的定义是一句话“A procedural roller coaster lifters geometry.”程序化过山车支撑结构几何体继承关系为EventDispatcher → BufferGeometry → RollerCoasterLiftersGeometry该文档同时提示RollerCoasterLiftersGeometry是一个 addon附加模块不属于three核心包必须显式导入不能依赖核心入口的聚合导出。其源码位于 examples/jsm/misc/RollerCoaster.js导入语句与官方文档一致import { RollerCoasterLiftersGeometry } from three/addons/misc/RollerCoaster.js;导入路径three/addons/...的解析方式有两种以当前仓库package.json的exports字段为准以 npm 包方式引入时package.json中配置了./addons/*: ./examples/jsm/*因此three/addons/misc/RollerCoaster.js会映射到包内examples/jsm/misc/RollerCoaster.js直接浏览仓库示例时页面用 importmap 把three/addons/映射到本地examples/jsm/目录例如 examples/webxr_vr_rollercoaster.html 中script typeimportmap { imports: { three: ../build/three.module.js, three/addons/: ./jsm/ } } /script同一个源文件实际导出 5 个类见文件末尾export语句examples/jsm/misc/RollerCoaster.jsRollerCoasterGeometry轨道本身、RollerCoasterLiftersGeometry支撑结构即本文主题、RollerCoasterShadowGeometry地面投影暗纹、SkyGeometry天空片、TreesGeometry树木。本文只展开RollerCoasterLiftersGeometry其余类在“配套几何体”一节中简要说明。构造参数与几何体输出属性new RollerCoasterLiftersGeometry( curve, divisions )构造器接受两个参数源码中的 JSDocexamples/jsm/misc/RollerCoaster.js与官方文档描述一致参数类型说明源码中的实际要求curveCurve沿其生成几何体的曲线只需具备getPointAt( t )与getTangentAt( t )两个方法参数t在[0, 1]区间getTangentAt必须返回归一化切线divisionsnumber分段数决定几何体精细度生成divisions个支撑段、12 × divisions个三角面曲线仅在t i / divisions (i 1..divisions)处采样两点值得注意的边界语义均可在 构造器主循环 中验证采样不包含 t 0。循环从i 1开始curve.getPointAt(0)在 lifters 几何体中根本不会被调用曲线起点附近缺少第一段支撑这是与RollerCoasterGeometry其prevPoint会取getPointAt(0)不同的细节。若曲线是闭合环如官方示例起点/终点首尾相接缺口影响很小若曲线是开放线第一个分段会从t 1/divisions才开始。切线采样对闭合曲线取模回绕banking 计算中前后采样点( t ± bankDelta ) % 1 1 ) % 1保证t在 0/1 边界附近也能正确取样因此闭合曲线在接缝处不会出现 banking 跳变。构造完成后几何体只写入两个BufferAttributeexamples/jsm/misc/RollerCoaster.js属性类型项数positionBufferAttributeFloat32ArrayitemSize 336 × divisions个顶点分量normal同上同上没有index、没有color、没有uv。由此推出两条实用结论这是一个非索引三角形汤triangle soup每段固定 12 个三角面3 个截面轮廓 × 每轮廓 4 个三角形顶点与法线均为按面展开的平直着色flat shading效果法线直接来自截面形状顶点方向无需computeVertexNormals()材质必须使用纯色材质例如官方示例用的MeshPhongMaterial。若误用vertexColors: true因为没有color属性顶点色读取结果未定义/异常。完整可运行示例自定义曲线对象 支撑结构examples/webxr_vr_rollercoaster.html 是RollerCoasterLiftersGeometry在仓库中唯一的实际调用点值得完整剖析。它展示了“curve参数到底需要什么”——示例并没有使用THREE.Curve子类而是用闭包对象手写了两个方法第 110-142 行const PI2 Math.PI * 2; const curve ( function () { const vector new THREE.Vector3(); const vector2 new THREE.Vector3(); return { getPointAt: function ( t ) { t t * PI2; const x Math.sin( t * 3 ) * Math.cos( t * 4 ) * 50; const y Math.sin( t * 10 ) * 2 Math.cos( t * 17 ) * 2 5; const z Math.sin( t ) * Math.sin( t * 4 ) * 50; return vector.set( x, y, z ).multiplyScalar( 2 ); }, getTangentAt: function ( t ) { const delta 0.0001; const t1 Math.max( 0, t - delta ); const t2 Math.min( 1, t delta ); return vector2.copy( this.getPointAt( t2 ) ) .sub( this.getPointAt( t1 ) ).normalize(); } }; } )();要点类内部所有曲线访问都走getPointAt/getTangentAt弧长参数化 API所以任何实现了这两个方法的对象THREE.CatmullRomCurve3、THREE.CubicBezierCurve3或手写对象都能传入getPointAt内部把t乘以2π把[0,1]映射成整圈闭合轨道——这正是 lifters 生成循环对t取模回绕能够无缝工作的原因getTangentAt用中心差分近似切线并normalize()满足源码对归一化切线的要求。随后三个几何体共享同一条曲线分段数各自独立第 144-163 行geometry new RollerCoasterGeometry( curve, 1500 ); material new THREE.MeshPhongMaterial( { vertexColors: true } ); mesh new THREE.Mesh( geometry, material ); scene.add( mesh ); geometry new RollerCoasterLiftersGeometry( curve, 100 ); material new THREE.MeshPhongMaterial(); mesh new THREE.Mesh( geometry, material ); mesh.position.y 0.1; scene.add( mesh ); geometry new RollerCoasterShadowGeometry( curve, 500 ); material new THREE.MeshBasicMaterial( { color: 0x305000, depthWrite: false, transparent: true } ); mesh new THREE.Mesh( geometry, material ); mesh.position.y 0.1; scene.add( mesh );这里有两个可直接复用的工程细节分段数按视觉密度分配轨道用 1500 段求光滑支撑立柱用 100 段已足够立柱本来就是稀疏排布的投影暗纹用 500 段。三者共用曲线但互不影响divisions只是各自内部采样密度mesh.position.y 0.1抬高 0.1 个单位支撑底部与投影贴花都落在y 0平面若不抬高会与地面/贴花发生深度竞争z-fighting产生闪烁条纹。源码剖析三个十字截面与 extrudeShape 挤出三组截面形状构造器开头定义了三组 3 点轮廓examples/jsm/misc/RollerCoaster.js都是“十字形/星形”截面坐标位于轨道切平面局部坐标系中局部y指向地面方向局部z为切线前进方向const tube1 [ new Vector3( 0, 0.05, - 0.05 ), new Vector3( 0, 0.05, 0.05 ), new Vector3( 0, - 0.05, 0 ) ]; const tube2 [ new Vector3( - 0.05, 0, 0.05 ), new Vector3( - 0.05, 0, - 0.05 ), new Vector3( 0.05, 0 ) ]; const tube3 [ new Vector3( 0.05, 0, - 0.05 ), new Vector3( 0.05, 0, 0.05 ), new Vector3( - 0.05, 0 ) ];tube1与tube3形状相同一条竖边加一个尖点仅朝向相反尖点朝x/-xtube2是它们的旋转版本尖点朝z即轨道前进方向。三组截面共同“拼装”出支撑结构高架段用tube1做横梁、tube2/tube3做左右斜撑低区段只使用tube3做单根斜撑。extrudeShape截面轮廓 → 两三角形/边的挤出核心挤出函数extrudeShape( shape, fromPoint, toPoint )examples/jsm/misc/RollerCoaster.js把截面轮廓沿fromPoint → toPoint连线“扫”出侧面function extrudeShape( shape, fromPoint, toPoint ) { for ( let j 0, jl shape.length; j jl; j ) { const point1 shape[ j ]; const point2 shape[ ( j 1 ) % jl ]; vector1.copy( point1 ); vector1.applyQuaternion( quaternion ); vector1.add( fromPoint ); vector2.copy( point2 ); vector2.applyQuaternion( quaternion ); vector2.add( fromPoint ); vector3.copy( point2 ); vector3.applyQuaternion( quaternion ); vector3.add( toPoint ); vector4.copy( point1 ); vector4.applyQuaternion( quaternion ); vector4.add( toPoint ); vertices.push( vector1.x, vector1.y, vector1.z ); vertices.push( vector2.x, vector2.y, vector2.z ); vertices.push( vector4.x, vector4.y, vector4.z ); vertices.push( vector2.x, vector2.y, vector2.z ); vertices.push( vector3.x, vector3.y, vector3.z ); vertices.push( vector4.x, vector4.y, vector4.z ); // 法线截面点方向经同一四元数旋转后归一化 normal1.copy( point1 ); normal1.applyQuaternion( quaternion ); normal1.normalize(); // ... normal2 / normal3 / normal4 同理 normals.push( /* ... */ ); } }其几何含义对轮廓的第j条边shape[j] → shape[(j1) % jl]取边两端在fromPoint处和toPoint处的 4 个角点拆成两个三角形写入vertices3 点轮廓即产生 3 条边 × 2 6 个三角形。每个截面轮廓贡献 6 个三角形所以每段的三角面总数为3 个轮廓 × 6 18注意源码实际调用方式每段最多调用 3 次extrudeShape每次 6 个三角形合计 18 个三角形若只走低区分支则 6 个。前面“每段 12 个三角面”应按分支修正高架段 18、低区段 6总面数介于6 × divisions与18 × divisions之间取决于曲线高度分布局部点先applyQuaternion( quaternion )再平移到曲线点等价于在“轨道切平面局部坐标系”里摆好截面再整体放到世界空间法线取截面点方向copy( point1 )后旋转、归一化因此每个三角面的法线都平行于该截面的径向方向——这正是一根“棱柱”应有的面法线也解释了为什么输出天然呈平直flat外观。姿态切线定朝向banking 定内倾主循环examples/jsm/misc/RollerCoaster.js每段先算两个四元数for ( let i 1; i divisions; i ) { point.copy( curve.getPointAt( i / divisions ) ); tangent.copy( curve.getTangentAt( i / divisions ) ); const angle Math.atan2( tangent.x, tangent.z ); quaternion.setFromAxisAngle( up, angle ); // banking const bankDelta 0.01; const t i / divisions; sample1.copy( curve.getTangentAt( ( ( t - bankDelta ) % 1 1 ) % 1 ) ); sample2.copy( curve.getTangentAt( ( t bankDelta ) % 1 ) ); let headingChange Math.atan2( sample2.x, sample2.z ) - Math.atan2( sample1.x, sample1.z ); if ( headingChange Math.PI ) headingChange - Math.PI * 2; if ( headingChange - Math.PI ) headingChange Math.PI * 2; bankedQuaternion.copy( quaternion ); rollQuaternion.setFromAxisAngle( tangent, - Math.atan( headingChange * 8 ) * 0.5 ); bankedQuaternion.premultiply( rollQuaternion );quaternion只绕世界up (0,1,0)旋转atan2( tangent.x, tangent.z )使局部z轴对准切线水平方向——它负责“朝向”不含内倾banking 部分在t ± 0.01处采样两条切线用atan2(x, z)求出航向角差headingChange并归一到(-π, π]防止跨 ±π 跳变再按-Math.atan( headingChange * 8 ) * 0.5得到绕切线轴的侧滚角premultiply到朝向四元数上得到bankedQuaternion。转弯越急单位弧长内航向变化越大侧倾越大这段 banking 公式与 examples/webxr_vr_rollercoaster.html 中列车train每帧的姿态计算完全同源——这就是为什么列车过弯内倾时轨道下的支撑柱也以同一角度内倾视觉上保持“车体与轨道刚性一致”。高度分支横梁双斜撑 vs 单斜撑每段姿态确定后代码按曲线点的高度point.y分成两条支路examples/jsm/misc/RollerCoaster.jsif ( point.y 10 ) { // 横梁轨道正下方 0.35 处沿左右方向 ±0.75 横向伸出 fromPoint.set( - 0.75, - 0.35, 0 ); fromPoint.applyQuaternion( quaternion ); fromPoint.add( point ); toPoint.set( 0.75, - 0.35, 0 ); toPoint.applyQuaternion( quaternion ); toPoint.add( point ); extrudeShape( tube1, fromPoint, toPoint ); // 左侧斜撑从轨道下方 0.3 处落到地面 ( -0.7, 0 ) fromPoint.set( - 0.7, - 0.3, 0 ); fromPoint.applyQuaternion( quaternion ); fromPoint.add( point ); toPoint.set( - 0.7, - point.y, 0 ); toPoint.applyQuaternion( quaternion ); toPoint.add( point ); extrudeShape( tube2, fromPoint, toPoint ); // 右侧斜撑镜像到 ( 0.7, 0 ) fromPoint.set( 0.7, - 0.3, 0 ); fromPoint.applyQuaternion( quaternion ); fromPoint.add( point ); toPoint.set( 0.7, - point.y, 0 ); toPoint.applyQuaternion( quaternion ); toPoint.add( point ); extrudeShape( tube3, fromPoint, toPoint ); } else { // 单根斜撑轨道下方 0.2 处落到正下方地面 fromPoint.set( 0, - 0.2, 0 ); fromPoint.applyQuaternion( bankedQuaternion ); fromPoint.add( point ); toPoint.copy( fromPoint ); toPoint.y 0; extrudeShape( tube3, fromPoint, toPoint ); }结构语义可以这样读高架段point.y 10生成 1 根横向横梁局部 x 方向±0.75距轨下 0.35加 2 根斜撑柱局部 x ±0.7从轨下 0.3 一直拉到y 0的地面。注意toPoint.set( ±0.7, - point.y, 0 )中的- point.y局部y指向地面因此“下降到地面”的局部距离正好是曲线点的世界高度point.y撑柱底部会精确落在y 0平面上——这也是为什么该类的“地面”被硬编码为y 0低区段point.y 10只生成 1 根短斜撑从轨下 0.2 处落到正下方地面姿态差异横梁与撑柱上端使用不含 banking 的quaternion保证连接轨道的端点始终与轨道切平面对齐而低区短撑整体使用bankedQuaternion让贴地短支撑在弯道处跟着内倾与截图中的视觉效果一致从源码结构看y 10这个阈值隐含了作者对场景的假设示例曲线y 5 波动乘 2 后大部分点高 8~14 之间波动10恰好把“高架段”与“近地面段”分开。若换用你自己的曲线需要确认曲线高度单位与10这个阈值匹配否则支撑风格会整体偏移全部高架或全部矮柱。实用细节与配套几何体divisions 与规模divisions直接决定曲线采样次数与面数每个采样段 6 或 18 个三角形且几何体非索引无法在 GPU 端共享顶点。官方示例取 100 段600~1800 个三角形对支撑结构这类远景元素完全够用不要盲目沿用轨道的 1500 段几何体在构造时一次性生成完毕之后不再变化。曲线是静态参数——若想让支撑跟随动态曲线只能每帧重建几何体会产生 GC 压力不推荐。材质与摆放清单结合“无 index、无 color、法线已内置”三点最小可用配置如下import { RollerCoasterLiftersGeometry } from three/addons/misc/RollerCoaster.js; const geometry new RollerCoasterLiftersGeometry( curve, 100 ); const material new THREE.MeshPhongMaterial(); // 纯色勿开 vertexColors const lifters new THREE.Mesh( geometry, material ); lifters.position.y 0.1; // 避免与地面/贴花 z-fighting scene.add( lifters );MeshLambertMaterial、MeshStandardMaterial同样适用关键约束只有“纯色材质”与“y平面是地面”两条。配套几何体同一条曲线的完整过山车RollerCoasterLiftersGeometry在仓库中的定位是 examples/webxr_vr_rollercoaster.html 场景的配角完整场景由同一份 examples/jsm/misc/RollerCoaster.js 提供三个几何体叠加而成类作用属性示例中的分段数RollerCoasterGeometry轨道本体双轨、阶梯、弯道自动 bankingposition/normal/color1500RollerCoasterLiftersGeometry支撑立柱本文主题position/normal100RollerCoasterShadowGeometry轨道在地面的半透明暗纹投影y 0平面仅position500三者都只要求curve具备getPointAt/getTangentAt且 banking 公式同源因此同一曲线生成的轨道、支撑、投影在姿态上天然自洽无需逐一对齐。小结与延伸阅读RollerCoasterLiftersGeometry用一个构造器参数曲线 分段数封装了完整的程序化支撑生成管线切线朝向四元数 → 航向差分 banking 内倾 → 三组十字截面沿高/低两分支挤出 → 直接输出含面法线的非索引BufferGeometry。其工程价值不在于“过山车”这个题材而在于它演示了 three.js 中“手写曲线挤出几何体”的标准做法用getPointAt/getTangentAt驱动局部坐标系、用四元数组合表达复合姿态、把法线随截面形状一起生成。延伸阅读路径均为当前仓库内文件类文档docs/pages/RollerCoasterLiftersGeometry.html.md、docs/pages/RollerCoasterLiftersGeometry.html源码examples/jsm/misc/RollerCoaster.js示例页面examples/webxr_vr_rollercoaster.html截图examples/screenshots/webxr_vr_rollercoaster.jpg同文件兄弟类的文档页docs/pages/RollerCoasterGeometry.html.md【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门