ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity人物渲染性能优化:从DrawCall到Shader的移动端实战指南

Unity人物渲染性能优化:从DrawCall到Shader的移动端实战指南 写这篇东西的契机是上个月帮朋友的项目救场。他们的二次元角色在主角登场那段一开大招帧率直接从60掉到20出头发热也压不住真机烫手。我把Profiler一拉DrawCall和渲染耗时完全失控角色身上那套材质和阴影方案在移动端根本顶不住。这种问题在Unity项目里太典型了人物渲染从来就不是“把模型放上去能看”就完事的东西它牵涉到骨骼、蒙皮、动画、材质、阴影、变体、后处理一整条链路任何一个环节偷懒最终都是玩家的手机替你买单。这篇Unity人物渲染性能优化的经验总结就是给那些想在移动端、低端机上把角色做得既好看又不卡的朋友看的不管是初中级开发者还是独立游戏作者都能在里面找到能直接抄作业的优化方案。1. 先搞清楚人物渲染的钱都花在哪了——性能开销的底层拆解很多人一提到角色性能问题第一反应就是“减面”。但说实话一个标准二次元角色的面数哪怕减到两万面对现代GPU来说也就是毛毛雨。真正让帧率崩掉的往往是那些看不见的隐藏账单。我见过太多项目模型面数不高但渲染一个角色要跑七八个Pass阴影还开了实时再加上一层bloom后处理结果角色一出场整个场景都跟着遭殃。要优化先得把账算明白。1.1 一个角色的渲染开销构成绝不是“一个Mesh”而已一个角色在屏幕上渲染一帧完整的开销链条是这样的CPU侧要更新动画骨骼、计算蒙皮矩阵GPU侧要把顶点做顶点变换、蒙皮权重计算然后光栅化到像素再逐像素跑一遍完整的片元着色器。这中间还要算上阴影深度写入、深度预处理、透明排序、特殊描边或者发丝透光的额外Pass。我常用一个生活化的类比来解释这事画一个人物网格是骨架材质是颜料Pass是你拿着笔在纸上重复描画的次数。骨架细了、颜料劣质了、描画次数太多出来的东西都不会好看而且越描越耗时间。具体到渲染性能上CPU和GPU的瓶颈经常是分开的必须分开看。1.2 骨骼蒙皮与顶点动画CPU和GPU的隐形杀手SkinnedMeshRenderer的更新是整个角色渲染里最容易被忽视的CPU开销点。它的流程是先按Animator驱动的骨骼层级逐节点算矩阵再把这些矩阵传给蒙皮计算最后按每个顶点的权重把骨骼矩阵作用到顶点上。移动端上顶点多、骨骼多、BlendShape多这三者叠加起来CPU的耗时就会非常难看。单独说BlendShape。表情系统用的就是它但每激活一个BlendShape每个相关顶点都要多做一次差值计算而且这个计算没法像骨骼蒙皮那样轻松丢给GPU加速。我现在的做法是移动端角色表情尽量控制在4到6个关键BlendShape剩下的微表情全部用贴图绘制或者换脸方案替代效果几乎无损性能差距却很大。1.3 材质Pass与Overdraw移动端最容易被忽略的账单移动端GPU的填充率Fill-rate是硬瓶颈尤其是高分辨率屏幕比如iPhone或者很多1440P安卓机。每个像素都要被片元着色器跑一遍如果角色身上压了多层半透明特效、刘海头发、披风边缘、粒子拖尾那同一块屏幕上可能要画几十遍这就是Overdraw。而Pass数则直接和DrawCall挂钩。一个角色如果有三个材质球身体、脸、头发每个材质又开了描边Pass、阴影Pass、自发光Pass那就是3×39次DrawCall。同屏五六个角色光是基础的人物DrawCall就四五十了再加上场景和UI轻轻松松破百。注意在移动端SetPass Call一般比DrawCall更贵。因为每次切换Shader状态GPU管线都要重新配置一遍。SRP Batcher能缓解的正是这个问题后面会细说。2. 从网格和骨骼下手减面不是唯一出路这一节讲角色资产侧的优化思路。很多人对资产优化的理解停留在“面数越少越好”但在角色身上面数和骨骼、BlendShape、材质布局是一个整体单纯减面往往撞上效果天花板该卡还是卡。2.1 网格减面与LOD的正确姿势给角色减面我做减法不是均匀地抽稀网格而是优先保住轮廓和表情区域。比如脸部、手指、发梢这些地方轮廓线一旦变形特别明显而身体大块平面和衣服褶皱贴图可以补掉不少细节就可以大胆减。LOD多细节层次方案我强烈建议从项目第一天就定下来后面补会很痛苦。给角色做三档就够了近距离用精细网格中距离用中等网格远距离直接换成简化版网格甚至公告板Impostor。这里有组实测数据供参考LOD档位面数顶点数距离范围说明LOD030000180000-8米立绘、剧情特写LOD11200070008-15米常规战斗视角LOD24000250015米以上远处小兵、路人LOD切换不能只看距离还要配合摄像机视野和遮挡关系。Unity的LODGroup组件里有个“Fade Transition Width”参数我习惯调到0因为交叉淡化期间会同时渲染两档网格反而白白增加一倍的顶点处理和DrawCall在移动端完全不划算。2.2 骨骼数量、BlendShape与GPU蒙皮的取舍骨骼数量直接决定蒙皮矩阵计算的次数。手游角色我建议骨架控制在60到80根以内很多项目动辄150根以上骨骼CPU开销肉眼可见地往上涨。像指甲、耳环这类细节骨骼美术效果上一米距离根本看不出来合并到相邻大骨骼上性价比更高。蒙皮计算在Unity里有CPU和GPU两种路径。默认情况下SkinnedMeshRenderer在CPU上做顶点变形但如果骨骼数量可控、顶点数适中完全可以在Quality设置里开启“GPU Skinning”选项让蒙皮计算走GPU能把宝贵的CPU时间还给动画和逻辑。开启GPU蒙皮后需要注意一点原来在CPU上访问顶点数据的代码比如行尸渲染、顶点偏移效果会失效需要改成在GPU侧通过材质属性传递。2.3 动画压缩与Animator层级优化Animation Clip的精度设置很影响包体和内存。我给项目定的规矩是骨骼Transform的压缩类型用“Optimal”浮点误差加上Position和Rotation曲线数量能删就删。一个角色几十个动画Clip压缩前和压缩后的内存差距可能是几倍。另外Animator Controller的层级不宜过深StateMachineBehaviours的OnAnimatorMove、OnStateEnter这些回调听着方便但每一个都在每帧CPU上走一遍。如果只是播个普通攻击、走路、待机完全不需要挂满回调。Animator的“Culling Mode”也要注意。默认的AlwaysAnimate会强制每一帧更新骨骼和动画哪怕角色已经出了屏幕。正确做法是选CullUpdateTransforms或者CullCompletely让Unity在角色不可见时跳过动画更新。这里踩过一个坑有些项目为了让角色在屏幕外也能保持动作同步比如联机对战直播强行开着AlwaysAnimate结果同屏几十个角色全都每帧更新骨骼CPU直接爆炸。针对这种需求正确解法是只在必要时同步关键帧数据而不是让所有角色都持续跑完整动画管线。3. Shader与材质层面的优化实战材质和Shader是人物渲染性能优化里最出效果也最容易翻车的部分。一个漂亮的二次元NPR角色在PC上可能跑得很欢放到中低端移动端就原形毕露。Shader的每一行指令最终都会转化成GPU上的开销而且这个开销是乘以覆盖像素数的。3.1 Shader变体裁剪把编辑器里“看着没事”的隐患清掉Unity的Shader变体机制设计的初衷是灵活但用不好就是个性能黑洞。一套角色Shader如果你保留了雾效、光照贴图、实时阴影、方向光、点光、聚光灯、HDR……这些multi_compile关键词Build的时候会自动生成成百上千个变体。变体多带来的后果有三个方面包体膨胀、Shader.Parse和CreateGPUProgram时的加载卡顿、运行时切换材质参数时GPU Program切换变慢。实际项目里最常见的现象是编辑器里一切正常打出来的包在低端机上一进战斗就卡一下打开Profiler一看一个CreateGPUProgram事件吃掉几百毫秒这就是Shader变体太多导致的。减少变体有几个手段一是把无关的关键词从Shader里删掉比如移动端根本不用点光源逐像素那就去掉相关的multi_compile二是做ShaderVariantCollection把构建时用到的变体提前预热并打进包三是用Unity的“Strip Unused Shader Variants”配合“Strip Engine Code”但这条要谨慎裁剪过头了之后角色可能直接变粉色或者黑块后面我会讲怎么排查。3.2 光照模型简化与半透明处理二次元NPR渲染是现在Unity角色渲染的主流方向但皮肤、头发、衣服全走实时PBR是不可能的。我见过一个很典型的错误方案角色所有材质都用同一个包含GGX高光、次表面散射、多层法线细节的标准PBR Shader结果整角色渲染耗时就占了帧预算的三分之一。正确的移动端NPR优化路径是降低光照模型复杂度。皮肤和衣服的高光可以用Ramp贴图查表实现头发用各向异性高光两层Kajiya-Kay近似脸部干脆用MatCap贴图模拟环境光所有这些方案打底都是4到8条ALU指令比一套完整PBR少两个数量级。半透明方面角色头发的自遮挡、纱裙、特效贴脸这些Case都很容易引发Overdraw翻倍。我的经验是头发尽量用Alpha Test配合深度写入纱裙用半透明叠加但限制层数特效叠加到角色上时优先用“屏幕空间透明度裁剪”而不是“全屏半透明模糊”。这里有个通用原则——能不依赖帧缓冲和混合的尽量靠剔除和贴图Alpha解决。3.3 SRP Batcher与GPU Instancing的正确使用SRP Batcher是URP/HDRP里默认的批处理方案核心思路是在GPU端常驻一份材质属性缓冲区切换材质不需要重新上传所有属性大幅降低SetPass Call。注意SRP Batcher只在URP/HDRP里生效内置渲染管线是没有的。如果你的项目用的是旧管线但又想让批量渲染效率提升可以考虑整体迁到URP这本身也是一次渲染性能优化的大机会。GPU Instancing则适合同屏大量相同角色的情况比如竞技场、割草类游戏里的NPC或是掉帧的重灾区——街道上一堆路人。Instancing的前提是这些角色除了部分参数比如颜色、胖瘦不同网格和Shader必须一致。颜色差异化不用新建材质用MaterialPropertyBlock就够否则材质数量会变成模型数量的平方往上涨。注意区分两类批量渲染SRP Batcher处理的是一批不同材质但相同Shader的物体重点是减少渲染状态切换GPU Instancing处理的是一批相同网格相同材质的物体重点是合批顶点数据别在项目里混用。4. 阴影、摄像机与渲染管线的联动优化人物渲染从来不是孤立的它和整个场景的光照、摄像机、后处理链路紧密耦合。很多时候单独优化角色本身还不够因为阴影、多摄像机、后处理这些全局设置才是压垮帧率的最后一根稻草。4.1 阴影设置怎么调才不“糊”又不“贵”实时阴影可能是角色渲染里性价比最低的开销之一。Unity的实时阴影会把所有被标记为阴影投射体的角色模型用Shadow Caster Pass再画一到两遍然后每个接收阴影的像素都要做一遍阴影贴图采样。一个角色就翻倍了DrawCall和顶点处理再乘以同屏角色数直接爆炸。Cascade Shadow的级联数越多阴影贴图渲染的Payload越大在移动端我比较推荐2 Cascade。移动端实战方案我一般用这三板斧。 第一阴影距离Shadow Distance设置为10到15米再远就断掉。 第二“边缘阴影”用平面阴影或者胶囊体阴影模拟就是对角色脚下投一张模糊的圆形或胶囊形贴图跟着角色走。立体感和地面贴合度远超实时阴影GPU开销几乎为零。 第三NPR项目里可以用“动态假阴影”按阳光方向旋转和拉伸假阴影贴图视觉效果不输实时阴影性能差距是几个量级。如果真要在移动端保留实时阴影那就把Normal Bias和Shadow Bias仔细调一调否则很容易出现阴影闪烁和“斑点脸”效应。4.2 多摄像机、RenderTexture与后处理的性能陷阱“主角特写镜头”“UI上放个角色模型”“小地图上显示人物”——这些看似贴心的功能全是性能陷阱。每个Camera都意味着场景被重新完整渲染一遍。如果你放了三台摄像机同一个角色就被画了三遍就问你怕不怕。我遇到过一个大聪明项目在UI界面上显示“角色更衣室”直接在UI面板上放了一个RenderTexture摄像机RenderTexture分辨率设置成和屏幕一样大渲染整个场景。结果是主界面帧率从60掉到了20。你说这个摄像机做错了吗方向对了但两个问题一是RenderTexture分辨率应该对着模型实际占据的UI尺寸来面板只有300×600那就用300×600的RT二是这个相机层的CullingMask要精简只渲染角色所在的层别把场景背景和特效也一块儿画进去。URP现在支持Camera StackingBase Camera负责主场景Overlay Camera只把角色画进屏幕特定区域能减少不少重复渲染。后处理方面Bloom的迭代次数、径向模糊范围、色散强度每一项都在成全屏遍历像素。角色特写时如果需要边缘光或者描边尽量用角色材质自带的“深度边缘检测”来实现不要每次都用全屏后处理再叠加一遍。4.3 移动端发热降频的根本解法移动端性能优化的终极目标是功耗不是帧率。一个机器跑60帧但机身烫手半小时后降频掉到30帧体验比稳定45帧还差。功耗和渲染耗时强相关所以盯着GPU耗时看还不够还要看芯片的发热设计。在真机上GPU持续高负载超过两分钟左右就会触发降频保护这也是为什么很多游戏刚开打流畅、打一会就卡顿的原因。从渲染侧控制功耗的手段我整理过一套优先级优先降分辨率URP开了HDR之后用RenderScale调到0.9甚至0.8绝大多数玩家肉眼看不出区别但GPU负载直线下降然后降阴影把实时阴影范围压到最低最后再降效果Bloom和MSAA二选一。要记住一个经验公式GPU负载 ≈ 像素数 × 着色器复杂度 × 余量分辨率永远排第一位。5. 用Profiler和数据说话一次完整的性能排查实录前面讲的是原理和方案但落到具体项目光凭感觉优化是不行的。必须用Profiler和数据说话。我整理了一套在多人角色同屏项目里反复验证过的排查路径照着走基本不会漏掉大头。5.1 从Stats窗口到Frame Debugger的排查路径第一步是打开Game视图右上角的Stats先看DrawCall、SetPass Call和Triangles三个数。如果DrawCall几十个但Triangles才几万面那问题多半在材质Pass和批处理没生效上如果Triangles几百万面球就出在网格LOD上减面都来不及。第二步是Profiler的CPU Usage模块。重点看PlayerLoop里的Animator.Update、SkinnedMeshRenderer.Update、脚本Update和渲染。角色多的时候Animator.Update和SkinnedMeshRenderer.Update会非常显眼这时候就是骨骼和蒙皮的锅。第三步是Frame Debugger。它能逐DrawCall列出渲染的每一个事件你可以按“按对象排序”查看同一个角色是不是被画了好几次也可以看到阴影Pass是不是占了大量DrawCall。最后一步是GPU端的时间移动端建议直接在真机上跑Profiler的GPU模块PC上的数值参考意义有限。真机测试环境要注意开启开发者模式的90Hz刷新率、关掉动态分辨率调整这样测出来的数据才具有可比性。5.2 实战案例一个二次元角色的优化前后对比下面用一个实际项目的角色优化案例来做复盘。背景是二次元手游战斗场景同屏最多8个角色每帧都有技能特效摄像机带Bloom后处理。优化前的情况是这样的角色使用PBR标准Shader三个材质球身体、脸、头发其中头发开启了透写Alpha混合皮肤有透皮次表面散射实时阴影开到20米4 Cascade开启Bloom和4x MSAA动画用AlwaysAnimateBlendShape挂了12个。测出来的数据相当难看DrawCall 46SetPass 39Triangles 180KCPU主线程耗时34msGPU耗时42ms帧率长时间在23到26徘徊机身温度高得能煎蛋。再看优化后我做了这些动作优化项优化前优化后角色材质PBR标准Shader × 3URP自定义NPR Ramp Shader × 2身体脸共用一个头发透写Alpha BlendAlpha Test 深度写入阴影方案实时阴影20米/4 Cascade关闭实时阴影改用脚下假阴影蒙皮方案CPU SkinningGPU Skinning动画更新AlwaysAnimateCullCompletelyBlendShape12个活动表情4个关键表情其余用换脸贴图后处理Bloom 4x MSAA仅BloomRenderScale 0.85批处理无SRP Batcher URP优化后的数据DrawCall 13SetPass 9Triangles 78KCPU耗时降到8msGPU耗时降到12ms帧率稳定60机身温度恢复正常水平。效果上角色虽然少了一些PBR的真实感但二次元风格下观感反而更干净利落美术对比后直接点头。这个Case说明只要从上文说的几个维度下手角色渲染性能的优化空间是很大的而且永远先处理最大的那块瓶颈再逐步收剩下的。6. 常见问题与避坑清单这部分是踩坑记录合集。我把过去几年在Unity角色优化上反复遇到的高频问题整理成速查表每个问题都附带排查思路和解决方案。很多坑是网上教程不会写的但实际项目中绕不过去。6.1 阴影闪烁、边缘黑线、阴影突然消失这几个看着是不同问题其实主线都在阴影参数上。阴影闪烁多半是Shadow Bias里的Bias设太小或者Near Plane设得太近导致Z-Fighting。边缘黑线多是Normal Bias太小让阴影采样在物体边缘产生了自遮蔽移动端GPU精度差的情况下更容易出现。阴影突然消失要么是Shadow Distance被调小了要么是角色进入了Shadow Cascade的覆盖范围外。还有一个NPR项目特有的坑实时阴影在卡通角色上特别容易破坏“赛璐璐感”阴影边界生硬又跳动。我的最终解法如前所述就是移动端直接关掉实时阴影改用脚下的贴图假阴影或胶囊体阴影。立绘、剧情演出需要高质量阴影的时候再单独用一台平行光摄像机配高分辨率阴影贴图专门照射主角。6.2 角色变粉、材质丢失、Shader变体被裁剪过头Unity引擎的“默认材质变粉色”是每个优化过的项目都会遭遇的噩梦。最常见的两个原因一是ShaderVariantCollection没做全运行时切到没有集齐的变体Unity找不到Program直接回退到粉色二是Build里开了Aggressive GPU Stripping或者Strip Unused Variants把某些变体误杀了Android平台尤其频繁因为图形API不同导致驱动对Shader能力的要求不同。规避方案是三步。第一步所有角色Shader在BuildSettings里人工加入ShaderVariantCollection涵盖所有变体第二步Stripping选项从Aggressive改回Conservative宁可包体大一点别拿稳定性赌第三步打出来的包在真机上用覆盖安装的方式全流程跑完所有角色和技能看到粉色立刻结合Frame Debugger和Logcat抓当时的Shader名称和关键词回编辑器反查。6.3 动画数据导致的包体和内存膨胀动画进阶项目里Animation Clip绕不开内存问题。重复的Clip、未压缩的Curve、每个Clip里挂了一堆无用曲线这些垃圾攒下来光动画文件就能吃掉几十兆包体和内存。在Addressables加载方式下一个角色A全部动画资源启动时全量加载场景只用到其中3个Clip白白增加加载时长。我的做法是动画文件入包之前统一做一次压缩检查Position/Rotation/Scale用Optimal压缩把每个角色的动画拆分到单独的AssetBundle或者Addressable Group里按需加载动画Clip里除了必要参数比如表情BlendShape权重和根运动曲线其他曲线全部删光。给个直观数字一个角色120个动画Clip压缩并裁剪之前大约是38MB压缩裁剪之后是11MB内存占用降低了将近七成。6.4 实际项目中容易反复踩的几个坑材质球爆炸是最容易犯的错误。给每个角色的每个部位都创建独立材质球方便美术调色换装直接导致Content内存和加载路径爆炸。解决思路是用共享材质MaterialPropertyBlock把颜色、Skin Tone这些差异通过Per-Renderer数据传进去既保留了美术自由度又不会生成上百个材质实例。第二个是模型更新时机的坑。SkinnedMeshRenderer更新是独立于渲染的哪怕角色在屏幕外只要Animator还在更新蒙皮也在算。我在一个项目里排查出一个角色明明在摄像机背后看不见CPU上还挂着1.2ms的蒙皮更新这就是没开Culling的代价。第三个是同屏大量角色时GameObject级别的Camera culling不够精细。比如竞技场里20个角色10个在屏幕边缘略出界BoundingVolume还在摄像机视锥里面全部渲染。可以自己写一层更细的视锥剔除或者用Occlusion Culling的遮挡剔除把被主角色挡住的NPC直接跳过去不渲染。第四个坑是后处理关闭时序。很多项目做优化时只想着关Bloom、关MSAA但忘了后处理Volume里的其他组件比如色差、噪点、景深这些都在同一张全屏RT上叠加循环。只要一个忘关帧率账单上就多了一笔。我个人在多个项目里打磨下来最深的一点体会是人物渲染的性能优化没有银弹它是一整套“从资产制作到渲染管线搭配”的系统工程。美术做减面骨骼优化TA管Shader变体和材质程序管动画Culling和渲染批处理三边协作才能在保住画质的同时换来稳定帧率。最后再分享一个小技巧项目开启时就给人物渲染做一个性能预算表。把同屏角色数、最大面数、最大骨骼数、DrawCall配额、Pixel Overdraw配额、CPU/GPU帧预算都列清楚每次接入新角色、加新特效都拿预算表过一遍。这个方法看起来笨但实际执行下来极其有用很多性能问题在设计阶段就被拦住了根本不会拖到优化阶段去救火。如果你正在做Unity角色渲染项目建议从今天开始就按这套思路过一遍自己的管线收获应该不会小。
返回列表