1.3 渲染循环与帧率控制


1.3 渲染循环与帧率控制

本节摘要:渲染循环是帧流水线的节拍器,由浏览器刷新信号驱动而非 while 死循环。本节讲清 runRenderLoop 的驱动机制、帧率与帧耗时的读数方法、掉帧的三种面孔与各自的归因方向,并给出按帧时间做自适应降质的常用手法。本节结束时,你能给自己的应用装上一块实时秒表。

先看一个现象

两个新人同写渲染循环。A 用 while(true) 包住场景渲染,页面打开直接冻死;B 用引擎的 runRenderLoop,画面流畅。A 的直觉没错——确实要"不停地画",错在谁来喊停。渲染不是画得越快越好,而是要跟着显示器的节拍画:显示器每秒刷新 60 次左右,你在两次刷新之间多画的那几帧,用户根本看不见,只换来风扇起飞。

所以引擎的循环是"被驱动"的。浏览器在每次刷新前发出一个信号,引擎收到信号才执行一次完整渲染。下面的时序图就是这个约定的展开:

时序里藏着三个实用推论。其一,页面切到后台时浏览器停发信号,循环自动休眠,功耗随之下降——不需要你手动暂停。其二,帧与帧的间隔由显示器决定,不由你的代码决定,代码只能决定"这一帧的活儿干不干得完"。其三,一帧超时不会丢帧,而是"顶替"下一次刷新位,用户看到的实际帧率立刻减半——这正是掉帧如此显眼的原因。

给循环装上秒表

谈帧率必须先有读数。引擎自带一组性能计数器,先把它显示出来:

// 每秒采样一次帧率,贴到页面角落或控制台 setInterval(() => { const fps = engine.getFps().toFixed(0); // 每秒帧数 const dt = engine.getDeltaTime(); // 上一帧耗时毫秒数 console.log(`帧率 ${fps},上一帧耗时 ${dt.toFixed(1)} 毫秒`); }, 1000);

帧率告诉你用户体感,帧耗时告诉你引擎内情,两个数要一起看。60 帧对应约 16.7 毫秒的预算,超过预算的那一帧,就是流水线上某个站点拖了后腿。把预算画成条形图,超标一眼可见:

图 1-2 帧预算条形图:不同站点组合的耗时分布

图 1-2 帧预算条形图:不同站点组合的耗时分布

掉帧的三种面孔与归因

第一种,CPU 超支。帧耗时曲线整体抬升,浏览器任务管理器里 GPU 并不忙。归因方向是流水线前六站:网格太多、每个网格单独成绘制调用、每帧新建大量对象。第二种,GPU 超支。CPU 有余量但帧率卡在某个值,降低画布分辨率后帧率明显回升——这是 GPU 着色超支的指纹。第三种,尖刺。平时 60 帧,偶发单帧几百毫秒,多半是纹理上传、着色器首次编译或垃圾回收,解法是把资源预热挪到加载阶段。

归因之后是处置。自适应降质是最常用的一招:按帧耗时动态调分辨率系数,卡了就降、松了就升:

// 一个极简的自适应分辨率:帧耗时连续超标则降档,连续富余则升档 let ratio = 1.0; // 硬件缩放系数,1 为原始分辨率 let slowStreak = 0; // 连续超帧计数 scene.onAfterRenderObservable.add(() => { const dt = engine.getDeltaTime(); if (dt > 20) { slowStreak++; if (slowStreak > 30 && ratio > 0.6) { // 连续 30 帧超标才动手,避免抖动 ratio = Math.max(0.6, ratio - 0.1); engine.setHardwareScalingLevel(1 / ratio); // 数值越大分辨率越低 slowStreak = 0; } } else { slowStreak = 0; } });

⚠️ 常见坑:在循环回调里做重型同步操作(大循环、同步网络请求)等于把整条流水线按住不放。重型任务挪到加载期或工作者线程,回调里只做轻量状态更新。

一次真实的掉帧排查

把三种面孔串成完整流程,走一遍真实案例。背景:一个虚拟展馆项目,开发机上稳定 60 帧,投到展会一体机后掉到 22 帧,现场只有十分钟。操作分三步。第一步读数:帧率 22、帧耗时约 45 毫秒,CPU 与 GPU 都有份但 CPU 更重。第二步归因:把镜头对准最密的展台,帧耗时几乎不变,说明不是可见集膨胀;把分辨率降到七成,帧率只回到 28,GPU 不是主犯。第三步查回调:翻代码发现每帧给 40 块展牌各做一次悬停拾取、还顺手克隆了一份材质——两处都塞在渲染回调里。结果:材质克隆挪到加载期、悬停拾取关闭,帧耗时回落到 14 毫秒,帧率稳定 60。解读:三步正好对应本节三块内容——秒表读数、面孔归因、回调减负;变式是若降分辨率后帧率显著回升,主犯在 GPU,处置走 5.1 节的像素压缩路径。两类处方完全不同,这就是"先归因再动手"的价值。

帧率目标怎么定

帧预算由目标帧率倒数而来,目标不是越高越好,而是按设备与场景用途定档:

目标帧率 帧预算 适用场景 主要代价
30 帧 约 33 毫秒 展厅一体机、长时低功耗运行 快速转镜发糊,体感一般
60 帧 约 16.7 毫秒 多数交互场景的默认目标 中端手机需节制材质与后期
90 帧以上 11 毫秒以内 头显渲染(第6章 WebXR 的硬门槛) 每站预算近乎腰斩

还有两个细节常被漏掉。一是高刷屏幕:120 赫兹的显示器会把循环也拉到 120 次,帧预算直接砍到 8.3 毫秒,低配设备反而更卡——必要时给引擎设帧率上限,主动放弃用不满的高刷。二是读数要带样本量:单帧耗时没有意义,至少取几百帧的中位数与最差值一起看,偶发尖刺藏在最差值里。

小结

  • 循环由浏览器刷新信号驱动:写 while 死循环必冻页面,节拍不归你管
  • 帧率与帧耗时一起读:帧率是体感,帧耗时指向超支站点
  • 掉帧三面孔:CPU 超支、GPU 超支、偶发尖刺,指纹与归因方向各不同
  • 超帧不丢帧而是顶位:所以帧率常见 60 直落 30 的减半式下跌
  • 自适应降质以帧耗时为依据:连续超标才降档,带迟滞防抖动

第1章到此收束:你已经能通电、看图、掐表。下一章开始往流水线里放东西——先从被遍历的主体"场景图"讲起,看看一棵节点树如何撑起整个世界。


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