尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PixiJS 渲染循环(Render Loop)深入解析:从 Ticker 到 GPU 绘制的每一帧

PixiJS 渲染循环(Render Loop)深入解析:从 Ticker 到 GPU 绘制的每一帧 PixiJS 渲染循环Render Loop深入解析从 Ticker 到 GPU 绘制的每一帧【免费下载链接】pixijsThe HTML5 Creation Engine: Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.项目地址: https://gitcode.com/gh_mirrors/pi/pixijs导读渲染循环Render Loop是 PixiJS 引擎的心脏它负责每帧驱动用户逻辑、更新场景图Scene Graph变换、执行视口剔除Culling并最终把显示对象批量提交给 WebGL / WebGPU 完成绘制。本文以仓库内 render-loop.md 为核心骨架结合 Ticker、TickerPlugin、Culler 等源码实现完整还原一帧从开始到上屏的内部时序并给出暂停/限帧/手动渲染等实战控制手段。读完本文你将理解app.ticker、deltaTime、onRender、cullable这些 API 背后各自的执行时机与优先级从而写出帧率独立、结构清晰的 PixiJS 应用。一、总体概览一帧内发生的三件事与传统 Web 开发中仅在事件触发时才重绘的模式不同PixiJS 采用持续动画循环continuous animation loop只要应用运行且 Ticker 处于激活状态每一帧都会依次完成下面三个阶段Ticker 回调执行用户逻辑——运行通过ticker.add()/app.ticker.add()注册的所有监听器更新游戏状态与动画参数场景图更新变换与剔除——从根节点app.stage向下遍历重算每个对象的局部/世界变换并对屏幕外的可剔除对象关闭渲染渲染发生GPU 绘制——渲染器遍历显示列表、批量打包绘制指令batcher、上传几何体/纹理/Uniform最后发出 GPU 绘制命令。这个循环以requestAnimationFrame为节拍器反复执行直到调用app.ticker.stop()或应用销毁为止。二、Step 1Ticker——渲染循环的驱动器2.1 三个时间单位的正确用法Ticker 在每帧回调中通过参数暴露三种时间信息源码 Ticker.ts 中的类型注释与文档给出了明确分工属性类型语义典型用途ticker.deltaTime无量纲标量缩放后的帧增量60 FPS 下约为1.0帧率无关动画sprite.rotation 0.1 * deltaTimeticker.deltaMS毫秒经过minFPS上限截断并按speed缩放后的毫秒数基于真实时间的计算如progress deltaMS / durationticker.elapsedMS毫秒未截断、未缩放的原始帧间隔性能测量、原始帧时统计三者的计算关系在 update() 中一目了然先保存未截断的elapsedMS再用_maxElapsedMS由minFPS反推截断、乘以speed最后换算成deltaMS与deltaTime deltaMS * Ticker.targetFPMS。默认Ticker.targetFPMS 0.06对应 60 FPS见 Ticker.ts因此1 / 0.06 ≈ 16.67ms即目标帧时长。2.2 监听器的注册、优先级与一次性回调Ticker.add(fn, context?, priority?)会把回调挂进一个按优先级降序排列的链表_addListener中listener.priority current.priority时插到前面见 Ticker.ts。仓库定义了五个内置优先级常量const.ts常量值用途UPDATE_PRIORITY.INTERACTION50EventSystem交互事件最先执行UPDATE_PRIORITY.HIGH25AnimatedSprite等高层更新UPDATE_PRIORITY.NORMAL0ticker.add()默认优先级UPDATE_PRIORITY.LOW-25Application的渲染渲染放最后UPDATE_PRIORITY.UTILITY-50PrepareBase预备系统最后执行这一点意义重大渲染是以UPDATE_PRIORITY.LOW注册进 Ticker 的见 TickerPlugin.ts 的ticker.add(this.render, this, UPDATE_PRIORITY.LOW)——也就是说先更新逻辑、后渲染画面的顺序正是由优先级保证的。此外ticker.addOnce()注册的回调只在下一帧执行一次TickerListener的once标志适合做延迟初始化ticker.remove()会按fn context匹配并删除同名多次添加会全部移除当链表清空时自动cancelAnimationFrame停止空转。基础用法示例// 帧率无关的旋转动画 app.ticker.add((ticker) { bunny.rotation ticker.deltaTime * 0.1; }); // 基于毫秒的进度动画 app.ticker.add((ticker) { const progress Math.min(1, ticker.deltaMS / 2000); sprite.alpha progress; }); // 指定优先级物理更新先于普通逻辑 app.ticker.add( (ticker) physics.update(ticker.deltaTime), undefined, UPDATE_PRIORITY.HIGH ); // 一次性回调下一帧执行一次 app.ticker.addOnce(() console.log(only next frame));2.3 帧请求的按需调度Ticker并不是无脑每帧都调用requestAnimationFrame。源码 Ticker.ts 中的_requestIfNeeded与_startIfPossible表明只有当 Ticker 已启动started且链表非空存在监听器时才会申请下一帧停止或清空监听器后会调用_cancelIfNeeded取消已申请的帧。这也是Ticker.shared供AnimatedSprite、VideoSource等共享autoStart true与Ticker.system供PrepareBase核心时序使用两个全局实例存在的意义——它们各自管理独立的帧调度。三、Step 2场景图更新——变换、onRender 与剔除3.1 从根到叶的层次遍历PixiJS 用一棵以app.stage为根的层次化场景图表示所有可见对象详见 scene-graph.mdx。渲染前引擎会遍历这棵树重算世界变换把 position / rotation / scale 从父节点逐级传播到子节点父级的位移、旋转、透明度会按层级累积到子级执行onRender回调对每个显示对象运行按帧逻辑跳过屏外对象当 剔除culling 开启时屏幕外的子树不再参与绘制。需要注意 v8 的变换模型与旧版本的区别Container.updateTransform()在 v8 中已不再是每帧必被调用的钩子见 onRenderMixin.ts 中的注释它现在只是一个带参的 setter 式工具方法Container.ts。想要每帧对某个对象执行逻辑正确姿势是下面的onRender。3.2 onRender挂在对象上的按帧钩子onRender是把逐帧逻辑直接绑定到显示对象自身的替代方案避免为单一对象引入全局 Ticker 回调sprite.onRender () { sprite.rotation 0.01; }; // 移除回调 sprite.onRender null;底层实现onRenderMixin.ts会通过renderGroup?.addOnRender(this)把该容器注册到所属 RenderGroup 的_onRenderContainers列表中渲染时逐帧遍历调用置null时则执行removeOnRender从列表中摘除。从源码结构看onRender的触发与 RenderGroup 的指令生成流程绑定适合做对象级的轻量每帧更新GifSprite等内置对象也借助该机制驱动自身动画见 GifSprite.ts。3.3 视口剔除让屏外对象不再白画对于横版卷轴游戏这类大量对象在屏幕外的场景不剔除意味着 GPU 仍会为不可见像素执行绘制。PixiJS 内置视口剔除启用方式与参数对应 scene-graph.mdx// 对精灵或容器启用剔除 sprite.cullable true; // 子对象不会超出父级边界时跳过递归子剔除以节省开销 container.cullableChildren false; // 提供自定义剔除区域避免每帧重新测量对象包围盒 sprite.cullArea new Rectangle(0, 0, 200, 200);剔除的算法核心在 Culler.cull()递归遍历cullable且measurable的容器用包围盒与视口矩形做相交测试不相交则把culled置为true跳过渲染当cullableChildren为false时不再下探子级。Culler 的单测覆盖了可剔除对象出屏不渲染 / 入屏恢复渲染 / 非 cullable 恒渲染 / cullArea 自定义边界等行为Culler.test.ts。Culler 与渲染循环的衔接点在 CullerPlugin.ts应用安装该插件后render会在渲染前先执行Culler.shared.cull(this.stage, this.renderer.screen, updateTransform)。注意插件选项culler.updateTransform ! true时默认由插件内部处理变换更新只有显式开启updateTransform时才跳过这一步——这是为了避免与手动渲染流程重复更新变换。四、Step 3渲染场景——保留模式与指令提交4.1 render() 的执行管线场景图就绪后渲染器从app.stage出发走完整个绘制流程。AbstractRenderer.render()AbstractRenderer.ts展示了调用链的轮廓入口与参数归一render(options)接受{ container, target, clearColor, clear, transform }形式的渲染选项兼容 v8 之前render(container, renderTexture)的旧签名默认目标与清屏目标默认是this.view.renderTarget清屏颜色默认取自background.colorRgba、clearBeforeRender决定是否每帧清屏可见性短路options.container.visible false时直接返回跳过整个渲染确保 RenderGroup调用container.enableRenderGroup()——渲染器只认 RenderGroup普通容器会被提升为渲染组Runner 事件序列依次触发prerender → renderStart → render → renderEnd → postrender各渲染子系统批处理、蒙版、滤镜等通过这套 Runner 管线协作。4.2 批处理与上传减少绘制调用进入各 RenderPipe 后渲染器会尽量批量打包绘制调用把共享纹理/状态的精灵合并进同一个批次batcher用一次 GPU draw call 画完显著降低drawElements次数上传几何体、纹理与 Uniform脏数据才重新上传未变化的资源直接复用 GPU 端缓冲发出 GPU 命令最终由 WebGL 或 WebGPU 后端执行绘制。所有渲染都是保留模式retained mode对象一旦加入场景图就会持续存在并逐帧重绘除非你显式removeChild/destroy这与每次重绘都重建画面的立即模式形成鲜明对比。4.3 一个值得注意的架构分层从 RenderGroup.ts 的类注释可以读出 v8 的优化思路RenderGroup 负责为根容器及其子树生成指令集InstructionSet并监听子树变化——只有发生变化的容器childrenToUpdate/childrenRenderablesToUpdate才重建或增量更新指令数据未变化的部分直接复用。这意味着每帧遍历场景图在 v8 中并不是无差别的全量重算而是结构化的脏检查 增量更新。想进一步了解可阅读 render-groups.md 与渲染器说明文档。五、完整帧生命周期图把上面三步骤串起来一帧的完整时序如下requestAnimationFrame │ [Ticker._tick()] // 见 src/ticker/Ticker.ts#L273 │ ├─ 计算帧间隔elapsedMS → deltaMS → deltaTime ├─ 按优先级调用用户 Ticker 监听器 ├─ CullerPlugin视口剔除标记屏外对象 │ └─ 剔除前按需更新世界变换 ├─ 遍历场景图 / RenderGroup │ ├─ sprite.onRender遍历到对象时触发 │ ├─ 增量更新世界变换与指令集 │ └─ 跳过 culled 对象 └─ renderer.render(stage) ├─ prerender / renderStart ├─ 批处理几何体、纹理、Uniform ├─ 发出 GPU 绘制命令WebGL / WebGPU └─ renderEnd / postrender六、控制循环暂停、限帧与完全手动渲染6.1 暂停与恢复Ticker 的stop()/start()会走_cancelIfNeeded/_requestIfNeeded真正取消/申请动画帧Ticker.tsapp.ticker.stop(); // 暂停渲染与所有回调 app.ticker.start(); // 恢复循环对应地应用初始化选项autoStart决定循环是否自动启动默认true当autoStart: false时需在合适时机手动app.start()。sharedTicker选项默认false则决定app.ticker是新建专属实例还是复用全局Ticker.shared——多个实例共享同一 Ticker 可保持更新同步但会失去独立控制更新顺序的灵活性相关选项说明见 TickerPlugin.ts。6.2 限制帧率以省电移动端可通过maxFPS封顶帧率minFPS则用于防止长时间卡顿后一次补很多帧导致的跳跃app.ticker.maxFPS 30; // 上限 30 FPS省电 app.ticker.minFPS 10; // 帧间隔超过 100ms 时截断 delta默认即为 10两者的内部映射关系在 Ticker.ts 的 setter 中实现maxFPS换算为最小帧间隔_minElapsedMS 1 / (fps / 1000)minFPS换算为最大允许帧间隔_maxElapsedMS 1000 / minFPS默认 100ms二者互相钳制以保证上限不低于下限。需要特别留意maxFPS会改变实测FPS因为限制了实际渲染节奏而minFPS只截断deltaTime/deltaMS的计算值、不影响实测帧率speed属性默认1可对整个 ticker 做慢动作0.5或快进2.0时间缩放。6.3 完全手动渲染无 Ticker若希望把渲染主动权完全交给自己的requestAnimationFrame例如与自研物理循环深度耦合app.ticker.stop(); function customLoop() { requestAnimationFrame(customLoop); app.renderer.render(app.stage); // 手动渲染支持 { container, target, clearColor } 选项 } customLoop();手动模式下请自行保证变换与剔除的更新时机可参考 CullerPlugin 的做法或自行调用Culler.shared.cull(stage, renderer.screen)否则可能出现逻辑已更新、画面未刷新的错位。七、性能实践小结动画参数用deltaTime无量纲标量真实计时用deltaMS/elapsedMS避免高刷屏与低帧率设备上动画快慢不一大量对象的逐帧逻辑优先挂onRender全局逻辑用ticker.add并善用优先级交互INTERACTION→ 动画HIGH→ 常规NORMAL→ 渲染LOW→ 预备UTILITY横版卷轴等大量屏外对象场景开启cullable并用cullArea缓存边界、cullableChildren false跳过递归以降低每帧测量开销对象少时剔除本身也有成本需按实际场景取舍可参考 performance-tips.md移动端用maxFPS限帧、必要时stop()暂停配合autoStart/sharedTicker选项按需组织循环保留模式 RenderGroup 脏检查意味着场景图未变化的部分不会重复上传与重建指令保持场景树稳定、避免每帧大规模增删节点能让渲染循环保持高效。【免费下载链接】pixijsThe HTML5 Creation Engine: Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.项目地址: https://gitcode.com/gh_mirrors/pi/pixijs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表