1. 人物渲染为什么成了性能黑洞做过手游的人大概都有过这种体验场景跑满60帧稳如老狗一进角色展示界面或者多人同屏战斗帧率直接腰斩手机背面烫得能煎鸡蛋。这不是错觉人物渲染在移动端就是妥妥的性能黑洞。我经手过好几个项目性能瓶颈追到最后十有八九都落在角色身上——不是面数爆炸就是材质太复杂要么就是骨骼和蒙皮计算把CPU拖垮了。先把这个问题的本质说清楚。人物渲染和场景渲染最大的区别在于场景是静态的、可合批的、可烘焙的人物是动态的、骨骼驱动的、材质多变的。一个场景里的建筑、地形、道具美术做完之后基本就定型了该合并合并该烘焙烘焙DrawCall能压到很低。但人物不行人物有骨骼动画每一帧顶点位置都在变SkinnedMeshRenderer天然就很难合批。再加上人物往往需要更精细的材质表现——皮肤、头发、布料、金属配件每种材质可能都要单独的Shader和贴图DrawCall直接起飞。更麻烦的是人物往往是玩家视线的焦点。你可以在场景里用LOD把远处的树砍成面片但主角站在屏幕正中央你敢给他降精度吗玩家一眼就能看出来。所以人物渲染优化比场景优化更棘手它要在视觉质量和运行效率之间走钢丝。这篇文章面向的是有一定Unity基础、正在做移动端项目、被角色性能问题折磨过的开发者。我会从渲染管线、骨骼计算、材质策略、LOD方案、Shader优化几个维度把人物渲染性能优化这件事拆开揉碎讲清楚。每个方案我都会说明为什么这么做、什么场景下适用、有什么坑。你不需要全部照搬但至少能建立起一套完整的排查和优化思路。2. 先搞清楚瓶颈在哪性能分析的正确姿势2.1 用Profiler定位问题源头优化最忌讳的就是凭感觉猜。我见过太多人一上来就说肯定是面数太高然后疯狂减面结果帧率纹丝不动。人物渲染的性能问题可能出在CPU端也可能出在GPU端定位错了方向后面所有工作都是白费。Unity自带的Profiler是最基础也最有效的工具。打开Window Analysis Profiler连上真机跑一遍重点看几个指标CPU Usage里的Rendering和Scripts如果Rendering占比很高说明DrawCall或者批处理有问题如果Scripts里Animator.Update或者MeshSkinning.Update很突出说明骨骼动画是瓶颈。GPU Usage如果GPU耗时远超CPU那问题在像素处理或者顶点处理上通常是Shader太复杂或者Overdraw严重。Rendering区域看SetPass Calls和Batches的数量。移动端上SetPass Calls超过100就要警惕了超过200基本就是灾难。真机Profiler的连接方式这里不展开但有一点要提醒一定要用Development Build加上Autoconnect Profiler不要用Editor里的数据做判断。Editor的渲染路径和真机差别很大Editor里跑60帧不代表真机能跑30帧。2.2 人物渲染的三大性能杀手根据我的经验人物渲染的性能问题基本可以归到三类第一类是DrawCall过多。一个角色如果有10个材质槽——皮肤、眼睛、头发、上衣、裤子、鞋子、配饰、武器……每个材质槽至少一个DrawCall10个角色同屏就是100个DrawCall。再加上阴影Pass直接翻倍。移动端GPU对DrawCall非常敏感因为每次DrawCall都有状态切换的开销。第二类是骨骼计算过重。SkinnedMeshRenderer的骨骼蒙皮计算默认在CPU上做骨骼数量越多、顶点数越多CPU开销越大。一个标准的人形角色大概有50-70根骨骼如果加上手指、面部表情骨骼轻松超过100根。每根骨骼的矩阵计算、蒙皮顶点的权重混合都是实打实的CPU时间。第三类是Shader和Overdraw。人物材质往往用了比较复杂的Shader——PBR、各向异性头发、次表面散射皮肤。这些在PC上没问题在移动端就是性能炸弹。再加上半透明材质头发、裙子、特效的Overdraw同一个像素可能被画了五六次。2.3 建立性能基线在开始优化之前先建立一个可量化的基线。我的做法是找一个代表性的战斗场景固定角色数量和技能特效。在目标机型上跑5分钟记录平均帧率、最低帧率、CPU耗时、GPU耗时、内存占用。每次优化后重复同样的测试对比数据。没有基线的优化就是盲人摸象。你改了一个参数感觉好像流畅了一点但实际可能只是波动。只有数据才能告诉你优化是否真的有效。注意测试时一定要关闭Editor的VSync用固定帧率或者不限帧率模式跑否则帧率被锁在60你根本看不出性能余量。3. 骨骼与蒙皮CPU端优化的主战场3.1 骨骼数量控制与LOD策略骨骼数量直接决定了蒙皮计算的开销。一个标准的人形Mecanim Avatar大概有55根骨骼这是Unity内置的标准。但实际项目中美术往往会给角色加上大量细节骨骼——每根手指3根、面部表情几十根、裙摆飘带若干根。骨骼数量翻倍蒙皮计算量基本也翻倍。我的建议是分档控制角色类型建议骨骼数说明主角近距离展示80-120可以有手指和基础表情主角战斗场景60-80砍掉手指细节保留主要关节NPC/小怪30-50只保留必要关节远景角色15-25可以用更简单的骨骼结构具体怎么砍最直接的办法是在DCC工具里合并骨骼。比如手指的3根骨骼合并成1根脚趾骨骼直接删掉面部表情骨骼在战斗场景中禁用。这些操作在美术制作阶段就要规划好不要等模型都做完了再返工。另一个思路是用骨骼LOD。Unity本身不直接支持骨骼LOD但你可以通过代码控制。比如距离摄像机超过10米的角色把Animator的更新模式改成AnimatePhysics或者直接CullUpdateTransforms只更新位置不更新骨骼变换。再远一点直接CullCompletely完全停止动画更新。// 简单的骨骼LOD控制示例 public class BoneLOD : MonoBehaviour { public float nearDistance 10f; public float farDistance 20f; private Animator animator; private Transform cam; void Start() { animator GetComponentAnimator(); cam Camera.main.transform; } void Update() { float dist Vector3.Distance(transform.position, cam.position); if (dist nearDistance) { animator.cullingMode AnimatorCullingMode.AlwaysAnimate; } else if (dist farDistance) { animator.cullingMode AnimatorCullingMode.CullUpdateTransforms; } else { animator.cullingMode AnimatorCullingMode.CullCompletely; } } }这段代码的逻辑很简单近距离全量更新中距离只更新Transform不计算蒙皮远距离完全停止。实测下来在20个角色的场景里这个策略能省下30%左右的CPU时间。3.2 GPU Skinning的取舍Unity有一个选项叫GPU Skinning在Player Settings里可以开启。开启后蒙皮计算从CPU转移到GPUCPU开销大幅降低。听起来很美好但有几个坑要注意第一GPU Skinning需要Shader支持。如果你的角色Shader是自己写的需要确保它正确处理了GPU蒙皮的数据结构。Unity内置的Standard Shader是支持的但很多自定义Shader不一定。第二GPU Skinning会增加显存占用和带宽消耗。骨骼矩阵要传到GPU每帧都要传对于骨骼数量多的角色这个数据传输量不小。第三GPU Skinning在低端机上不一定更快。有些老款GPU的顶点着色器性能有限把蒙皮计算丢过去反而更慢。我的经验是中高端机开GPU Skinning低端机关掉。或者更精细一点根据设备等级动态切换。Unity的SystemInfo.graphicsDeviceType和SystemInfo.processorCount可以帮你做简单的设备分级。3.3 蒙皮网格的合并与优化SkinnedMeshRenderer有个很烦人的特性每个Renderer至少一个DrawCall而且很难合批。但如果你的人物模型是分部件制作的头、身体、手脚分开可以考虑合并成一个SkinnedMeshRenderer。合并的方法有两种一种是在DCC工具里直接合并成一个网格另一种是用Unity的CombineInstance在运行时合并。前者更彻底后者更灵活。合并的好处是减少DrawCall坏处是失去了部件级别的可见性控制。比如你想单独隐藏角色的帽子合并后就做不到了。所以合并策略要根据实际需求来定。还有一个细节Mesh的顶点属性。Unity默认的SkinnedMeshRenderer会包含Position、Normal、Tangent、UV0、UV1、Color、BoneWeights等属性。如果你的Shader不需要Tangent或者UV1可以在导入设置里把这些属性去掉能省不少内存和带宽。4. 材质与ShaderGPU端优化的核心4.1 材质槽合并与纹理图集一个角色10个材质槽就是10个DrawCall。如果能合并到3-4个性能提升立竿见影。合并的核心思路是纹理图集把不同部件的贴图打包到一张大图上然后用同一个材质渲染。具体操作把角色的所有贴图——皮肤、衣服、头发、配饰——按分辨率需求打包到一张2048x2048或者4096x4096的图集里。在DCC工具里重新映射UV让每个部件对应图集上的不同区域。用一个材质渲染整个角色Shader里根据UV区域采样不同的贴图。这样做的好处是DrawCall从10降到1坏处是图集分辨率有限每个部件的贴图精度会下降。所以图集方案适合中低精度角色主角如果要求高精度贴图可能还是需要分材质。另一个折中方案是部分合并把皮肤和衣服合并到一个材质头发单独一个配饰单独一个。这样DrawCall从10降到3-4贴图精度也能接受。4.2 移动端Shader的简化策略移动端GPU的算力和带宽都有限PC上跑得好好的Shader直接搬到手机上大概率会翻车。人物Shader的简化我一般从这几个方面入手第一减少光照计算。移动端不要用多光源Forward渲染尽量用单方向光加烘焙。如果必须有多光源用Vertex Light或者SH Light。PBR材质在移动端可以用简化版的BRDF比如把GGX换成Blinn-Phong视觉差异不大但计算量小很多。第二砍掉不必要的通道。很多角色Shader带了Detail Map、Rim Light、Emission、MatCap等多个通道每个通道都是一次额外的纹理采样和计算。根据实际需求保留必要的砍掉锦上添花的。第三控制纹理采样次数。移动端GPU的纹理采样单元有限一个Shader里采样超过4-5次就要警惕了。能合并的贴图合并比如把AO、Metallic、Roughness打包到一张图的RGB通道能预计算的预计算。第四慎用半透明。头发、裙子、翅膀这些半透明材质Overdraw非常严重。如果一定要用尽量控制半透明区域的面积或者用Alpha Test代替Alpha Blend。Alpha Test虽然边缘硬一点但没有Overdraw问题。4.3 头发渲染的专项优化头发是人物渲染里最头疼的部分。真实的头发需要各向异性高光、半透明散射、多层叠加移动端根本扛不住。我的优化方案是用面片代替发丝不要用几千根发丝模型用几十个面片加透明贴图。面片数量控制在20-30个以内。用Kajiya-Kay模型代替各向异性BRDFKajiya-Kay是专门为头发设计的简化光照模型计算量小效果也不错。深度写入开启头发面片之间会有排序问题开启深度写入可以避免大部分错误排序但要注意半透明边缘的处理。LOD分级近距离用完整头发模型中距离用简化版远距离直接用一个球体或者面片代替。我做过一个测试一个角色从3000根发丝模型优化到25个面片GPU耗时从8ms降到1.5ms视觉上在手机屏幕上几乎看不出差别。5. LOD与剔除让不该渲染的东西消失5.1 人物LOD的分级策略LOD是人物渲染优化里性价比最高的手段之一。一个角色在屏幕上的像素面积越小能省的细节就越多。我的LOD分级一般是这样的LOD级别距离范围面数骨骼材质阴影LOD00-5米100%全量全材质实时阴影LOD15-15米60%砍手指合并材质实时阴影LOD215-30米30%砍手指脚趾单材质关闭阴影LOD330米10%20根以内单材质无光照关闭Unity的LOD Group组件可以很方便地配置这些。但要注意LOD的切换距离要根据实际屏幕占比来调不要死板地按世界距离。一个角色在21:9的宽屏上和4:3的屏幕上同样距离的屏幕占比是不一样的。5.2 遮挡剔除与视锥剔除Unity自带的视锥剔除和遮挡剔除对人物同样有效。但人物是动态的遮挡剔除的烘焙数据不包含人物所以人物之间的遮挡关系需要额外处理。一个实用的技巧是用简单的包围盒做遮挡判断。在角色管理器里维护一个角色列表每帧检查每个角色是否被其他角色或者场景物体遮挡。如果被完全遮挡直接关掉Renderer。// 简单的角色可见性检查 public class CharacterVisibility : MonoBehaviour { private Renderer[] renderers; private static ListCharacterVisibility allCharacters new ListCharacterVisibility(); void OnEnable() { allCharacters.Add(this); } void OnDisable() { allCharacters.Remove(this); } void Start() { renderers GetComponentsInChildrenRenderer(); } void LateUpdate() { // 检查是否在摄像机视锥内 Plane[] planes GeometryUtility.CalculateFrustumPlanes(Camera.main); Bounds bounds new Bounds(transform.position, Vector3.one * 2f); bool visible GeometryUtility.TestPlanesAABB(planes, bounds); foreach (var r in renderers) { r.enabled visible; } } }这个方案对同屏角色数量多的场景特别有效。实测在50个角色的场景里能省下40%以上的渲染开销。5.3 阴影的取舍实时阴影是性能大户。一个角色如果开启实时阴影相当于要渲染两遍——一遍正常渲染一遍阴影贴图。对于人物渲染我的建议是主角开实时阴影但阴影分辨率不要太高1024x1024足够了。NPC和小怪用烘焙阴影或者假阴影一个简单的圆形面片贴在脚下。远景角色直接关阴影玩家根本注意不到。还有一个技巧是阴影LOD近距离用实时阴影中距离用烘焙阴影远距离关阴影。Unity的Quality Settings里可以配置阴影距离但更精细的控制需要自己写代码。6. 动画系统与更新频率的调优6.1 Animator的更新模式选择Unity的Animator有三种更新模式Normal、AnimatePhysics、UnscaledTime。对于人物渲染性能来说更重要的是Culling ModeAlwaysAnimate始终更新最耗性能。CullUpdateTransforms不可见时只更新Transform不计算蒙皮。CullCompletely不可见时完全停止。默认是CullUpdateTransforms但很多项目为了保险起见改成了AlwaysAnimate这是不必要的浪费。我的建议是主角用AlwaysAnimate因为可能随时出现在屏幕里NPC和小怪用CullUpdateTransforms远景角色用CullCompletely。6.2 动画帧率与采样优化动画的采样频率也会影响性能。Unity默认的动画采样率是60FPS但很多动画其实不需要这么高的精度。你可以在动画导入设置里把采样率降到30FPS甚至15FPS视觉上差别不大但计算量减半。还有一个技巧是动画压缩。Unity的动画压缩可以去掉冗余的关键帧减少动画数据的大小和计算量。在动画导入设置里把Anim. Compression设为OptimalRotation Error和Position Error根据实际需求调整。我一般设成0.5和0.5大部分动画看不出差别。6.3 骨骼更新的按需计算不是所有骨骼都需要每帧更新。比如角色的手指骨骼在战斗场景中可能根本不需要动。你可以通过代码控制Animator的骨骼更新// 禁用不需要的骨骼更新 public class BoneUpdateOptimizer : MonoBehaviour { public HumanBodyBones[] bonesToDisable; private Animator animator; void Start() { animator GetComponentAnimator(); foreach (var bone in bonesToDisable) { var t animator.GetBoneTransform(bone); if (t ! null) { // 把骨骼从Animator的更新列表中移除 // 注意这需要配合自定义的动画系统 } } } }更彻底的做法是用Playable API代替Animator。Playable API可以精确控制哪些骨骼参与动画计算哪些不参与。对于只需要上半身动画的角色比如坐着说话的NPC可以只更新上半身骨骼下半身完全不动。7. 实战案例从30帧到60帧的优化过程7.1 项目背景与初始状态去年我接手了一个二次元风格的手游项目战斗场景10个角色同屏目标机型是骁龙660级别的中端机。初始状态下战斗场景平均帧率只有28-32帧团战的时候掉到20帧以下。Profiler数据显示CPU耗时22msGPU耗时18ms其中SkinnedMeshRenderer的蒙皮计算占了8msDrawCall数量达到180。7.2 优化步骤与效果对比我按照优先级分了三轮优化第一轮DrawCall合并与材质简化把每个角色的10个材质槽合并到3个皮肤衣服、头发、配饰。头发从发丝模型改成面片模型。关闭了不必要的Detail Map和Rim Light通道。效果DrawCall从180降到65GPU耗时从18ms降到12ms帧率提升到38-42帧。第二轮骨骼与蒙皮优化骨骼数量从平均95根降到65根砍掉手指和面部细节。开启GPU Skinning。配置骨骼LOD远景角色停止动画更新。效果CPU耗时从22ms降到14ms帧率提升到48-52帧。第三轮LOD与剔除优化配置4级LOD远景角色面数降到10%。实现角色可见性检查被遮挡的角色直接关闭Renderer。阴影距离从50米降到20米远景角色用假阴影。效果DrawCall进一步降到40左右GPU耗时降到8ms帧率稳定在58-62帧。7.3 关键决策的思考过程这三轮优化里有几个决策点值得展开说为什么先做DrawCall合并而不是骨骼优化因为DrawCall是GPU端的瓶颈骨骼是CPU端的瓶颈。初始状态下GPU耗时18ms已经接近临界值先解决GPU问题能让帧率快速提升给后续优化争取空间。为什么选择GPU Skinning而不是继续砍骨骼骨骼已经砍到65根再砍会影响动画质量。GPU Skinning能把蒙皮计算从CPU转移到GPU而GPU此时已经有余量了DrawCall合并后GPU耗时降到12ms。为什么LOD放在最后LOD配置需要美术配合制作不同精度的模型周期比较长。前两轮优化不需要改模型可以快速见效。等前两轮做完帧率已经到50帧左右再用LOD补足最后的差距。8. 常见问题与排查技巧实录8.1 人物渲染性能问题速查表现象可能原因排查方法解决方案帧率低但GPU耗时不高CPU端骨骼计算过重Profiler看MeshSkinning.Update减骨骼、开GPU SkinningGPU耗时高但DrawCall不多Shader太复杂或Overdraw严重用Frame Debugger看渲染顺序简化Shader、控制半透明DrawCall数量异常高材质槽过多或批处理失败Frame Debugger看每个DrawCall合并材质、纹理图集团战时帧率骤降同屏角色过多统计同屏角色数量LOD、剔除、限制同屏人数角色边缘闪烁Z-Fighting或LOD切换问题检查LOD距离和深度设置调整LOD距离、开启深度写入阴影边缘锯齿严重阴影分辨率不足检查阴影贴图分辨率提高分辨率或改用软阴影8.2 那些文档里不会写的坑坑一GPU Skinning和BlendShape冲突。如果你的角色用了BlendShape做表情开启GPU Skinning后BlendShape可能会失效。这是因为BlendShape的计算也在CPU端和GPU Skinning的流程有冲突。解决方案是表情用骨骼驱动或者表情和GPU Skinning二选一。坑二纹理图集的UV接缝问题。把不同部件的贴图打包到图集后UV接缝处可能会出现颜色渗漏。这是因为纹理采样的双线性插值会采样到相邻区域的像素。解决方案是给每个区域留2-4像素的Padding或者在Shader里用Point采样代替Bilinear。坑三LOD切换时的跳变。LOD模型之间的面数和材质差异太大切换时会有明显的跳变。解决方案是让相邻LOD的面数差异不要超过50%材质尽量保持一致或者用CrossFade过渡。坑四Animator的Culling Mode在Editor里不生效。Editor里Animator的Culling Mode可能不会按预期工作必须在真机上测试。我踩过这个坑Editor里看着没问题真机上角色动画全停了。坑五移动端的纹理压缩格式。不同GPU厂商支持的压缩格式不一样ASTC虽然通用但压缩率不如ETC2。人物贴图建议用ASTC 6x6或者ETC2根据目标机型选择。纹理格式选错显存占用可能翻倍。8.3 性能优化的优先级原则最后分享一个我总结的优先级原则先GPU后CPU先批量后单体先远处后近处。先GPU后CPU是因为GPU瓶颈通常更容易定位和解决DrawCall、Shader、Overdraw而且GPU优化往往能快速见效。先批量后单体是因为批量优化材质合并、LOD的收益比单个角色的精细优化大得多。先远处后近处是因为远处角色的优化空间更大对视觉质量的影响更小。这个原则不是绝对的具体项目要根据Profiler数据来定。但如果你不知道从哪里下手按这个顺序走大概率不会错。提示优化过程中一定要持续测试每改一个参数就跑一遍Profiler。不要一次性改一堆东西否则出了问题你都不知道是哪个改动导致的。9. 写在最后的一些个人体会人物渲染优化这件事说到底是在有限预算下做取舍。你不可能让手机跑出PC的效果但你可以让玩家在手机上看到足够好的画面。我见过太多项目在画质上死磕结果帧率崩了玩家流失了画质再好也没人看。我的经验是帧率稳定比画质极致更重要。玩家能接受画面稍微糙一点但绝对不能接受卡顿。所以优化的第一目标是保帧率第二目标才是提画质。在这个前提下该砍的细节就砍该降的精度就降不要舍不得。还有一个体会是优化要趁早。不要等游戏做完了再优化那时候改动的成本太高了。在美术制作阶段就把骨骼数量、材质数量、贴图规格定好在程序开发阶段就把LOD、剔除、Shader简化的框架搭好。后期只需要调参数不需要大改。最后多和美术沟通。很多性能问题是美术制作习惯导致的——面数超标、材质槽过多、贴图分辨率过大。程序单方面优化不如从源头控制。我现在的习惯是在项目启动阶段就和美术定好规范角色面数上限、骨骼数量上限、材质槽数量上限、贴图分辨率上限。有了规范后面的事情就好办多了。
阅读完成 · 觉得有帮助?