每帧重建一次:Godot 的 Polygon2D 修复如何把演示场景从 28 FPS 推到 83 FPS

约 20 个动画角色的 Godot 演示场景只有 28 FPS,原因是 Polygon2D 每帧都在重建网格。顶点数不变时就地更新已有网格后,作者的演示场景达到了 83 FPS。

每帧重建一次:Godot 的 Polygon2D 修复如何把演示场景从 28 FPS 推到 83 FPS
本篇导航9 个章节

大约 20 个动画角色,本不该拖垮帧率。但在一个 Godot 演示场景里,它们确实拖垮了:场景只有 28 FPS,而最大的单项开销不是画画面,是每帧从头重建画面。修复没有改变场景的样子,只改变了引擎丢弃已有成果的频率。

自制标题卡,不是游戏截图或测试截图。
  • 现象:约 20 个动画角色的演示场景性能下滑,作者测得单帧 35 毫秒。

  • 剖析:Polygon2D 通知占 15.7 毫秒,绘制占 11.7 毫秒,其中真正用于渲染的只有约 3.4 毫秒。

  • 根因:顶点动画导致 Polygon2D 内部网格每帧重建,顶点数组每帧都在创建与释放。

  • 修复:顶点数不变时,用 Godot 低层 API 就地更新已有网格的顶点数据,不再重建网格。

  • 复测:单帧从 35 毫秒降到 13 毫秒,作者的演示场景在作者设备上从 28 FPS 升到 83 FPS。

  • 第二个案例:给着色器转译的每个线程各配一个堆,TPS 演示的加载时间省了 11 秒(Godot 团队报告)。

数字背后的游戏

Polygon2D 案例始于一次对话,而不是一张基准表。正在开发 Heidi's Legacy: Mountains Calling 的 Aurélien Condomines 告诉 Godot 团队,动画代码很慢,限制了他往游戏里加内容的能力;约 20 个动画角色上屏,性能就开始下滑。这个数字让作者起了疑心:区区约 20 个角色不该让动画代码成为瓶颈,一定还有别的问题。

具体做法确实不寻常,文章也直言不讳:用 AnimationPlayer 给 Polygon2D 的顶点做动画,再把 Polygon2D 显示在 Sprite3D 上的 Viewport 里,好让 2D 动画在 3D 世界里看起来自然。不寻常不等于有错,作者指出这本身不该造成显著问题。于是下一步不是重写动画,而是给演示场景做剖析,看时间到底花在哪里。文章注明,该演示场景的全部测试都在 Ryzen 5 9600X CPU 上完成。

自制说明卡:五步优化方法。

先有方法,再有修复

按作者的概括,CPU 性能优化分五步:定位瓶颈、理解瓶颈为什么在那里、研究解法、重新测量、重复。最难的是第一步。引擎开发者面对的不是一个游戏,而是成千上万个不断变化的目标,往往只有收到缺陷报告时才知道出了严重问题。定位 CPU 瓶颈最好的工具是 CPU 剖析器。Godot 编辑器内置了两个,一个针对 GDScript 代码,一个针对渲染器,但引擎层 C++ 代码需要外部剖析器。作者的首选是 Superluminal,一款外部采样剖析器,可以挂到正在运行的 Godot 实例上记录数据。Godot 也支持用 Tracy 做追踪剖析的构建。

还有一句必须放在前面的警告:性能由 CPU 与 GPU 中较慢的一方决定,再多的 CPU 优化也救不了低效的着色器。渲染工作总是在两者之间权衡:2D 批处理会轻微损害 GPU 性能,但大幅改善 CPU 性能,而 2D 游戏往往先碰到 CPU 瓶颈,所以总体是赚的;3D 里 Godot 则把遮挡剔除放在 CPU 上做,以减轻 GPU 负担。而最后一步重新测量不是可选项:文章举例说,直觉上更快的改法,比如加分支跳过计算、缓存数值,反而可能因为阻断编译器向量化、或用昂贵的内存读取换取廉价计算而变慢。

自制说明卡:约 20 个动画角色,帧率开始下滑。

症状,要用数字说

演示场景单帧 35 毫秒:AnimationMixer 占 4.6 毫秒,作者认为比理想值慢,但考虑到场景里动画轨道非常多(每个顶点都在动),并不意外;Polygon2D 通知占 15.7 毫秒;绘制场景占 11.7 毫秒,其中创建顶点数组花 3.3 毫秒、释放花 5 毫秒,真正渲染只占约 3.4 毫秒。

  • AnimationMixer:4.6 毫秒,比理想值慢,但轨道众多时在意料之中。

  • Polygon2D 通知:15.7 毫秒,最大的单项开销。

  • 绘制:11.7 毫秒,其中创建顶点数组 3.3 毫秒、释放 5 毫秒。

  • 真正渲染:11.7 毫秒绘制开销中仅约 3.4 毫秒。

自制说明卡:35 毫秒单帧的分解。
自制说明卡:网格每帧都在重建。

根因

一眼就能看出两处不对:Polygon2D 在做意料之外的事情,而这件事导致顶点数组每帧都在创建与释放。放大时间线、查看调用图,指向同一个地方:大部分时间花在新增表面与释放旧表面上。构建 Polygon2D 内部网格的代码每帧都在运行,而且非常慢。拼在一起,结论事后看来几乎显而易见:因为顶点每帧都在 CPU 上变化,网格每帧都在重建。

修复,以及让修复成立的条件

自制说明卡:顶点数不变就地更新。

Godot 提供了一个可以手动更新顶点数据的低层 API,这就是修复所用的杠杆。已知网格的顶点数不会变时,根本不需要释放旧网格、分配新网格、再把顶点数据上传到新网格,直接把顶点数据上传到已有网格即可。作者的拉取请求就是这么做的。

作者强调,拉取请求的大部分工作不是上传本身,而是区分哪些情况适合更新网格、哪些情况仍需重建网格。这个区分才是能迁移到别处代码的部分:就地更新只在数据形态不变时才安全,修复是一条判定规则,而不只是一次更快的调用。不问条件直接套用,顶点数一变就会出错,所以文章把这项工作表述为分情况处理,而不是一行提速。

自制说明卡:修复后的 13 毫秒单帧。

在作者设备上复测,单帧从 35 毫秒降到 13 毫秒;渲染约占帧时间的 50%,动画约占 30%,Polygon2D 更新约占 20%。作者直接给出了标题性结果:动画 Polygon2D 的性能大约提升了三倍,演示场景在作者设备上从 28 FPS 升到 83 FPS。读数说明:35 毫秒到 13 毫秒说的是单次捕获的帧,28 FPS 到 83 FPS 是作者报告的演示结果,两者是不同的测量,不应互相换算。

作者也诚实地写下了没做的事:只要有顶点移动,修复后仍会每帧重传整个网格数据。进一步的优化可以只更新变化的网格区域,让只动少数顶点的动画不再为全量上传买单。但这一步是刻意没做的:该演示场景基本每帧动全部顶点,做局部更新所需的追踪很可能得不偿失。文章把这条写成了规则:没有真正受益的测试用例,就没法把优化做好。

第二个案例:线程在等内存

自制说明卡:每线程一个堆省了 11 秒。

当年早些时候,Asilkan 在研究 Godot 的 D3D12 后端着色器编译管线,流程分四步:内置着色器编译器把 GDShader 编成 GLSL,GLSLang 把 GLSL 编成 SPIRV,Mesa 的 NIR 转换器把 SPIRV 转译成 DXIL,DXIL 再交给 GPU 驱动编成二进制。其中转译一步是最慢的,而且从 Vulkan(可直接使用 SPIRV)切到 D3D12 时,加载时间差异很明显。

剖析显示有些时段只有少数线程在工作。放大一看,这些空隙都是线程在向操作系统申请更多内存:工作本身是高度多线程的,但所有线程都在等操作系统从同一个全局堆里分配更多堆内存,于是互相拖住。修复是给每个线程各配一个堆,让它们从自己的堆里分配,不再频繁向操作系统申请共享堆内存。报告的结果是 TPS 演示的加载时间省了 11 秒,文章称这 11 秒纯粹是 CPU 空转等待浪费掉的时间。和第一个案例一样,这 11 秒是针对该演示的团队报告数字,不是普适的加载提升。

自制说明卡:带回自己项目的检查清单。

带回自己项目的东西

  • 先剖析再改动,别轻信症状的表面位置:Polygon2D 场景看起来像动画问题,实际是网格生命周期问题。

  • 先拆分开销再下判断:11.7 毫秒绘制开销里真正渲染只占约 3.4 毫秒,其余是分配 churn。

  • 优先选择不做,而不是做得更快:修复删掉了一次重建,堆修复删掉了一段等待。

  • 写清修复成立的条件:顶点数不变就地更新,变了就重建。

  • 在同一场景、同一硬件上重新测量,数字归谁就写谁,绝不写成通用承诺。

这个案例没有证明什么

  • 28 FPS 到 83 FPS 的结果,并不证明每个 Godot 2D 游戏都能快三倍。测量来自一个演示场景、一颗 CPU、一种技术。

  • 文章没有说明 Polygon2D 拉取请求在哪一版 Godot 发布,因此本文不做版本声明。

  • 源文章没有发布代码清单,只有剖析器截图,因此本文不发布代码、不转录片段。

  • 11 秒的着色器数字只针对 TPS 演示与 D3D12 后端,不适用于可直接使用 SPIRV 的 Vulkan。

来源与证据边界

来源:Thaddeus Crews,Optimizing CPU-side Rendering Code,Godot 引擎官方文章,2026 年 9 月,godotengine.org/article/rendering-cpu-optimizations;Godot 引擎文档 CPU optimization,仅作剖析器背景,不取数字。本文全部帧时间、帧率与加载时间数字均为 Godot 官方文章中针对其演示场景与硬件的报告,并在正文里做了归属。本文没有编造任何新的基准、帧率、代码或测试结果。本文不含任何联盟链接。

  • 一行被重算 63000 次的字符串长度,是怎么让 GTA Online 读盘 6 分钟的

  • 一个缺失字形就要 16 MB:Foxy Dumplings 如何把字体图集内存从 48 MB 压到 4-8 MB

  • 为什么 Godot 网页版在编辑器里能跑,到了浏览器却打不开?

  • Godot 游戏编辑器里好好的、网页版一交就崩,是为什么?

本文不含任何联盟链接。正文中的全部时间与帧率数字均已在文中归属来源;本文没有编造任何新的基准、帧率、代码或测试结果。

收藏这篇攻略

登录后可收藏攻略,并在不同设备同步。

登录 / 注册