
上周在给模拟项目X的线上虚拟展厅做收尾时运营突然提了个需求展厅中央的展台周围要加一圈一直在滚动的大字标语就像老式门店上头那种跑马灯而且得在3D场景里能看、能转、能截图发朋友圈。我当时的反应是这不是有手就行网页跑马灯谁不会一行CSS的事情。结果在ivx的3D世界编辑器里一顿操作后我发现事情完全不是这样静态贴上去的文字不会动给文字加位移动画又会跑来跑去不连贯放了一堆文字组件后帧率肉眼可见地掉。折腾了两天我把两条能稳定跑起来、且能出片效果的路子都摸了一遍也踩了五六个结结实实的坑。这篇就专门聊这件事在ivx的3D世界里做跑马灯有哪些原理要先想清楚两条实现路线各自怎么做以及那些文档里不会写的坑与调参细节。如果你正在做3D可视化、虚拟展厅、发布会场景或者以后会碰ivx的3D模块这篇值得花几分钟看完。1. 先想清楚3D世界里的跑马灯到底在“跑”什么1.1 我的需求排期与最终验收标准运营口里的“跑马灯”其实是个非常模糊的概念。接到需求后我列了一个确认清单文字出现在哪个位置、内容是静态还是动态、相机能不能自由转动、文案更换频率高不高。当时的需求是展厅中央圆形展台外围一圈环绕宣传标语相机可以绕着展台缓慢旋转标语要持续、匀速、无缝地在观众眼前滚过去。我给自己定的验收标准很直白三分钟循环里不能出现一次肉眼可见的“闪跳”绕到展台背面时字还能认出来整体帧率不能因为这一圈字掉到30fps以下。说实话这个标准在网页里很容易但在3D场景里并不容易因为它的本质不是“让文字动起来”而是“让文字动起来之后在三维空间里依然保持可读、连续、稳定”。“可读”意味着文字朝向、光照、清晰度都要管“连续”意味着循环不能有缝隙“稳定”意味着不能因为逻辑循环把整个场景拖垮。三个维度互相牵制所以选哪条实现路线其实是选你愿意在哪一个维度上妥协。1.2 两条路线的本质差别我和同事当时先抛出了两种方案方案A我把它叫“纹理方案”在3D场景里放一块平面或圆柱面把跑马灯画在Canvas上再把Canvas作为贴图贴在平面上动态更新贴图内容。方案B叫“物体动画方案”直接在3D场景里放一排或一圈立体文字组件通过逻辑让它们整体移动或者旋转。为了讲给运营听我打过一个比方纹理方案是“大厅里的电视屏屏幕自己滚动播放”物体动画方案是“一队人举着灯牌绕着场子走灯牌本身是立体的”。电视屏的好处是内容变起来容易缺点是它永远是块屏拍出来缺立体感举牌队伍的好处是真实、有光影、有空间感坏处是组织起来更麻烦而且人一多物体一多就容易乱。维度Canvas纹理方案物体动画方案实现成本低一个平面加绘制逻辑中需要建多个文字并管理位置动态换内容方便改绘制文本即可麻烦需要重建/修改文字内容无缝循环容易把绘制起点复位即可需要首尾相接或父节点整体旋转立体感与光影弱像贴纸强文字有真实厚度和光照自由视角下的可读性一般贴近看容易糊好任何角度都立得住性能开销取决于Canvas刷新面积取决于文字组件数量和精度看完表格基本能判断如果做的是“横幅滚动字幕”纹理方案最省心如果做的是“展厅环绕标语”那多半得走物体动画方案。我一开始先做了纹理方案后来又被运营逼着换成了物体动画方案两条路的坑都踩完了下面分别说。2. Canvas纹理方案让跑马灯“印”在3D平面上2.1 为什么先试这条动态内容最友好先说结论如果你想实现的效果是“一行字在一个横条里从左往右滚”并且内容经常会变纹理方案绝对是最快能跑通的一条路。在ivx的3D场景里平面模型表面都可以贴材质材质可以用图片也可以用画布生成的纹理。我尝试的方式是先在场景里放一个平面作为“屏幕”再准备一块画布把文字、颜色、滚动逻辑都在画布上解决最后把画布每一帧的输出同步到平面材质上。这样做有个天然好处跑马灯的一切逻辑都被锁在2D画布里。文字内容、字体大小、滚动速度、背景颜色全部可以通过修改绘制代码调整不需要去动3D场景里的任何物体。对后面要接数据源的项目来说特别方便只需要把数据源字段拼进字符串里即可3D部分完全不用改。2.2 在ivx里把画布变成3D贴图我在ivx里具体操作大概是这样的不同版本入口会有差异但思路通用在2D设计区放一个Canvas组件尺寸先设成2048 x 256。这个尺寸不是随便给的跑马灯横幅如果画布太小贴到3D平面后一定会发糊原因下面踩坑章节会说。在Canvas上画背景色和文字文字内容先写死几段测试文案。进入3D场景编辑器新建一个平面体调整宽高比例和画布一致例如宽度20、高度2.5。把画布输出设置为该平面的漫反射贴图并在材质设置里关闭平铺重复、关闭不必要的抗锯齿。在逻辑编辑器里把画布的绘制函数绑定到持续触发的循环上。这里唯一的难点是“把画布作为3D场景中的纹理”这个映射关系。ivx里材质节点一般支持直接引用2D组件生成的图像资源找到入口后绑定即可。问题在于绑定成功后画布并不会自动逐帧更新成“动态纹理”需要在逻辑里不断触发重新生成图像。2.3 一段能让文字滚起来的绘制逻辑下面这段代码是我当时写在绘制逻辑里的核心部分以JavaScript为例const CANVAS_W 2048; const CANVAS_H 256; const ctx canvas.getContext(2d); // 用 offset 控制文字整体滚动偏移量 let offset 0; const TILE_W 600; // 单段文字占用的宽度 const SPEED 3; // 每帧移动像素数 function drawMarquee() { ctx.clearRect(0, 0, CANVAS_W, CANVAS_H); // 背景 ctx.fillStyle rgba(0,0,0,0.85); ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); ctx.font bold 80px sans-serif; ctx.fillStyle #ffcc00; // 画多段文字保证画布被塞满 for (let i 0; i 5; i) { const x offset i * TILE_W; ctx.fillText(活动主视觉 · 新品体验区, x, 170); } // 滚动核心offset 持续向左移动 offset - SPEED; // 关键一旦某段文字完全移出画布左边界 // 就把它“加回来”到右侧制造无缝衔接 if (offset -TILE_W) { offset TILE_W; } // 把画布内容同步到3D平面材质上 updateTextureFromCanvas(canvas); // 控制下一帧 requestAnimationFrame(drawMarquee); } drawMarquee();这里最关键的是那个if (offset -TILE_W) offset TILE_W。很多第一次写的人会直接让offset无限变小结果跑一会儿就发现整块画布被清空了。其实跑马灯的本质就是“循环播放同一段内容”所以偏移量永远只需要在[-TILE_W, 0]这个区间里循环超过边界就加回一个周期视觉上完全看不出来。要注意requestAnimationFrame在桌面浏览器里是每帧执行但在低代码平台里如果逻辑循环本身也有帧回调容易重复触发。我在ivx里做了节流把绘制频率限制在每秒30帧左右跑马灯上屏后肉眼基本分不出30fps和60fps的区别性能却能省一半。2.4 实际跑起来之后的三个问题第一次跑通时我很兴奋但拿到手机上一看三个问题立刻冒出来。第一个是糊。2048宽的画布在PC预览里还行但到了手机屏幕上3D平面缩小后画布内容被做了缩采样文字边缘发虚。解决办法是把画布宽度提到4096同时把3D平面的尺寸调小让贴图像素密度足够高。代价是纹理内存变大所以我最后采用了“文字区域单独画高清、背景用纯色”的方案避免整张画布堆满无意义像素。第二个是“躺平感”。平面默认是竖直的还行但一旦平面放在地面上比如地面滚动字幕相机视角压低后文字几乎贴在一条线上完全没法读。我的处理办法是把平面朝相机方向上仰45度并且在材质里加一点自发光保证斜着看也有辨识度。第三个是“橡皮擦”。因为每帧都在clearRect整张画布再重绘快速滚动时文字会出现残影闪烁。把clearRect改成画一个不透明的背景矩形能显著减少这种闪烁。道理很简单透明清除和覆盖绘制交错的瞬间底层纹理没有被完全刷新干净看起来就像擦了又没擦干净。这三个问题让纹理方案最后没有成为我项目里的主力但它依然是“直线横幅跑马灯”最快的实现方式。3. 文字物体真正绕场跑无缝循环的关键在“容器旋转”3.1 什么时候必须放弃纹理贴图我这边放弃纹理方案是被一句话点醒的运营在验收时说“这圈字怎么看起来像个贴纸贴在展台上我想要那种立体的、会发光的、真的在绕场跑的灯牌效果”。这句话翻译成技术语言就是文字必须是三维空间中的实体而不是一张贴图。于是转向物体动画方案。在ivx的3D场景里文字可以作为一个独立物体存在它有位置、旋转、缩放也可以被光照影响。把多个文字沿圆形路径摆放好后只要让它们围绕中心轴匀速转动就能实现非常自然的“绕场跑马灯”。这才是“3D世界里跑马灯”应该有的样子而不是把网页里的CSS marquee硬搬到3D里。3.2 搭建一圈立体文字的核心步骤我当时的做法分四步在3D场景里先放一个空节点作为整个跑马灯组的“中心锚点”。后续所有文字都挂在它下面方便统一旋转。创建立体文字组件内容写成“新品体验区”之类的一段话。字体选比较粗的黑体太小太细的字在立体化之后根本认不出来。复制出8份文字沿半径为R的圆周均匀放置。角度按360/845度一份位置用三角函数算x R * cos(angle)z R * sin(angle)。高度按展台高度定我用的y 2.2。每个文字做一次Y轴旋转让文字正面朝向圆心或者朝向观众。展台场景里我做成面朝外观众站在展台外看。位置和角度初看很像数学课实际跑起来就发现只要锚点坐标是场景中心文字分布的均匀度直接决定了跑马灯有没有“圈”的感觉。如果角度分配不均转动时会有一眼就能看出的尴尬疏密。3.3 让整圈文字平滑滚动旋转父节点而不是每个文字这是整个方案里最值得记住的一个经验不要给每个文字单独做位移循环那样要做N份动画还要处理它们之间的碰撞和复位正确的做法是只旋转它们的父节点。我操作时是这么做的在ivx逻辑编辑器里新增一个持续旋转动作作用对象选“中心锚点”绕Y轴方向角速度设为每秒18度。这样所有文字作为一个整体绕场转圈。每个文字相对父节点的相对位置永远不变但在世界坐标里它确实在绕场移动。用父节点旋转至少有三个好处动画只有一个性能开销极低文字之间永远不会“撞车”因为它们相对静止无缝循环天然成立转360度一圈后视觉完全回到初始状态。相比纹理方案需要在2D里考虑“复位移出屏幕的段落”物体动画方案根本不需要复位。这是3D环形跑马灯最舒服的地方。3.4 循环为什么不卡壳360度角周期性的妙用直线跑马灯要处理“终点回到起点”的跳变环形跑马灯不需要因为圆周运动的本质是角度循环。0度和360度是同一个位置所以只要让角度持续增加不存在“复位”那一刻。这里有个新手容易踩的坑如果动画是用“从0转到360度播放完再回到0重新播”实现的那一瞬间文字会倒转看起来就是闪跳。正确做法是让角度一直累加比如从0累加到720、1080不做归零如果平台会处理数值溢出再用取余运算angle % 360但不要主动把角度重置回0。原因很简单角度取余之后依然连续而单次动画的播放/重放机制会先“重置回起点”再开始帧与帧之间会差一个不连续位移。当时我在ivx里排查了很久才意识到问题不在动画速度而在于循环方式。把“播一遍360度动画并循环”改成“持续累加角度值”后闪跳直接消失。3.5 调试中关于速度和数量的建议绕场跑马灯的“观感”跟线速度有关线速度等于角速度乘半径。如果半径很大角速度很小也会显得跑得很快半径小反而要调大角速度。我当时的经验值半径8米左右角速度每秒15到20度比较合适也就是一圈18到24秒。太快了像“消防警报跑马灯”太慢了又看不出“在滚动”。文字数量上8到12组比较合适。少于6组会显得稀稀拉拉多于16组会遮挡视线而且立体文字组件数量翻倍后加载时间和内存都会上去。每组文字内容尽量短8到12个汉字以内最好如果一句话太长拆成两组并排间距拉开一点转动时反而更有节奏感。还有一个笨办法值得分享因为文字需要“面朝圆心”或“面朝外”旋转方向反了会让字背对观众。先把文字放在(1,0,0)位置看它的默认朝向再套角度公式。不要凭感觉猜朝向用最小实验验证一次后面复制就不会乱。4. 踩坑记录从“能跑”到“跑得好看”的五个坑4.1 坑一循环衔接处出现明显顿挫第一个坑是循环衔接处的顿挫。现象很好识别文字转到某个固定角度时会像皮筋一样往回弹一下然后继续走。我当时第一反应是动画速度不够顺滑调了好几轮速度都不对后来才发现问题在循环方式——如果用“转满360度再重新播”的动画播放器在进入下一轮前会把角度先重置回起点这一瞬间文字就发生了肉眼可见的跳变。修正办法是让角度全程持续累加不做0度重置如果担心数值溢出就用取余运算保持连续性比如angle (angle 0.5) % 360但绝不能主动把角度归零。这个坑让我意识到在做任何“循环动效”时最先要找的就是这个循环的“边界变量”是什么以及这个变量在边界处会不会产生不连续。4.2 坑二文字在动态视角下细碎闪烁第二个坑是文字细碎闪烁。相机绕到跑马灯侧面时文字边缘出现锯齿状的闪烁细笔画尤其明显。一开始我以为是3D文字组件精度不够换更细的字形反而更糟糕后来才明白真凶有两个相机近裁剪面离得太近导致深度精度不足文字之间间距太小面与面重叠后产生了z-fighting。解决办法是把相机近裁剪面调到0.1以上远裁剪面适当缩小文字组件关闭动态阴影字体用加粗样式多个文字靠得近时给它们之间留出2到3个单位的间距。如果想确认是不是深度精度问题最简单的方法是让相机固定一个角度转圈看如果闪烁只在某个特定距离出现那基本就是裁剪面或z-fighting的问题。4.3 坑三离远看字太小离近看字太大第三个坑是比例失控。展台中心的标语站在展厅门口看是“一条线”走近看又像巨幅广告一样冲出屏幕。原因是文字尺寸与场景空间尺寸没有建立线性关系字高和展台半径的比例没调好。我调了几轮后得出结论把文字高度设为展台半径的1/4到1/3比较稳。比如半径8米文字高度2米左右。需要注意的是每个文字的字号必须统一整圈文字组通过父节点做整体缩放微调而不是逐个改字号否则位置和朝向会全乱套。跑马灯最重要的是“整组一致性”单独微调某个文字会非常破坏观感。4.4 坑四材质没做自发光灯光一变字就看不清第四个坑是材质受光问题。换了一套夜间风格灯光后金黄色文字变成了棕色几乎融进背景里。原因是跑马灯文字材质用的是基本漫反射颜色完全取决于场景光照光照一变字就变。跑马灯这类“要持续吸引眼球”的物体材质最好直接走自发光通道不受场景灯光影响。我在ivx里把所有跑马灯文字材质改成了自发光/Emissive颜色固定在暖黄色文字在任何灯光风格下都能保持基础亮度。需要一点“光晕感”的话可以在文字后面再加一圈光带平面而不是依赖后处理特效性能更好。4.5 坑五逻辑循环嵌套导致CPU吃满第五个坑是CPU占用。跑马灯和另一组粒子特效同时跑页面CPU占用直接飙升手机开始发烫。早期我把“每一帧更新位置”写在一个每帧都触发的自定义事件里事件又同时执行了位置赋值、纹理更新、UI绑定相当于每帧做了一堆不必要的工作。解决方法是做职责拆分旋转交给父节点动画纹理和UI更新放到低频定时器里每秒5到10次足够能用平台内置连续动画动作解决的绝不在事件循环里手动改坐标。这个坑让我记住了“动画和数据更新要分离”的原则跑马灯看起来在动但其实我们只需要让“动画属性”持续变化而不是每帧执行一整串业务逻辑。5. 参数调优与进阶玩法跑马灯也能“有呼吸感”5.1 一组可以直接抄的参数表我把自己最终调试下来的参数整理成表按场景半径8米、展台高2.2米来算参数推荐值说明文字数量8~12组太少稀疏太多遮挡每组文字字数6~10个汉字太长时转动中读不过来文字高度1.8~2.4米约为半径的1/4文字离地高度2.0~2.5米与视线大致平齐角速度15~20度/秒一圈约18~24秒材质自发光暖黄色不受场景灯光影响文字间距圆周均分不要手摆用公式算这些数值是调试出来的“甜点区间”不是必须精确到小数。换场景时我建议先按这个表搭一版再根据视角动线微调。如果目标是“拍照好看”角速度可以降到8到10度每秒跑马灯变成“缓慢漂移”高级感更强。5.2 进阶双圈反向跑马灯与事件联动跑马灯不止能绕场一圈。我在模拟项目X的最终版本里做了两层外圈文字顺时针缓慢转内圈光带逆时针匀速绕。光带是自带发光材质的细环不产生文字负载视觉上却一下子丰富很多。做两层时要注意两个锚点不能重名旋转方向务必一个顺时针一个逆时针并且角速度错开比如外圈17度/秒、内圈25度/秒。如果角速度接近会出现“相对静止”的尴尬看起来像两个圈锁在一起跑马灯的“动态感”就没了。事件联动也可以加上。比如观众点击展品后跑马灯先在2秒内减速到零再反向加速形成“反转提示”的交互。关键点在于减速不能通过突然修改角速度实现那样会跳变应该把角速度也做成一个可动画化的变量在逻辑里用“线性插值”从当前值过渡到目标值。ivx的动画节点一般能处理属性插值把角速度暴露成属性再做一段“从17到0”的动画即可。5.3 性能护栏别让跑马灯吃掉整个场景的帧率多个跑马灯同时开最怕的是逻辑事件每帧都在算。我给自己定了几条护栏动画尽量收敛到“一个父节点加一个持续动画”避免每个子节点各写一套逻辑。立体文字组件太多时考虑用图片贴图冒充近景用立体字远景用贴图相当于做了个LOD分级。多个圆形跑道时共用同一个材质实例减少渲染状态切换。手机端打开时纹理画布宽度限制在1024以内并用降频重绘后台运行时暂停绘制逻辑等用户重新看到场景再继续。实在卡顿就把“每帧同步”改成“定时同步”比如每秒同步30次跑马灯一样顺滑CPU立刻降下来。6. 最终项目里我留下的方案经验收尾最后交代一下模拟项目X最终交付时留了什么中央展台外圈用的是“物体动画方案”的立体文字跑马灯8组文字自发光暖黄色绕Y轴顺时针旋转入口处一条横向“欢迎语”用的是Canvas纹理方案方便每周换文案。两条路各司其职。有几条经验我可以明确分享不要一听“跑马灯”就想到CSS先问是在屏幕里跑还是在3D空间里跑这完全是两个世界3D里做循环动效优先找“角度/位置的周期性”让循环天然成立别靠手动复位文字类的可读性不在文字本身而在材质、光照、视线夹角这三者的组合用父节点统一控制一组的动画永远比单个物体各动各的省心。如果以后你还想在ivx的3D世界里做更花的效果比如跑马灯加霓虹灯管、跑马灯加粒子拖尾前面的父节点旋转思路都能直接复用只不过把材质换成发光管、粒子系统挂在锚点上跟随旋转而已。我自己的体会是3D世界里的动效并不难难的是先想清楚“谁在动、谁跟着动、循环的周期是什么”想明白这三个问题剩下的都是参数微调。