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

资讯详情

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

微信小游戏SRP合批优化:从原理到实战的排查指南

微信小游戏SRP合批优化:从原理到实战的排查指南 做TA这几年最让我头疼的不是shader写不出来而是“为什么这个场景合不了批”。尤其到了微信小游戏这种环境下CPU性能捉襟见肘Draw Call稍微多一点点帧率就肉眼可见地往下掉。最近我把手头几个小游戏项目里跟SRP合批相关的场景问题重新梳理了一遍越理越觉得这东西其实有规律可循不是靠玄学调优而是有一套固定的记忆框架和排查流程。这篇文章我主要想聊聊我在Unity微信小游戏开发中梳理“SRP合批问题”的方法包括SRP Batcher到底在小游戏环境里扮演什么角色、合批失败的原因怎么快速归类、用Frame Debugger做场景梳理的完整流程以及小游戏打包环境里那些容易忽略的隐藏坑。适合正在做Unity小游戏性能优化的TA、客户端开发以及刚接触渲染优化但被合批问题折磨过的朋友。内容不会绕弯子全部来自实际项目里踩过的坑和验证过的方案。1. 先把概念理顺SRP Batcher在小游戏渲染里到底干了什么很多刚接触这块的同学会把SRP Batcher当成一种“合批器”以为它和静态合批、动态合批一样是把多个物体的网格合并成一个再提交。这个理解不能说全错但方向偏了。它本质上是一个渲染状态打包器做的事不是合并顶点数据而是把兼容SRP Batcher的材质属性统一打包成GPU侧的CBUFFER一次性上传然后在绘制时跳过大量重复的渲染状态绑定。打个比方传统每画一个物体就像每个人去食堂点餐师傅现炒一份端一份排队时间长SRP Batcher是食堂把整个班级的饭统一用保温箱装好窗口师傅只管递省掉的是“一份份现做”的重复劳动。它省的是CPU在每帧准备渲染状态、上传材质参数上的时间而不是减少GPU侧的绘制次数。1.1 小游戏环境下的性能瓶颈和原生项目不一样在PC端或者高端手机上做优化CPU和GPU通常没那么紧张但微信小游戏不一样。小游戏的逻辑和渲染提交要经过一层额外的转换加上小游戏运行在浏览器/WebView体系下内存、CPU频率、发热控制都受限制。实测下来同样的场景在原生Android上能稳定跑到60帧打进微信小游戏容器里可能只有30多帧而且掉帧往往先出现在CPU侧。这就导致一个现象原生项目里“多几个Draw Call无所谓”的想法放到小游戏里就是灾难。你把Draw Call从300降到120帧率可能就上来了反过来如果你只盯着GPU时间发现渲染没压力但帧数就是不高那大概率是CPU在渲染提交路径上被消耗掉了。SRP Batcher在这类场景里的价值被放大了。它不是在减少Draw Call数量而是降低每个Draw Call的CPU成本让同样的绘制次数花更少的时间。对TA来说这是一件“性价比”非常高的改造方向不用合并网格不用重做资源只要保证材质和Shader具备兼容条件就能吃到红利。1.2 SRP Batcher和静态合批、动态合批、GPU Instancing的关系这四者的关系很容易被搞混我简单画一下它们在项目里各自的位置静态合批适合静态物体把网格在编辑器里提前合并减少Draw Call代价是内存和包体变大。动态合批适合小且顶点少的物体逐帧在CPU侧合并网格限制很多小游戏里用得越来越少。GPU Instancing适合大量复用同一网格和材质的物体比如草、树、石头。SRP Batcher适合所有材质兼容的物体它不合并网格就管状态绑定和材质数据上传。很多时候它们是共存的而不是互斥的。一个场景可以同时有静态合批的物体、GPU Instancing的物体、以及走SRP Batcher的普通物体。我在实际优化时通常不会特意关掉哪个而是先看当前瓶颈在哪再决定用哪一套组合拳。提醒一下小游戏环境下动态合批的负面效果可能比正面还大因为它把网格合并放在了CPU侧本身就需要额外计算合并出来的大网格对GPU也不友好。我现在的习惯是小游戏项目默认关掉Player Settings里的Dynamic Batching优先保证SRP Batcher路径能走通再考虑静态合批和Instancing。1.3 URP里SRP Batcher的启用条件和基础要求如果你用的是URPSRP Batcher默认是打开的但仅仅是打开开关还远远不够。Shader必须声明兼容SRP Batcher最关键的标志是材质属性需要写在CBUFFER块里不能直接用内置的_Color、_MainTex_ST这类变量或者用了这些变量但没有把它们放进CBUFFER_START(UnityPerMaterial)里。URP自带的Lit、Unlit这些标准Shader是兼容的但你自己写的Shader就未必了。很多美术同学网上抄的Shader没有CBUFFER声明一旦放到项目里物体就自动退出SRP Batcher路径。这个在Frame Debugger里能看到标记但前提是你得知道去看哪里。在小游戏项目里我强烈建议所有自研Shader都写兼容SRP Batcher的版本。写法其实不复杂核心就是把材质属性包进CBUFFERCBUFFER_START(UnityPerMaterial) float4 _BaseColor; float4 _MainTex_ST; CBUFFER_END这样引擎就能把材质数据统一上传。而如果你在Shader里直接使用材质属性变量但没有CBUFFER包装SRP Batcher会用不了退回老的绑定流程CPU开销自然就上去了。2. 合批失败现场大盘点小游戏里最常见的八种断批场景合批失败的原因五花八门但梳理下来绝大多数都跑不出几个固定套路。我在小游戏项目里排查时已经把“合批为什么会断”这个事收敛成了一张问题清单每次遇到优化任务就照着过一遍速度比以前快很多。2.1 第一位材质实例化与MaterialPropertyBlock的滥用这是最普遍的问题。场景里明明用的是同一个Prefab、同一个材质球但只要代码里对某个物体执行过material.color xxxUnity就会自动实例化出一份新材质这个新材质和原来的材质球已经不是同一个物体了SRP Batcher自然就会把它拆出去。更隐蔽的是MaterialPropertyBlock。很多人知道用MPB可以避免材质实例化但未必知道MPB也有副作用。当你对一批物体使用MPB设置不同的_Color时SRP Batcher虽然会尝试把这些数据打包但不同物体之间的MBP数据不同也会导致批处理断掉。原生的SRP Batcher对MPB的兼容性是有条件的它要求MPB里设置的属性必须在Shader里明确声明在CBUFFER中而且属性值随物体变化时本质上是“一个物体一个batch”合批效果就大打折扣。在小游戏里这个问题会被放大因为小游戏的内存和性能都比较敏感材质实例化多一份就多一份内存绘制时还多一份状态切换。我的经验是能用顶点色存储的差异数据就不要用材质属性能用贴图UV区分就不要用_Color。万物皆可进顶点色真的。2.2 第二位Shader变体与关键字不一致同一份材质球Unity渲染时受关键字影响会编译出不同的Shader变体。两个物体尽管引用同一个材质但如果运行时Shader的关键字状态不同比如一个物体被代码加了_ALPHATEST_ON另一个没有这两个物体就会走不同的Shader变体SRP Batcher直接断开。这个现象在UI图标、道具、角色换装这类场景里特别常见。同一批武器模型因为部分武器开启了“边缘发光”关键字另一部分没开于是相邻的两个同模型物体就是两个batch。我处理这类问题时会先去检查Keywords。如果发现一个材质同时用多个关键字组合先想想能不能拆成多个材质、多个Mesh让变体是在“不同物体”之间区分而不是在“同场景同批次”里区分。次选方案是用代码在场景加载时统一设置关键字确保同一渲染批次里的物体关键字一致。2.3 第三位多Pass Shader和雾效、阴影、额外Pass如果一个Shader有多个Pass比如一个Pass画主体一个Pass画描边或者阴影Pass那么SRP Batcher对它的支持是很有限的。实际表现通常是主体绘制可以批但第二个Pass开始批处理状态会被打断。小游戏项目里很流行“描边风”“卡渲风”很多效果靠多Pass硬叠。每次看到这种Shader我都会打个问号值不值如果描边只是美术上的一点偏好但换来的是整个场景的合批全部失效那代价就太大了。更合理的做法是把描边放到后处理或者用单独的层单独渲染别跟主体物体混在一起。2.4 第四位Lightmap与实时阴影带来的“隐形断批”光照贴图是另一个常见断批点。两个物体如果使用了不同的Lightmap索引或不同的Lightmap UV那么即使材质一样SRP Batcher也会认为它们是不同的渲染状态从而分成两个批次。这在多人共享一个场景、每个区域烘焙不同光照贴图时非常常见。实时阴影更麻烦它需要额外提交一个Shadow Pass而且阴影本身的开销在小游戏里可能是致命的。我一般会在小游戏项目的URP Asset里关闭“Cast Shadows”或者只在关键角色上开启否则全场景的实时阴影不仅打断合批还会把GPU性能砸穿。2.5 第五位RenderQueue和混合模式是美术容易忽略的状态开关两个物体明明用同一个材质但只要RenderQueue被代码或某个工具改过比如一个在Transparent队列一个还在Geometry队列它们在渲染排序上就不一样合批也断了。混合模式同理Alpha Blending和Opaque不能混在一起合批。有时候问题出在资源导入阶段。某个FBX进来时导入器自动给材质设置了Fade模式导致渲染状态变成透明混合场景里的其他同款物体还是Opaque合批就静悄悄地失败了。2.6 第六位网格与顶点数据格式不一致这是最容易被忽视的。两个物体即使使用同一个材质但如果网格的顶点数据布局不同——一个模型有顶点色另一个没有一个有UV2另一个没有——那么SRP Batcher也会认为它们是不同的输入要求不能合批。在小游戏项目里模型来源很杂可能是外包给的、商店买的、网上下的顶点属性经常对不齐。我的习惯是在导入设置里统一模型格式或者在制作规范里规定所有角色都用一套模板从源头避免这种问题。2.7 第七位UI的Canvas与图集UI这块虽然是UGUI的范畴但小游戏项目里UI占的帧时间非常可观。UGUI合批和3D的SRP Batcher不是一回事它自己有一套Canvas Batcher。如果一个UI界面里既有图片又有文字、用了Mask、有大量修改颜色或材质的动画Canvas会把批次拆得很碎。小游戏上我做过一个UI优化把同一个界面里所有图标从散图改成图集并且把所有动态颜色改成预先烘焙好的SpriteDraw Call从200多降到60多肉眼可见地流畅了。UI优化很多时候不是在技术层面而是在资源规范层面。2.8 第八位ParticleSystem的乱入粒子系统默认会加ParticleSystemRenderer它的渲染路径和普通MeshRenderer不太一样如果粒子用了自定义材质而场景里恰好有普通物体也用这个材质往往无法合批。粒子本身因为要排序、用不同的顶点流SRP Batcher基本是不生效的。我自己处理粒子的思路是粒子永远单独分层渲染不要试图跟场景物体“套近乎”合批。粒子Draw Call多的话靠的是减少发射器数量、复用发射器、用Texture Sheet Animation替代多余粒子而不是指望SRP Batcher救场。3. 场景梳理实操用Frame Debugger逐个击破断批点工具层面我主要靠Unity的Frame Debugger配合Profiler在一帧画面里逐条看Draw Call找出哪些地方断了批。这个过程说难不难但要有顺序不然容易被信息淹没。3.1 梳理前提先保证能稳定复现再看数据每次梳理前我会先把场景固定下来。游戏里的角色、特效、UI都要处于一个相对稳定的状态避免镜头一转、怪物一刷新数据就变了。然后关掉VSync打开Development Build用真机或模拟器抓帧。这里有个小技巧先看Profiler里的RenderLoop.Draw和Gfx.WaitForPresent时间如果RenderLoop.Draw占了大头说明CPU侧提交确实吃紧值得去查合批如果瓶颈在Gfx.WaitForPresent或GPU时间那合批优化救不了你得去压Shader复杂度和Overdraw。3.2 逐条读取Frame Debugger事件打开Frame Debugger后每一行都是一个渲染事件。我会重点看标记为“SRP Batch”的事件以及它前后相邻的事件。如果本该批量绘制的一群物体被拆成了很多条说明合批失败。Frame Debugger详情面板里会直接显示当前Draw Call对应的Mesh、Material、Shader、Keywords你可以一一核对。我最常用的操作是点击某一个Draw Call看它的Shader和Keywords再点相邻的Draw Call比对两者差异。一旦发现两个Draw Call的材质一样、网格一样但就是没批上那一定能在Shader变体、材质状态、光照贴图索引、顶点格式这几项里找到答案。这个过程我一般会写一个场景梳理记录表方便后续跟团队对线检查项取值是不是断批点备注Draw Call总数320-目标降到150以内SRP Batch数量180是材质兼容性整体有问题材质实例数量95是大量代码SetProperty导致实例化同模型不同Shader变体3组是需要统一KeywordLightmap索引数量12是同一区域太多不同索引粒子发射器数量45是粒子Render进不了SRP批顶点格式不一致的网格8个是导入设置需要统一3.3 从断批点到根因的三步定位法第一步按“材质分组”视角看场景把所有Draw Call按材质排序同材质的物体能不能连续绘制。如果同材质物体被隔开大概率是渲染状态差异或排序问题。第二步按“Shader变体”视角看查看同一材质在不同物体上的Keyword差异。这一步基本能定位到90%的断批问题。第三步按“数据格式”视角看剩下还没找到原因的查网格顶点格式、光照贴图UV索引、骨骼权重数量。到这里还没找到那就要怀疑是不是引擎API的调用顺序问题比如有人强制调用了DynamicBatch或StaticBatch。3.4 把梳理动作固化成SOP现在我每个项目都会在优化阶段定一个“合批日检”SOP流程是打开场景、关掉干扰、抓一帧、导出Frame Debugger数据、跑一遍上面那张表、把结果贴到项目群。这套动作花不了半小时但能把问题消灭在提交前。很多断批问题不是查不出来而是发现得太晚等场景堆到几万人同屏时才查成本就高了。4. 微信小游戏打包环境下的隐藏断批原因小游戏打包和普通App不太一样有一些坑是只有在提交微信小游戏包之后才会冒出来的。这些坑平时在编辑器里一点问题没有打出来的包就是各种断批让人非常崩溃。4.1 Shader变体裁剪与合批的冲突微信小游戏对包体大小很敏感Unity做了非常激进的Shader变体裁剪。如果你的Shader变体被裁剪掉运行时材质就会走Fallback或者改用其他Shader这不仅仅是画面变丑的问题还会直接导致材质不再兼容SRP Batcher整个场景的合批全部失效。我之前遇到一个情况场景里有个箱子用了自研Shader编辑器里看得好好的打到小游戏包后变成一个粉红色物体而且周边的其他物体也跟着断批。查了半天发现是Shader变体被裁剪了Fallback到了一个完全不兼容SRP Batcher的Shader所有“同材质”判定全部作废。处理办法是在打包设置里不要乱开“Strip Shader Variants”要么用ShaderVariantCollection把你实际用到的变体提前收集好要么在构建脚本里手动添加必需的变体。这个事必须在项目早期就规划不然后期一堆Shader要手工补工作量非常大。4.2 AssetBundle依赖与材质实例化小游戏项目一般都会用AssetBundle做资源分包而AB包的依赖关系如果没理清楚同一个材质会被打进多个AB包里运行时就会生成多份材质实例。看起来是同一个材质实际不是同一个对象SRP Batcher无法识别自然合不了批。我在一个项目里就见过同一个角色的身体材质因为被场景A和场景B分别引用AB打包时被自动复制成了两份结果角色只要换个场景材质就多一份不仅内存慢慢涨合批也慢慢断。后来加了依赖检查和资源去重才把这个问题按住。4.3 纹理格式与内存压力间接影响合批策略微信小游戏要支持多种机型纹理压缩格式很乱。有些安卓机用ETC2有些低端机不支持某些格式纹理就会降级为RGBA32占用内存暴涨。内存一旦告急Unity可能自动降低纹理质量或者卸载资源物体就会重新加载材质加载过程中渲染状态是乱的合批也会瞬断。虽然这类问题和SRP合批本身关系不大但小游戏项目里“内存不足导致资源重载重载导致材质状态抖动”的现象很常见。优化时最好把纹理格式、压缩方案和AB包加载策略一起考虑不要只盯着合批看。4.4 UI、字体和Overdraw在小游戏里的连锁反应UGUI的动态字体是最容易让Canvas批处理失效的元凶之一。如果界面上有大量动态文字Canvas会把文字所用的图集搞得很碎UI合批也跟着碎。更麻烦的是小游戏环境下Overdraw造成的GPU压力比原生更明显一个界面套多层Panel层层半透明帧率直接就没了。我在优化小游戏UI时有个原则能用预烘焙字体的坚决不用动态字体能用整图合并的坚决不用散图能不透明就不透明能少层就少层。UI的合批优化不复杂但需要下决心砍资源、改界面。美术和策划那边需要有人去沟通这个只能靠TA或者技术负责人来推。5. 实战排查记录与团队合批守护经验文档写了再多最终还是要落到实际项目里。最后我把最近做的一个小游戏合批优化项目里最有代表性的几个排查案例拿出来聊聊顺便说一下我是怎么通过流程和工具让团队里每个人都能快速判断合批问题的。5.1 案例一同一图标的20批次凶手是MaterialPropertyBlock项目里有一个技能图标面板30个图标用的都是同一张图集的Sprite、同一个材质球。按道理这30个图标应该能在同一个Canvas下合批但实际上每个图标都占了一个Draw Call。我用Frame Debugger点开一看每个图标虽然材质相同但都被代码里的SetProperty改了_Color来表现技能冷却的变暗效果。每个图标一份材质参数SRP Batcher没法把30份不同的_Color打包成一个批次。解决方案是图标颜色变化改到顶点色上材质Shader里用顶点色乘以纹理色这样顶点数据本身可以不同但材质数据完全一致合批就恢复了。改动量不大效果立竿见影30个Draw Call降到2个。这件事之后我定了规矩所有UI图标的材质一律禁止用MaterialPropertyBlock逐物体改属性。5.2 案例二一个Shader关键字让半个场景断批另一个项目里场景里有大量地砖和墙体用的都是URP的Lit Shader但运行时地砖被脚本加上了_ALPHATEST_ON用来做挖洞效果墙体没有。就这么一个关键字差异导致地砖和墙体明明同材质、同Shader却死活合不了批。优化时我没有改Shader而是把所有需要挖洞效果的地砖拆分到一个单独的子场景层用另一个专门的关键字变体。这样普通地砖和墙体在一个批次里稳定合批需要挖洞的特效地砖走单独的渲染路径互不干扰。这个案例给我的教训是渲染优化一定要从“设计阶段”就开始考虑不要等美术资源全堆上来以后再救火。Shader关键字、材质渲染状态、顶点格式这些东西应该作为一种制作规范提前定下来。5.3 案例三真机Profile时“时好时坏”的诡异断批还有一次很诡异的排查编辑器里合批一切正常真机微信小游戏环境里Draw Call却忽高忽低有时30有时150。后来发现是不同AB包加载顺序导致的——某个共享材质先被场景A加载状态是A里的参数等场景B加载时又拿到了一份重新实例化的材质于是出现了两份同源不同实例的材质球。这个问题光看Frame Debugger很难发现必须把AssetBundle的加载日志和Profiler的Memory快照放在一起看。最终方案是给所有AB包做了统一的依赖项管理所有共享材质都放进一个Common包由启动流程提前加载后续所有场景只引用Common包里的那份材质不再各自实例化。踩完这个坑我现在对团队有一条硬性要求任何材质、Shader、纹理等渲染资源都必须明确归属到唯一资源目录禁止散落在各个场景目录里被重复打包。5.4 把合批检查变成团队的日常动作最后说一个非技术但很关键的事合批优化做了一次两次不难难的是让整个团队在后续开发中不再破坏它。我见过太多项目优化完一版两周后美术上了一个新资源代码加了几行合批又全乱了。我现在每个小游戏项目都会维护一份《合批检查清单》内容包括材质是否统一、是否禁用MPB、Shader关键字是否收敛、模型顶点格式是否一致、UI图集是否规范、AB包依赖是否干净。每次提测前程序或TA至少过一遍清单。如果发现问题直接贴Frame Debugger截图到工作群附上断批原因和修改建议大家按同一套语言沟通效率高很多。框架定了以后我自己处理合批问题的速度已经快了很多而且越来越觉得SRP合批在小游戏场景里不是一个孤立的技术点它牵扯到资源规范、打包策略、代码写法、团队协作。参数调优只是一部分真正值得花时间的是把规则变成可执行的流程让大家在开发过程中自然而然避开这些坑。
返回列表