上一款项目上线前,美术同事把主角的模型精度提了一档,衣服褶皱、头发丝、武器纹路全部加了细节,PC端预览没觉得有什么问题,一旦打到中端安卓机上,同屏只要出现五六个角色,帧率就开始跳崖。我第一反应是Draw Call太多了,打开Profiler一看,主线程反而没爆,真正吃掉帧预算的是阴影、Shader片元开销、骨骼动画和蒙皮更新叠在一起。那次之后,我基本把Unity人物渲染性能优化从里到外踩了个遍,这篇就是把当时实测过、排过坑的做法整理出来,给正在优化角色渲染、尤其是移动端项目的朋友一套可以照着做的排查和优化顺序。
1. 角色渲染卡顿的第一现场:先把瓶颈定位到CPU还是GPU
优化角色渲染最忌讳的事情是上来就改参数。改了半天,可能真正的问题在同一个方向之外。正确做法是先用工具把瓶颈定性,再做针对性处理。
1.1 三件套:Profiler、Frame Debugger与真机数据
Unity项目里做性能分析,绕不开三个东西。
第一个是Profiler窗口。先把CPU和GPU两条线分开看。角色渲染相关的常见计时项包括PlayerLoop里的Animation、SkinnedMeshSkinning,以及Renderer相关的SetPass Calls等。如果主线程的Animation和蒙皮耗时明显,说明动画系统和CPU蒙皮是主要瓶颈。如果CPU里没爆,但GPU那一栏长时间占满,就要去查Shader复杂度和渲染数量。
第二个是Frame Debugger。它能逐事件回放一帧的渲染过程,清楚看到这个角色一共被画了几次,每个Draw都用了哪个ShaderPass、哪个Mesh、哪个材质。很多角色看起来只画了一遍,实际上因为阴影、深度、透明、额外光照,会被多画好几遍,这些在Frame Debugger里一目了然。
第三个是真机数据。Editor里的帧率和耗时参考价值有限,桌面显卡和移动GPU完全不是一回事,想要结论可靠,一定要连着真机Profile。中端安卓机、旧款iPhone,都至少要覆盖一款。我的习惯是开Profiler的同时固定跑同一段角色密集的场景,前后数据对比才有意义。
1.2 同屏很多角色卡和单个角色卡,病因完全不同
这个判断思路非常关键。很多项目优化迟迟没有进展,就是因为没有区分这两种情况,把力气用错了地方。
同屏角色一多就卡,通常是这些问题:角色材质数量太多导致Draw Call翻倍;骨骼动画和CPU蒙皮的开销随着角色数量线性增长;阴影CasterPass把每个角色又画了好几遍。这些问题属于“数量型瓶颈”,跟单个角色多精致关系不大。
单个角色转镜头或者靠近镜头时卡,则是“质量型瓶颈”。常见原因有:Shader片元计算太重,比如每个像素都在做多层贴图采样;半透明头发和披风产生大量Overdraw;阴影级联和软阴影分辨率过高;角色模型面数过高导致顶点处理跟不上。
这两种病都能导致掉帧,但方向完全相反。前者要减数量、减批次,后者要减质量、减指令。接下来所有优化手段,我都会按这个思路去讲。
值得注意的是,在定位瓶颈阶段,有一类问题特别容易误导人,就是“看起来主线程没爆、GPU也没满载,但帧率就是上不去”的情况。这时候大概率是渲染线程和主线程的同步等待、垂直同步或者功耗发热降频在起作用。移动端还有一类很坑的情况:设备刚开机跑Benchmark很稳,玩十分钟后开始掉帧,那是因为发热降频,性能数据仅供参考,需要多测几轮取稳定状态。
2. 合批难题:为什么角色网格的Draw Call压不下来
网格渲染的合批逻辑在角色身上经常失效,这是很多Unity开发者困惑的点。画一堆Cube可以合批得干干净净,换成角色就全乱了。原因需要从合批机制本身说起。
2.1 静态合批、动态合批、GPU Instancing和SRP Batcher对角色是否适用
我把Unity常见的几种合批手段对角色网格的适用情况整理成了表,方便对照。
| 合批方式 | 原理 | 能否用于角色网格 | 主要限制 |
|---|---|---|---|
| 静态合批 | 运行前把静态网格合并成一个大网格 | 基本不可用 | SkinnedMeshRenderer是动态网格,每一帧顶点都会变化,无法预先合并 |
| 动态合批 | 运行时把符合条件的小网格合并 | 不推荐 | 顶点数限制严格,蒙皮网格参与动态合批条件苛刻,CPU开销常常高于收益 |
| GPU Instancing | 用同一个Mesh、同一份材质、不同矩阵一次绘制多份 | 有限适用 | 需要Mesh和材质完全相同;角色Mesh相同但骨骼Pose不同,蒙皮结果不同,不能直接Instancing |
| SRP Batcher | 缓存材质参数和Shader状态,减少SetPass Call | 有效,但要Shader支持 | 只适用于URP/HDRP,Shader必须声明CBUFFER,材质数据在内存布局上保持一致 |
从我实际项目经验来看,角色合批最有效的路径不是指望通用合批,而是从源头减少角色自身的Draw Call数量。一个角色身上挂五个材质,每个材质再叠加两三个Pass,单个角色渲染调用就奔着十几去了,同屏再来十个角色,SetPass Call直接爆表。
2.2 一份Unity官方没有明说的取舍:一个角色最多两到三个材质
很多美术同学习惯把角色拆成身体、头发、衣服、鞋子、配饰、眼睛、眉毛,每个部位独立材质。好处是调起来方便,坏处是渲染成本成倍增加。我的实际建议是,角色身上的材质数量控制在两到三个以内。
具体怎么压缩材质数量?最常用的是把各种贴图合并成一张大图集。身体、衣服、鞋子的颜色纹理尽量拼在一张Atlas里,法线、高光、AO这些属性也各自拼图集。图集尺寸控制在1024到2048之间,太大在移动端会出现采样精度下降和显存浪费。
关于合并网格,如果美术工具链支持,可以把身体、头部、上衣、下装合并成一个SkinnedMesh,重新绑定骨骼,共享一套权重。合并时需要注意顶点数不能超出移动端的合理范围,还要保证骨骼权重最多四根骨骼影响一个顶点。这个操作的风险在于,重新绑定后衣服边缘可能会穿插,需要反复调权重复刷权重,所以只推荐项目定制化角色这么做,换装类游戏还是保留部位拆分,但尽量共用材质。
2.3 用Data-Driven方式看透一次Draw Call里到底有什么猫腻
Frame Debugger里经常会看到一个角色被拆成很多个子Draw。要养成检查每条Draw的习惯,重点看几个信息:
- 这个Pass的名称是什么,是ShadowCaster、DepthOnly还是BasePass;
- 当前ShaderPass用的材质数量是否合理;
- 被画了多少个顶点,Mesh是否和想象中的一致;
- 是否勾选了某些额外Pass,例如ForwardAdd的逐像素灯。
一套常见操作是,先把光源数量砍到只剩一盏方向光,再看角色是不是还有多余Pass。很多角色Shader里被默认开启的额外Pass,既不在美术的预期内,也没带来可见的画质收益。Frame Debugger是验证这些细节最直接的窗口。
另外一个非常容易被忽略的点,是材质Inspector上那些没有实际使用的贴图槽位。美术同学拖了一张Normal Map进材质,但粒子系统或者系统默认的URP Lit Shader会在所有用到该材质的角色上保留对应的采样和计算开销。只要这些贴图槽位存在,Shader变体就可能被打包进去。发现角色身上某个贴图根本没用的时候,先从材质上清掉。
3. Animator和蒙皮:CPU侧的角色开销大头
同屏角色多导致的卡顿,根源往往不只是Draw Call,而是CPU侧的角色动画更新和蒙皮计算。这两块在PlayerLoop里都是主线程耗时,只要角色数量一多,无论GPU多快,都会被拖死。
3.1 骨骼动画的逐帧成本到底花在哪里
一个带动画的SkinnedMeshRenderer角色,每一帧要经历大概这么几条开销:
- Animator组件从所有动画状态机、混合树、动画片段里采样当前Pose;
- 根据采样结果更新骨骼节点的层级变换,把每个骨骼的LocalTransform组合成WorldTransform;
- 遍历蒙皮网格的所有顶点,把顶点从绑定姿势变换到当前骨骼姿势下的位置,如果有多个骨骼影响,还要加权;
- 更新Renderer的包围盒,用于视锥剔除。
前三项是典型的CPU密集操作。角色骨骼数量越多、顶点数越多,单角色耗时越高;而同屏角色数量上升,这部分开销几乎线性增加。所以控制CPU侧的开销,本质上就是控制三个数:骨骼数量、顶点数量、更新频率。
我见过一个项目,每个角色身上还挂着十来个空节点,专门用来挂刀、挂盾、挂特效。这些空节点虽然不做蒙皮,但Transform层级更新和Animator的骨骼采样依然会计算它们。干掉不受动画控制的空节点,改为代码直接维护挂点位置,同样可以省掉一批更新量。
3.2 基于距离的动画降频:最简单的开关式优化
角色离摄像机很远的时候,动画精度对玩家几乎没有感知,这时候完全可以降低更新频率。这是我个人认为移动端同屏大量角色时最有效、最划算的一招。
Unity里可以写一个简单的动画LOD策略:距离近的角色每帧更新,中等距离的每两到三帧更新一次,远距离的干脆停止更新。规范做法是直接关闭Animator组件,让Render不再使用骨骼Pose,或者用Animator.Update手动隔帧推进。我习惯用一个轻量脚本来控制,思路类似这样:
public class CharacterAnimationLOD : MonoBehaviour { [SerializeField] private Animator animator; [SerializeField] private float fullUpdateDistance = 12f; [SerializeField] private float lowUpdateDistance = 30f; private int frameCounter; private float timer; private void Update() { if (!animator) return; float distance = Vector3.Distance( Camera.main.transform.position, transform.position); if (distance < fullUpdateDistance) { animator.enabled = true; timer = 0f; frameCounter = 0; return; } if (distance < lowUpdateDistance) { // 每隔0.1~0.2秒手动推进一次动画 timer += Time.deltaTime; if (timer >= 0.15f) { animator.Update(Time.deltaTime); timer = 0f; } } else { animator.enabled = false; } } }注意,这个脚本只是展示思路,实际使用时要根据动画Clip的播放速度和角色动作类型调试间隔。匀速跑步的角色隔20帧更新一次问题不大,但转身或者抬手这类变化剧烈的动作,间隔太久会产生肉眼可见的抽搐。所以降频档位要配得保守一点,中距离每两到三帧更新一次就够了,不要让画面穿帮。
还有一个更容易被忽略的开销:每次调用Animator.SetFloat、Animator.SetTrigger、获取Animator.GetCurrentAnimatorStateInfo都可能触发状态机的脏标记和重新评估。在同屏大量角色的情况下,绝对不要在每帧Update里对每个角色都做这类调用。把参数更新集中到一个低频循环里,可以显著降低CPU压力。
3.3 蒙皮计算和GPU Skinning的边界
Unity支持把蒙皮计算从CPU搬到GPU,在SkinnedMeshRenderer或者Quality设置里可以切换。这个选项听起来很美好,实际使用时有前提:
- GPU Skinning不支持BlendShape,也就是不支持表情变形和大部分形态键;
- 如果角色Shader已经非常复杂,GPU已经被片元计算占满,那么把蒙皮搬上GPU不一定更快,只是把压力从CPU挪到了GPU;
- 移动端对GPU Skinning的支持程度因设备而异,低端机型不一定更快。
我的判断标准很简单,先看Profiler里CPU侧的蒙皮耗时。如果CPU侧蒙皮占比不到1毫秒,就不用折腾GPU Skinning;如果同屏角色数量很大,蒙皮耗时超过两三毫秒,再检查角色是否有BlendShape,没有的话就可以试试切到GPU,用真机对比前后帧耗时再决定去留。
另外一个优化方向是模型顶点数。角色面数在移动端要严格控制,一个全精细角色两三万面已经偏高,五万面以上对CPU蒙皮和GPU顶点处理都是很大压力。用了LOD之后,近景高模、远景低模才是移动端角色的常态结构。
4. Shader侧:移动端角色渲染的GPU开销到底怕什么
Shader是GPU侧优化最核心的战场。很多人一说优化Shader就想到写低精度、减少贴图,但实际项目里最常见的瓶颈往往比这更隐蔽。从实际采坑经验看,移动端角色Shader最怕的不只是贴图多,而是逐像素计算过重、Pass数量过多、以及变体数量爆炸。
4.1 一条常规角色PBR Shader的逐项拆解
拿一条很常见的URP Lit风格角色Shader来说,片元着色器里通常会发生这些事情:采样BaseMap、采样NormalMap、解包法线、采样Metallic/AO/Roughness贴图、计算主光源光照、采样ShadowMap做阴影、采样环境反射或LightProbe做间接光、可能还叠加雾效。这一套全部跑在片元阶段,角色身上的每一块像素都在执行。
移动端GPU的像素填充率是硬指标,分辨率越高、Overdraw越严重,这条Shader的耗时就越离谱。优化手段是按需裁剪,不需要的功能直接移除:
- 角色不需要法线贴图就删掉NormalMap采样,法线空间变换是片元Shader里比较贵的操作;
- Metallic和Roughness如果没有明显效果,可以合并成一张ORM贴图,减少一次采样;
- 反射探针在户外场景中对角色的贡献很小,但CubeMap采样不便宜,移动端默认可以不开;
- 多盏实时点光源的影响范围如果覆盖角色,会产生额外的ForwardAdd或者RenderLoop开销,尽量让角色只受主光源影响。
建议开发时开着Frame Debugger,逐条观察角色身上的Draw和Pass,能发现很多“看起来用到了、其实毫无感知”的功能。删除这些功能,帧耗时会立刻下来。
还有一个细节是关于half与float。在移动端,片元计算尽量用half精度是有收益的,因为很多移动GPU对half有专门的快路径。但回报程度取决于具体GPU,不必为了追求精度优化而引入一堆#if,保证高光和贴图采样差异不明显的前提下,能降就降。
4.2 二次元与卡通渲染角色的Shader优化路线
很多Unity开发者做二次元风格角色,Shader从PBR美术流程里拿过来又用不上,沉重且难看。卡通渲染的正确路线是大幅简化光照模型:
- 把逐像素的PBR光照换成Ramp贴图采样,也就是所谓的色阶渐变光照,一次LUT采样替代多次光照计算;
- 高光从物理模型变成简单的Matcap或者色块高光,Shading效果稳定且便宜;
- 阴影过渡用Shadow Boundary或Ramp硬切,不需要复杂的半影过渡算法;
- 环境光直接踩成固定色或通过LightProbe采一次,不在片元里做逐物体间接光。
描边技术也要小心。全屏后处理描边对移动端负担太重,角色描边用边缘光Mesh或反向法线描边更可控。反向法线描边其实是在顶点阶段把角色面沿着法线方向往外扩一层,背后再画一个纯色Mesh,虽然会额外增加一个Mesh的渲染调用,但总成本远低于全屏后处理。
4.3 隐藏的Pass:阴影Caster、深度、透明混合
普通的URP Lit角色至少会有BasePass和ShadowCasterPass,有时还有DepthOnlyPass。ShadowCasterPass每帧会把角色几何体在光源视角下重画一遍,这个成本很容易被忽略,因为Frame Debugger里它画得整个屏幕半黑半白,不太直观,但顶点数和网格复杂度和普通Pass几乎一样。
优化方式有几档:
- 如果角色离相机很近,阴影消失不明显,可以关闭阴影投射或者让角色使用LOD1作为ShadowCaster;
- 简单粗暴但常用,给角色做一套低面数专用阴影代理网格,同一套骨骼驱动,阴影画得粗糙一点没人看得出来;
- 阴影距离有效范围缩减,超出距离的角色自动不投射阴影。
透明混合是另一类隐形开销。角色头发、披风、装饰用的半透明材质,每个像素都会做一次混合计算,并且和后面已经画过的内容叠加,产生Overdraw。移动端Overdraw超过3层就已经有明显风险了。能做不透明就尽量不透明,头发能做成Cutout就做成Cutout,至少区域限定明确,混合层数会少很多。
4.4 Shader变体数量:打包和运行时双重炸弹
Shader变体是项目后期最容易爆炸的地方。美术同事在材质上勾了一堆开关,系统生成了几百上千个变体,哪怕场景里只用了一两个,打包时依然会被保留,运行时Shader初始化、加载和切换也会明显变慢。
解决办法是在Player Settings或者URP Asset里打开Shader Striping,把项目和场景里没用的变体剥离掉。那些全局关键字,比如阴影、雾效、额外光照,如果角色场景不需要,就把关键字一并在打包阶段移除。
Build Report里能找到Shader的编译后大小和变体数量,优化前后对比非常直观。我曾遇到一个项目,单纯把不需要的变体Strip掉之后,安装包体积和首帧卡顿都改善了一大截。不要小看这一步,它属于典型的投入小、见效快。
5. 阴影距离与遮挡剔除:人物渲染中最容易偷走帧率的隐形开关
阴影和遮挡属于那种“画面上占便宜、性能上吃大亏”的模块。很多时候角色本身的Draw Call和Shader都优化得不错,但帧率还是上不去,真凶就是阴影设置。
5.1 阴影距离、级联数值和软阴影的杠杆效应
URP Asset里有一个Max Distance阴影距离设置,力和收益关系非常明显。阴影距离从60米降到35米,参与阴影渲染的场景几何体数量可能会减半以上,角色也同样受益。对移动端来说,阴影距离通常控制在20到40米比较稳妥,超出这个范围的阴影肉眼基本感知不到。
阴影级联数目也需要克制。四个级联在PC上看着舒服,移动端两个级联都是负担,一个级联反而更适合以角色为核心的手游。级联的作用是让近处阴影更清晰,如果画面结构以人物和室内战斗为主,两级已经足够。
软阴影在URP里做起来方便,但代价是GPU在ShadowMap上做额外的过滤计算。低端机建议硬阴影或者只有主光开软阴影,Soft Shadow Quality控制在低档。这个取舍在截图对比时几乎看不出来,帧率差异却很明显。
角色自身的阴影投射也值得单独说。主导角色保留自阴影,次要NPC和远处的角色可以关闭Receive Shadows。自阴影对画面立体感的贡献主要来自脸部领口、袖口、衣摆这些局部遮挡区域,一旦接受阴影关闭,这些区域会显得“浮起来”。所以我的默认策略是主角和核心NPC保留自阴影,背景角色和敌人模型关闭。
5.2 阴影代理网格:用低模去画阴影
阴影代理这个技巧,很多项目没用上,属于性价比很高的方案。角色阴影投射时,GPU实际上是把整个角色Mesh在光源视角下又画了一次,面数完全一样。如果能把这次绘制换成低配网格,阴影质量几乎不变,但顶点开销能降下来。
实现上通常是三选一:
- 使用角色模型的LOD1或者LOD2去投射阴影;
- 美术同事专门做一版面数极低的Shadow Proxy模型,和角色共用骨骼和动画,只用于投射阴影;
- 在阴影CasterPass里通过Vertex Shader直接简化网格的几何细节,比如把离光源远的顶点合并到低LOD顶点。
要注意角色动画中的骨骼旋转可能让Proxy网格和显示网格出现较大误差,所以Proxy绑定要尽量贴近显示网格的骨骼结构,误差控制在半米以内即可。如果角色模型本身面数不高,用LOD1就够了,Proxy可有可无。
5.3 遮挡剔除对动态角色基本失效,别把希望全押在上面
Unity场景烘焙的Occlusion Culling对动态角色几乎没有作用,这是很多新人的认知误区。遮挡剔除体系默认主要剔除静态场景物体,角色是动态对象,即使站在墙后面,只要它的包围盒还在视锥体内,Unity依然会把它送去渲染。
所以同屏大量角色时,引擎自带的剔除帮不上太多忙。我自己更愿意写一套基于距离、角度和状态的角色管理策略:
- 超出最大可见距离的角色直接禁用Animator和Renderer;
- 在角色和相机之间做一个简单的射线或包围盒测试,被遮挡且不在屏幕内的角色延迟更新;
- 角色播放特定动画时,比如死亡、倒地、远离视野,单独控制它的更新频率。
这套思路听着简单,但在多人大世界项目里往往是压垮帧率的关键救星。角色渲染的优化,物理解法有时候比图形学解法更立竿见影。
另外,角色Renderer的包围盒在视锥剔除里也常出问题。Unity会按照蒙皮后的顶点范围来更新包围盒,如果Initial Bounds设置得过大,角色明明已经在屏幕外了,包围盒依然在视锥体内,白白多画一次。对于动画幅度可控的角色,可以考虑在Start阶段用renderer.SetBounds把包围盒收紧到合理范围,但一定要谨慎,如果收紧后动画顶点越出边界,角色就会从画面上消失,需要反复验证。
6. LOD与显存:让角色在远处自动“降级”的工程化思路
角色渲染性能优化,越往后面越是工程问题,LOD就是很典型的工程解法。它不做任何单点技法的复杂优化,而是通过分层管理把资源用在刀刃上。
6.1 角色LOD组怎么配:几何、材质、动画三路并行
角色LOD组的搭建,核心不能只把网格面数降一降,而是几何、材质、动画三个维度同步降级,效果才能拉满。
LOD0,近景全精度。完整网格、完整材质、完整阴影、完整动画、Receive Shadows开启。玩家在战斗和对话时看到的就是这个版本。
LOD1,中景中精度。面数减到一半,关掉不必要的NormalMap和反射探针采样,关闭动态骨骼细节,动画按两帧甚至三帧更新一次。阴影保留但可以用简化的ShadowCaster。
LOD2,远景低精度。面数再减一半,Shader切换成无阴影、无光照细节的简化变体,贴图Mipmap偏置调大,动画甚至可以直接停在一个静态Pose上。这个距离角色基本就是个轮廓,玩家不会转头去确认它还在呼吸。
实际参数参考:
| LOD等级 | 距离范围 | 面数档 | 材质复杂度 | 阴影 | 动画更新 |
|---|---|---|---|---|---|
| LOD0 | 0-12米 | 20000-30000 | 完整 | 投射+接收 | 每帧 |
| LOD1 | 12-30米 | 8000-12000 | 简化 | 只投射 | 每2-3帧 |
| LOD2 | 30米以上 | 3000-5000 | 极简 | 关闭 | 静态Pose或极低频 |
注意LOD切换可能造成明显的视觉突变。Unity的LODGroup里可以做交叉淡化CrossFade,但代价是切换瞬间两个LOD会同时渲染,峰值开销更高。角色这种高频出现在屏幕中心的物体,切换突变很容易被玩家看到,我的办法是把切换距离拉远一点,让切换发生在玩家的视线盲区,比做CrossFade更省性能。
6.2 把动画LOD和渲染LOD合并起来控制
这里想分享一个很多项目没做过的实践:动画LOD和渲染LOD共用同一套距离阈值,放在同一个脚本里管理。
角色远到一定程度,动画更新频率降低;更远一点,直接停用Animator,同时把角色换到低模。这个过程不需要美术参与,纯代码逻辑控制。核心是保持几个阈值之间的紧凑感,不要出现“角色已经切了低模但动画还在高频率更新”这种浪费。
有一个额外的经验,如果角色是可换装、可持武器的NPC,LOD策略要跟着装备Mesh走。武器、披风这些附属物件也有自己的渲染和阴影成本,它们在LOD1和LOD2中也要同步降级或关闭。只给身体做LOD,附属物件保持高模,优化效果会大打折扣。
6.3 贴图格式、Mipmap和显存控制:常见但容易被忽视
角色贴图这块,最典型的两个误区,一个是所有贴图都上2048,一个是完全不看压缩格式。
移动端贴图格式要选对。Android首选ASTC,苹果设备也支持ASTC,兼容性差一点的老设备再退到ETC2。ASTC压缩比灵活,同样的贴图尺寸,用6×6块压缩比4×4更高,图片质量和显存消耗的平衡性更好。角色Diffuse贴图用ASTC 6×6通常在视觉上损失不明显,NormalMap建议更保守一点,比如5×5或4×4,避免法线细节丢失。
显存计算有个简单的估计方法:一张1024×1024的RGBA贴图,未压缩约4MB,ASTC 4×4大约1MB,ASTC 6×6大约0.5MB。角色身上若有8张2048贴图,显存压力立刻上好几个数量级。所以尽量用Atlas合并,大小控制在2048以内,是显存控制的第一原则。
Mipmap这个选项让人纠结。开着会多约三分之一的显存,但远处采样不会闪缩,并且GPU能自动使用更小层的Mip,实际上会降低带宽。角色始终在近距离的项目可以关掉,但大多数动作游戏镜头会拉远,建议保持开启。
还有一个导入选项“Read/Write Enabled”,如果忘了关,Unity会在CPU内存里保留一份贴图副本,纯属浪费。所有角色贴图都应该关掉这个选项。MeshCompression和动画压缩也有类似问题,Mesh压缩太高会导致法线偏离,角色皮肤会出现奇怪的硬边,一般调整在Low档就好,极端情况再单独处理。
角色渲染优化做到这一步,通常已经能稳定拿到很好的帧率了。剩下的事情主要靠测试,真机上反复跑,多看Profiler,别让优化完的版本出现新的热点。我自己的习惯是每一轮优化只动一个变量,改完跑一次基准数据,确认这个改动确实有正向收益再继续下一步,避免陷入“什么都改了但瓶颈还是没解决”的境地。
回到最开始那个项目,最后把角色渲染帧耗时从11毫秒压到了4.8毫秒,最大的功臣并不是什么花哨的Shader技巧,而是三件不起眼的事:一个角色只保留两个材质,阴影距离砍到30米,加了基于距离的动画和渲染LOD。这三步做完,同屏二十个角色都已经不卡了。性能优化往往就是这样,高级玩法用不上的时候,老老实实把基础项排干净,效果反而最好。