前几天帮朋友排查一个Pico4项目空荡荡的场景里帧率愣是上不去最后定位到问题全出在一个3万面、8个材质球的角色身上。Unity人物渲染性能优化这件事看起来是换个Shader就能解决实际上一踩一个准骨骼、蒙皮、材质球、纹理、阴影几条线叠在一起任何一处超标帧率都会给你颜色看。这篇文章我从实际的排查和优化经验出发把一条完整的人物渲染链路拆开讲清楚从资产层的网格骨骼到材质Shader、光照阴影再到移动端实测复盘适合被帧率压着打的Unity开发也适合想搞懂为什么角色这么贵的美术同学。1. 一条完整的动画人物渲染链路钱都花在哪了1.1 一帧画面里人物渲染到底做了什么很多人优化场景时思路很清晰静态物体烘焙、合批、遮挡剔除一套组合拳。但人物不一样人物是活体每一步都在实时计算。一个正常被渲染的动画角色在一帧里大致经历了这些事Animator从动画剪辑里采样骨骼姿态更新骨骼层级变换SkinnedMeshRenderer把所有受影响顶点按骨骼权重做蒙皮变换然后材质Shader跑顶点着色器和片元着色器如果角色投射实时阴影还要额外从光源视角再渲染一次Shadow Caster Pass。每一步都不是免费的。骨骼层级的Transform更新在CPU上逐节点跑蒙皮计算如果不开GPU Skinning也是一笔巨大的CPU开销材质球一多GPU上的SetPass Call和状态切换跟着涨。更难受的是人物这类动态物件没法走静态合批和预计算烘焙每一帧都得从头算一遍。所以它天然就是场景里的性能大头跑得慢不是偶然。1.2 人物身上的隐形开销清单我习惯把人物相关的开销分成几个模块来审计下面这份清单基本覆盖了大多数项目的常见雷区Animator骨骼更新骨架越复杂层级越深CPU遍历成本越高。某些插件会逐帧修改骨骼Transform比如飘带、头发附件每加一个都在吃CPU。SkinnedMeshRenderer蒙皮顶点越多越贵如果走了CPU蒙皮几万顶点就能吃掉好几毫秒。材质球数量和Shader Pass一个材质球就是一个SetPass Batch起点多个材质球意味着角色无法合批还要反复切换渲染状态。阴影投射动态角色每帧都要被阴影相机重新渲染一遍等于角色整体多渲染一次。BlendShape表情关键帧驱动的BlendShape逐帧在CPU/GPU上插值计算叠加过多会相当可观。动态骨骼、物理附件Dynamic Bone、Magica Cloth这类组件移动端尤其容易超标每个碰撞体都在做模拟计算。纹理内存和带宽贴图太大且压缩格式不合适时GPU带宽成为瓶颈表现出的症状是GPU耗时高、发热快。这份清单里很多项目在编辑器里看不出来因为PC端的CPU和GPU余量太大了只有真机测试才会暴露。我一直主张移动端项目从一开始就按清单逐项做预算管理而不是等卡了再去查。1.3 先立一个移动端的预算账本结合团队在Pico4、Quest和主流安卓机型上的长期调试我通常会先给项目立一份这样的基础预算项目建议目标说明角色相关Draw Call6个以内只统计角色本体不含特效和UISetPass Call3个以内超过3个就要警惕材质球和Pass膨胀LOD0三角形数3万~6万二次元风格2万~4万足够写实另说骨骼数量60根以内手指、牙齿等无交互骨骼尽量合并角色材质球数量1~3个超过5个建议重新整理贴图和UV布局全套纹理内存8MB以内主贴图和法线贴图控制在1024以下实时阴影距离15米以内移动端级联阴影开2级就够同时投射阴影的角色数1个通常是主角其他角色用假阴影这份账本不是死标准但每项超了都意味着你在某一帧里多花了一笔钱。后面讲优化方案时我会反复回到这张表上所有动作都在往这些数字上面靠。2. 网格与蒙皮从资产层做减法2.1 三角形和骨骼数目的主流预算很多团队一提到优化就盯着Shader实际上真正的源头在资产层。模型刚进引擎时如果三角面和骨骼数就超标后面不管怎么调材质都救不回来。移动端角色的LOD0我建议控制在3万~6万三角面。市面上很多好看的手游角色其实是2万~4万面靠法线贴图和风格化材质撑出细节画面并不差。写实方向的项目可以往6万以上走但要清楚每一个顶点都是要算蒙皮的。LOD1通常砍到LOD0的60%左右LOD2再砍到LOD0的20%~30%。远处根本看不清的地方别让GPU白算那么多顶点。骨骼数量是另一个容易被忽略的点。默认情况下角色模型的骨骼层级会跟着Animator逐帧更新每根骨骼都是一个Transform节点CPU都要遍历。移动端我的建议是控制在60根以内实际上二次元角色30~40根就足够用了。手指骨骼除非要做手部抓握交互否则直接合并掉牙齿、眼睛、飘带这类装饰骨骼如果不是动画必须也一起合并。注意做骨骼瘦身最好在DCC软件里做比如3ds Max、Blender、Maya导出前就把骨架整理干净。不要在Unity里直接删节点很容易把蒙皮权重搞乱角色动作变形找起来更痛苦。2.2 蒙皮到底跑CPU还是GPU蒙皮计算放在哪里直接决定这批顶点的开销花在哪个硬件上。Unity的Player Settings里有个GPU Skinning选项勾上之后蒙皮计算会放到GPU执行能明显把CPU从几万顶点的矩阵运算里解放出来。对那些CPU非常紧张的移动端项目这个开关收益很大。但GPU Skinning有它的代价和BlendShape的兼容性不好。如果你的人物脸上有大量表情BlendShape开启GPU Skinning后引擎可能会被迫把相关Mesh退回CPU路径或者出现表情异常。所以我的习惯是纯身体Mesh可以开GPU Skinning头部和面部Mesh保留CPU路径或者干脆把表情数量做少一点让整个角色都走GPU。另外还有一个容易被忽略的选项Optimize Game Objects。它能让Unity对骨骼层级做转换优化降低逐帧更新骨骼Transform的开销。代价是不能在运行时直接通过Transform去读某些骨骼的局部坐标如果项目里有脚本依赖骨骼Transform开启前先查一下引用关系。骨骼数少的项目甚至可以考虑完全绕开Animator的Transform层级用GPU Instancing加自定义骨骼动画驱动但那套方案复杂度高不是常规项目的首选。如果团队里有人提到上Burst和DOTS来做动画我的意见是成百上千个角色的集群场景才值得上DOTS三五个核心角色上这套架构就是自己给自己挖坑调试成本远大于收益。2.3 LOD设计不能只砍三角面角色LOD最常见的错误是只减三角形数效果上没做任何降级。事实上LOD每一级都应该同时做两件事几何减面效果降级。我常用的角色LOD策略是LOD0完整模型完整材质Pass有描边、有实时阴影用于近距离对话和过场。LOD1模型减到60%面数关掉描边Pass或换简化描边保留主光阴影和环境反射。LOD2模型减到LOD0的20%~30%换更简单的Toon材质关闭实时阴影接收只用光照探针加假阴影。举个例子LOD1开始可以把材质从三Pass的卡通描边Shader换成单Pass简化版LOD2甚至可以直接关掉头发的高光层和发光贴图。这些降级在远处根本看不出差别但GPU上的Pass数量会肉眼可见地往下掉。SkinnedMeshRenderer做LOD切换要格外注意Culling和过渡否则角色会在切换瞬间闪现或跳变。我一般把切换距离留出缓冲避免角色在视线边缘来回横跳时反复切换LOD。Unity的LOD Group支持交叉淡入淡出但对蒙皮网格来说这个过渡成本偏高移动端我建议直接硬切把切换距离阈值错开就好。2.4 BlendShape表情系统也得有成本表BlendShape是最容易被忽略的隐形开支。一个角色脸上挂几百个BlendShape即使当前没用引擎也要为它们维护内存动画驱动表情时每个激活的BlendShape都要按权重做顶点插值数量多了CPU和GPU都会被拖住。移动端的表情预算我建议控制在50个以内核心表情保持在20~30个。表情的关键帧动画最好提前烘焙成动画剪辑运行时只做Clip采样不要每个表情都在LateUpdate里手动改权重。现在很多项目开始用贴图动画或者UV动画去替代部分BlendShape比如脸颊红晕、眼睛高光这类效果其实一张序列帧贴图就能搞定没必要用网格变形去实现。有一次我排查一个卡顿问题发现脸部Mesh有120多个表情其中大量表情是眉毛上挑1度这种几乎看不出差别的变体。跟美术沟通后砍到40个脸部蒙皮开销直接掉了一半。BlendShape这种东西数量太多不仅费性能还浪费美术同学的时间。3. 材质、Shader与纹理藏性能黑洞最多的地方3.1 一个角色应该有多少个材质球材质球数量对人物渲染的影响比很多人想象中大得多。每个材质球代表一组独立的渲染状态GPU要为此切换Shader、混合模式、贴图绑定SetPass Call和Draw Call一起涨。一个角色如果贴了8个材质球基本就等于告诉引擎我拒绝合批我要画8遍。理想的移动端角色应该是1个材质球走天下实际项目中2~3个也可以接受。比如身体一个材质、头发一个材质、脸一个材质、眼睛一个材质这已经算很宽裕了。超过5个材质球我建议直接返工整理贴图和UV布局。怎么把材质球数量压下来核心方法就是把多张贴图合并成一张图集让所有部位共用一套UV。比如身体、四肢、衣服全部摊进同一张1024贴图再靠Tiling和Offset或者材质属性在Shader里做区域裁剪。这样角色本体只有一个材质球Draw Call就是1Batch就是1GPU上的状态切换几乎为零。有一个操作细节同一个角色的不同表现比如受伤变红隐身闪烁不要通过复制Material来实现。直接用MaterialPropertyBlock改颜色、改透明度、改AlphaClip阈值开销小而且不会破坏合批。这是很多初级开发容易踩的坑new一个Material出来Batch就毁了。3.2 变体膨胀Shader功能开关的隐形代价Shader变体是Unity渲染性能里的隐形杀手尤其是用URP之后。一个默认的URP Lit Shader动辄几百上千个变体到了构建的时候每个关键字组合都可能被编译成独立的GPU程序。这些东西最直接的影响是打包时间变长、包体变大、玩家第一次进游戏时Shader编译卡顿微信小游戏环境尤其明显。人物的主Shader更要严格控制变体数量。一些团队美术想给角色加上一堆功能开关雨雪效果、溶解、描边粗细、风格化阴影、各向异性高光……每个开关都是一个Keyword每多一个Keyword变体组合可能是翻倍地涨。我做过一次实测某个角色Shader把不用的Keyword全剪掉之后变体数量从两千多降到三百多构建时间缩短了一大截真机首帧的卡顿也消失了。维护方法很简单用一个Shader Control之类的变体管理工具把项目里实际用到的Keyword白名单列出来裁剪掉永远用不到的变体同时把最常用的几个变体加入Always Included Shaders防止运行时因为漏编译出现粉红色模型。如果你有精力写一个精简的人物专用Shader效果会更彻底。移动端角色Shader最多保留这些功能就够了主贴图和颜色法线贴图可选阴影Ramp或卡通分级阴影高光或者MatCap二选一AlphaClip用于头发刘海和边缘ShadowCaster Pass其他都是可以砍的。写实向角色可能还需要环境反射和SSAO的接口但SSAO这类屏幕空间效果不要做到角色材质里交给后处理统一处理。3.3 二次元卡通渲染在移动端的优化开关二次元风格在PC上跑非常华丽liltoon、UTS等插件提供了轮廓线、各向异性头发高光、视差贴图、高级阴影等特性。这些放到移动端全是性能刺客尤其是liltoon这种Pass种类繁多的Shader在Pico4上能轻松把一个角色推到2个Draw Call变8个。移动端用卡通渲染我建议只保留这几个特性Ramp阴影、基础高光、可选描边、AlphaClip头发。liltoon和UTS在各自的高级设置里都有针对移动端的优化项本质是把那些视差、各向异性、多层高光、反射探针叠加层全部关掉。描边如果非留不可尽量用几何描边而不是后处理描边如果角色不是主角建议直接不做描边远处的描边本来就糊成一片看不出差别。还要注意卡通渲染的多Pass问题。很多二次元Shader里面一个Pass管主体、一个Pass管描边、一个Pass管头发高光。UE风格、Unity风格的项目里如果里面还挂了Dissolve、Emission闪烁之类的效果Pass数量能上到6~8个。这种东西在材质球制作阶段就要控制别等上真机再发现Overdraw爆表。3.4 纹理压缩、尺寸与带宽预算移动端的GPU带宽非常宝贵尤其像Pico4、Quest这类一体机发热和掉帧常常是带宽打满。纹理这块做不好其他优化全都白搭。移动端主流压缩格式是ASTCUnity的Android平台默认支持。同一个纹理用RGBA32真彩色和用ASTC 6x6压缩内存差距可以达到4倍以上。以一张1024x1024贴图为例RGBA32大约是4MBASTC 4x4降到1MBASTC 6x6再降到不到0.5MB。纹理尺寸和格式对角色整体内存的影响就摆在那里。我推荐移动端角色贴图规格如下贴图类型建议尺寸建议格式角色主贴图Color1024ASTC 6x6法线贴图1024ASTC 6x6金属/粗糙度/Ramp图512ASTC 6x6发光贴图512ASTC 8x8或6x6阴影衰减/透贴256~512ASTC 8x8如果角色是二次元风格法线贴图甚至可以不用靠纯色块和Ramp阴影就够。每个角色全套纹理控制在8MB以内这个数字在移动端是安全的。这里有个容易踩的坑美术给的源文件是PSD或者16位TGA直接从外部导入Unity后如果没设置压缩内存会很难看。正确做法是在Import Settings里把Texture Type和Compression统一规范做成美术侧的Prefab规范文档而不是每次手动调。3.5 半透明排序和AlphaClip的陷阱半透明材质在人物身上几乎无处不在刘海头发、轻纱裙摆、武器光效。半透明渲染的问题首先是不排序透明物体之间要按深度排序排序一旦出错就是长发穿脸、裙摆穿腿其次是Overdraw半透明像素要混合同一块屏幕区域被反复画很多遍。在移动端半透明材质的Overdraw对GPU的压力非常直接。我做过一个头发发光的特效起初用半透明ShaderRenderDoc里看Overdraw模式头部区域接近全屏8倍OverdrawGPU时间直接垂直起飞。换成AlphaClip的裁剪模式之后开销降了一大半虽然边缘还是有一点锯齿但用一张边缘噪点图做AlphaClip就能柔化得不错。优化原则很简单能不用半透明就不用半透明。头发高光用贴图做布料花纹用贴图做武器能量条用贴图序列帧做全都是不透明的解决方案。实在必须半透明的限制到只有一个Pass的简单Shader而且不要让多个半透明角色堆叠在画面里。AlphaClip本身也有开销毕竟要做像素丢弃但它的开销远小于半透明混合和排序。使用AlphaClip的材质要记得在Shader里启用Alpha To Coverage或者加边缘AA否则头发边缘会像狗啃一样。相关参数如果用脚本控制记得走MaterialPropertyBlock不要为了做一个逐渐消失效果就复制Material。4. 光照与阴影的取舍实时阴影不是默认选项4.1 实时阴影的真实成本实时阴影的本质是从光源的视角把整个场景再渲染一遍生成Shadow Map然后在主摄像机的Pass里采样阴影贴图。场景里每有一个投射阴影的实时物体都在给这个额外Pass增加负担。人物这种每帧都在动的物体阴影没法烘焙只能实时算成本就比静态物体高得多。很多人第一次调阴影习惯把Directional Light的Shadow Distance拉远一点觉得阴影范围大一点更真实。这在PC上没问题在移动端就是灾难。阴影距离从15米拉到50米意味着阴影相机要覆盖更大的场景范围为了保持清晰度还得提高Shadow Map分辨率或增加Cascade级联GPU开销几倍几倍地涨。移动端我建议主光源阴影距离控制在15米以内Cascade开2级Shadow Map分辨率1024软阴影用低开销的PCF 2x采样关掉高精度软阴影。主角可以在这套配置下接收实时阴影但NPC和敌人严格执行总数不超过1个实时阴影对象的纪律对主角打不打阴影可以做成选项由策划在对话镜头和战斗镜头里按需开关。4.2 移动端人物阴影的替代方案既然实时阴影这么贵移动端就得学会做假。假阴影方案我在多个项目里用过效果不错而且性能稳定方案适用场景实现方式性能平面假阴影NPC、普通敌人在角色脚下放一个半透明或不透明的圆形/椭圆形面片跟随角色移动极低接触阴影贴花主角近距离细节用Projector或Decal Projector在地面投影角色脚部阴影低Contact Shadow主角近距离在Shader里用屏幕空间采样做短距离阴影中光照探针所有动态角色角色只接收LightProbe的间接光不参与实时阴影极低实时阴影仅限主角配合15米内的Shadow Distance使用高一个很常见的问题很多朋友问过场景里明明开了实时阴影为什么角色周围有方块状阴影、阴影在角色脚下闪跳大多数时候是Shadow Distance和Cascade的边界没有调好或者角色的Shadow接收设置在不同LOD下表现不一致。先把距离压短、级联调好再看是不是ShadowCaster Pass的问题。关于遮挡剔除要提一句遮挡剔除对静态场景非常有效但人物是动态物体它能被剔除的机会很少除非你按区域做人物进入房间才渲染这类逻辑。真正决定人物性能的还是距离、可见性管理、LOD和阴影预算这一套组合拳。4.3 别忘了几件小事ShadowCaster Pass、层和距离自定义Shader的开发者最容易忘的一件事如果Shader里没有ShadowCaster Pass这个角色在实时阴影系统里就是隐形人。它不会向阴影贴图里写入数据也不会正常接收阴影。Unity的默认Lit和Toon Shader都带这个Pass但很多网上找来的二次元Shader、精简Shader是缺的表现出的现象就是角色站在阴影区域却亮堂堂的。另外可以用Layer和Culling Mask来控制谁投射阴影、谁接收阴影。比如主角放Player层普通NPC放NPC层主光源的Culling Mask设置为只包含Player层能做到只有主角产生实时阴影其余NPC全部走假阴影方案。这种做法的好处是不用改任何材质只改一个光源的设置。最后是阴影距离它不是孤立参数。人物进入阴影区域后如果发现角色脚底阴影突然消失或阴影从远处跳出来先别怀疑Shader把Directional Light的阴影设置和相机Far Clip、场景规模一起核对一遍。我见过太多人花一下午调Shader最后发现是Shadow Distance设得太短导致阴影刚到脚边就没了。5. 移动端实测Profiler、Frame Debugger与一次完整优化复盘5.1 排查工具的正确打开方式性能优化不能光靠眼睛看要按数据说话。我日常排查人物渲染性能工具链基本是固定的Unity Profiler的CPU模块看GameLogic、Animation、SkinnedMeshRenderer、Meshes这几个模块的耗时能快速定位是CPU侧的骨骼更新在拖后腿还是蒙皮计算在吃时间。Frame Debugger逐帧看Draw Call和SetPass Call能直接看到某个角色被拆成了多少个批次、每个批次切了什么渲染状态。这是检查材质球数量的最好工具。RenderDoc看GPU侧的Pass实际开销、Overdraw和带宽。移动端很多CPU看不出来的问题一开RenderDoc的Overlay就现形了。Unity的Statistics窗口看当前视野的Batch数量和三角形数改完一版立刻肉眼可见。URP Render Pipeline Debugger检查URP的Renderer Features实际生效顺序防止某个后处理特性在空格内偷偷吃掉大量GPU时间。只用一个工具很容易误判。比如Profiler里CPU不高但帧率还是掉那问题大概率在GPU或带宽这时候得靠RenderDocFrame Debugger显示Draw Call很低但画面还是卡那重点就该转向Overdraw和Shader复杂度。工具之间是互补关系。5.2 一次Pico4项目的优化复盘从4.8ms到1.6ms拿一个实际项目案例来说。一个Pico4上的3D展示项目最初版本角色相关的性能非常紧张我抓数据时的情况是这样的指标初始值优化后改动内容三角形数8万3.5万重建LOD0新增LOD1/LOD2骨骼数量12058合并手指、牙齿、装饰骨骼材质球数量82贴图集合并统一UV布局纹理格式RGBA32ASTC 4x4/6x6全套压缩尺寸压到1024以内阴影距离50米15米只给主角保留实时阴影CPU帧耗时4.8ms1.6ms骨骼迭代和蒙皮压力大幅下降GPU帧耗时10ms约4.5ms材质Pass数量减少带宽大幅降低每一步改动都不是拍脑袋。骨骼从120根降到58根Animator的骨骼更新开销直接少了一半多材质球从8个变2个Frame Debugger里角色的Draw Call从10多次降到了3次纹理全改ASTC之后GPU带宽明显松快发热也减轻了。最终整帧水平从20多帧稳定到60帧角色观感在10厘米和2米距离上没有明显差别。这套过程里最值钱的不是某一项魔法改动而是预算账本驱动的逐项审计。每次改动都能在Profiler里看到对应数据的改善改起来心里有底不会瞎试。5.3 现场最容易踩的两个坑优化过程中最容易翻车的两个坑我基本每次都遇到。第一个坑是BlendShape和GPU Skinning打架。某个角色开启GPU Skinning之后面部表情突然失效或者某几个表情错乱。原因就是引擎检测到Mesh带大量BlendShape时会退回CPU路径进行蒙皮结果CPU压力没减下来表情还出了问题。解决办法很简单要么减少BlendShape数量让GPU Skinning顺利接管要么头部Mesh单独设置不走GPU Skinning用CPU跑几十个顶点代价可控。第二个坑是动态骨骼插件偷偷改Transform。项目里加了Magica Cloth或者Dynamic Bone做头发物理和裙摆摆动这些组件每一帧都在修改骨骼Transform把Unity针对蒙皮做的优化缓存整体打断导致蒙皮重新计算。表现就是Profiler里可以看到SkinnedMeshRenderer耗时飙升。移动端上这类物理附件必须限量最多一两处而且更新频率要降档别让它以完整帧率跑。还有一个附加坑Shader变体裁剪过狠某些角色到特定场景或特定光照下突然变粉、变黑。这是Keyword被误删了。处理办法是变体裁剪列表保留一份常用变体白名单并且Always Included Shaders里至少把最基础的主光、无光照、阴影接收三个变体放进去兜底。人物渲染优化这件事做得多了你会发现真正的大头永远是资产层的取舍和Shader清理而不是某个看起来很高端的渲染技巧。先把网格骨骼理清楚把材质球压到最低再把阴影搬到该用的地方多数卡顿问题到这里就已经解决大半了。最后再分享一个小技巧改完一版别在空场景里测帧率一定要跑完整的关卡。人物渲染和场景光照、阴影、后处理是缠在一起的空场景里好看的数字到真实环境经常撑不住。我见过太多团队在灰盒环境里测得漂漂亮亮一到正式场景就原形毕露。拿完整关卡跑一遍RenderDoc和Profiler看到的才是真答案。