6.1 传动轴:渲染循环与时钟


文档摘要

6.1 传动轴:渲染循环与时钟 本节摘要:渲染循环是整条产线的心跳,Clock 是挂在心跳上的节拍器。本节讲清增量时间 dt 与累计时间 t 的分工,把"帧率无关动画"从一个乘法讲成一种纪律,并给出循环体内的工序顺序表——顺序错了,交互动效全部慢半拍。 心跳的节拍问题 把第 1 章首件样板的循环拿到两台机器上跑:144 帧的电竞屏上立方体转得飞快,30 帧的办公机上慢了将近五倍——代码一模一样,转速天差地别。原因藏在 += 0.01 这个写法里:它是"每帧转 0.01 弧度",而每帧的真实时长因设备而异。修正只需一个乘法:把"每帧转速"换成"每秒转速"乘以本帧时长(dt)。dt 从哪来?Clock。

6.1 传动轴:渲染循环与时钟

本节摘要:渲染循环是整条产线的心跳,Clock 是挂在心跳上的节拍器。本节讲清增量时间 dt 与累计时间 t 的分工,把"帧率无关动画"从一个乘法讲成一种纪律,并给出循环体内的工序顺序表——顺序错了,交互动效全部慢半拍。

心跳的节拍问题

把第 1 章首件样板的循环拿到两台机器上跑:144 帧的电竞屏上立方体转得飞快,30 帧的办公机上慢了将近五倍——代码一模一样,转速天差地别。原因藏在 += 0.01 这个写法里:它是"每帧转 0.01 弧度",而每帧的真实时长因设备而异。修正只需一个乘法:把"每帧转速"换成"每秒转速"乘以本帧时长(dt)。dt 从哪来?Clock。它内部基于高精度计时器,getDelta() 每次调用返回距上次的秒数,elapsedTime 累计总时长——前者驱动逐帧动画,后者驱动周期性效果(正弦起伏、闪烁节拍)。

图:一帧内的工序节拍

图:一帧内的工序节拍

动手:校准心跳并修一处隐患

背景:沙盘上要加三个动效:传送带匀速滚动、货箱正弦浮沉、警示灯按秒闪烁。要求在 30 帧与 144 帧的机器上表现一致,且用户切后台再切回不发生"瞬移"。

操作:全部动效接入 dt 与 elapsed,切回瞬移用 dt 钳制处理。

const clock = new THREE.Clock(); stage.loop((dt, elapsed) => { const step = Math.min(dt, 0.05); // 钳制:切后台回来 dt 可能是几秒,限到 50 毫秒 // 1. 传送带匀速滚动:每秒 0.8 弧度,与帧率无关 belt.rotation.z += step * 0.8; // 2. 货箱正弦浮沉:用累计时间 elapsed,周期 2 秒、幅度 0.15 crate.position.y = baseY + Math.sin(elapsed * Math.PI) * 0.15; // 3. 警示灯按秒闪烁:对 elapsed 取 2 取余,大于 1 时亮 warnLight.visible = (elapsed % 2) > 1; controls.update(); stage.renderer.render(stage.scene, stage.camera); });

结果:三个动效在任何设备上节奏一致;把页面切到后台十秒再切回,场景只是"接着走",不再跳变。删掉 Math.min 那行钳制再试一次切后台,货箱与传送带瞬间"闪现"——dt 暴涨的现场教学。

解读:这段小代码里有三条真知识。其一,dt 与 elapsed 的分工:凡"往前走多少"的动效用 dt,凡"周期往复"的动效用 elapsed——传送带是前者,浮沉与闪烁是后者,用反了要么速度漂移要么相位错乱。其二,钳制 dt 是工业惯例:标签页切回、断点调试、长 GC 都会制造超长帧,钳到 50 毫秒意味着最坏情况也只是"慢一瞬"而不是"瞬移一截"。其三,visible 开关比改发光强度便宜:闪烁这类纯开关效果直接切可见性,不进光照计算,这个细节到第 8 章帧预算时会再次体现。

变式:同一动效在不同设备上"观感一致"与"时长一致"是两回事——转盘类动效按 dt 走(观感一致),进度条类动效按 elapsed 归一化走(时长一致,从 0 到 1 固定耗时)。另一个进阶方向是循环内的条件渲染:静展台无交互且无动画时跳过 render(第 8 章的"按需渲染"),心跳还在、功耗归零——传动轴学会偷懒,也是本事。

排错台:循环三事故的会诊

事故一,"切后台回来场景瞬移"。超长 dt 未经钳制,一步跨过十秒的位移全部画进一帧;钳制之外,周期类效果改用绝对时间驱动(elapsed 取模)可以从根上免疫——绝对时间不关心中间跳过了多少帧,回来时相位自然接上。事故二,"每隔几秒莫名卡一下"。多数是垃圾回收与频繁建对象的合谋:循环体内每帧 new 一个向量或颜色对象,短命对象攒多了 GC 就要停下来大扫除。处置是把循环内的临时对象全部提出去复用,Three.js 的许多方法支持传入目标对象接收结果,这是官方留的复用口。事故三,"帧率一直上不去但没有明显大户"。逐项排除:先把像素比压到 1 试帧率(排除像素税),再关后期链(排除整屏税),再砍灯光(排除着色税),最后看绘制调用——按"单价乘数量"的顺序排查,多数冤案十几分钟就能定位。

循环内的分工与整洁

心跳健壮之后,循环体内的代码组织就是第二门手艺。第一条原则是"循环里只留调度":把传送带的滚动、货箱的浮沉、灯光的呼吸各自封装成接受 dt 的更新函数,循环体只剩一行清单式调用——动效从三个涨到三十个时,循环体依然可读。第二条是"能并行的别串行依赖":各动效之间不共享临时变量,谁也不依赖谁的本帧顺序,这样增删一个动效不用回归测试全场景。第三条是"可暂停":给动效注册表加一个总开关,页面不可见或弹窗打开时批量暂停,回到前台再统一恢复——实现在一处,收益遍及全站。循环是全册代码里唯一"每帧都在跑"的地方,它的整洁度直接决定后期维护的心智负担,这三条原则值得在第一个项目就养成。

帧率统计与"慢"的两种口径

排查循环问题前,先统一度量口径:帧率统计要用"滑动窗口平均"而不是单帧瞬时值——单帧偶尔 40 毫秒只是毛刺,连续半秒低于 30 帧才是事故。实现只需一个帧数累加器每秒清零。口径之外还要分清两种"慢":渲染慢(每帧的绘制耗时超预算)与逻辑慢(更新代码本身昂贵)。区分办法是二分注释:把循环里的更新逻辑全注释只留渲染,帧率回升就是逻辑贵,不动则是渲染贵——一行注释胜过半小时瞎猜。另外注意标签页的定时器节流:浏览器会把后台标签页的循环降到极低频,任何"依赖前台节奏"的逻辑(倒计时、实时对账)都不能只挂在渲染循环上,需要独立的时间源兜底。这类"环境行为"不算故障,但不知道它的人会在排查上耗掉一晚。

本节要点回顾

  • dt 是帧率无关的钥匙:速度按"每秒"声明、逐帧乘 dt,任何设备转速一致;
  • dt 与 elapsed 分工:推进用增量、周期用累计,用反必漂移;
  • 钳制超长帧:切后台与卡顿制造的暴涨 dt 截到 50 毫秒,杜绝瞬移;
  • 先摆后拍:更新逻辑在前、渲染在后,顺序颠倒交互动效全慢一帧;
  • 开关比数值便宜:纯闪烁切 visible,不进光照计算,白省一笔着色开销。

心跳已校准。下一节接两条传动链:官方动画系统管模型自带的骨骼剪辑,手写缓动管自制属性动效——分工表配齐,动效不再打架。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U