做Unity项目尤其是带角色的游戏,性能优化这件事迟早要正面刚。很多人开场觉得“先把功能做出来,后面再优化”,结果一到真机测试,人物一多、镜头一拉近,帧率直接塌方,再回头改模型、烧香找Shader问题,成本比一开始就规划高好几倍。我这些年经手过好几个不同规模的项目,从MOBA到二次元ARPG都有,人物渲染的优化每次都是重头戏。这篇文章就把我实际验证过的一套东西整理出来,从瓶颈分析到具体参数,从工具使用到踩坑记录,尽量讲透。
需要说明的是,下面这些方案大部分来自我自己的项目实践,结合了Unity各个版本通用的规则。不同项目、不同机型、不同美术风格,结论可能不完全一致,但排查思路和优化框架是可以复用的。如果你正卡在“人物太多跑不动”“皮肤头发效果上不去”“Profiler看不懂”这几个问题上,这篇文章应该能帮你省掉不少弯路。
1. 先搞清楚人物渲染的瓶颈到底在哪
1.1 人物渲染为什么比场景渲染更“烧钱”
做过优化的人都知道,一个角色的渲染开销往往比同等面积的墙体、地面高出一大截。原因并不复杂:人物是动态的,几乎占满了所有“高成本特性”。
第一是骨骼动画。每次蒙皮计算都要把顶点跟着骨骼权重刷一遍,顶点数越多、骨骼权重越复杂,CPU和GPU的负担就越大。你场景里的石头、墙壁是静态网格,不需要每帧更新;人物每帧都在动,蒙皮矩阵要刷新、骨骼层级要遍历、动画曲线要采样,这些全都要算。
第二是材质种类多。一个像样的角色,皮肤、头发、眼睛、布料、金属饰品往往各用各的Shader,或者在一个Shader里开很多特性开关。每多一种材质,就多一次Draw Call,多一份Shader变体编译压力。
第三是光照和阴影。人物是要在场景里“立”住的,接连受到环境光、主方向光、实时阴影的影响。移动端如果给人物上了实时阴影,整个性能预算会被吃得很惨。
我之前遇到过最夸张的情况:一个5人小队同屏,每个人物8个材质,加上武器特效,一次战斗场景Draw Call直接飙到400多。这在PC上不算什么,但放在中低端手机上就是灾难,掉帧掉到没法玩。
1.2 优化前要先定性能基线和目标
很多人一提优化就抓起Profiler开始看,其实这不对。第一步应该是定基线。
你要先明确目标平台是什么档次的机型。是做iPhone 15 Pro级别的旗舰体验,还是做骁龙6系、天玑700这种中低端全覆盖?不同档位对应完全不同的预算。我自己的经验是按三档来定:
- 高端档:同屏4~6个角色,All Draw Call控制在200以内,帧率保持60 FPS;
- 中端档:同屏2~3个角色,All Draw Call控制在120以内,帧率30 FPS稳定;
- 低端档:同屏1~2个角色,Draw Call控制在80以内,帧率30 FPS但不能闪退。
定了帧率和数量之后,再用Profiler测一下当前的数据,把超预算的部分列成清单,逐个击破。没有目标就动手优化,很容易陷入“看哪都慢、改哪都没底”的状态。
2. 模型与骨骼:先把“底子”做瘦
2.1 顶点数和蒙皮权重:够用不等于能用
人物模型的顶点数,是很多美术和程序争论最多的地方。美术觉得“细节越多越精致”,程序觉得“跑不动就是白搭”。我的观点是:顶点数本身不是唯一标准,UV密度、贴图尺寸、蒙皮权重数量共同决定最终开销。
先说顶点数。一个用于移动端的写实人物,我的建议是控制在1.5万到3万顶点。二次元风格、卡通渲染的角色,可以更激进一点,8千到1.5万就够用,因为卡通风格靠贴图画细节,不需要几何细节硬撑。
真正容易被忽略的是蒙皮权重。每个顶点最多可以被多少个骨骼影响,这个数值越大,蒙皮计算的消耗越大。默认引擎往往允许4根骨骼影响一个顶点,但很多模型在导出时实际只用了2根骨骼就能达到同样的变形效果。把权重数量从4砍到2,蒙皮计算量能肉眼可见地下降。
实操中有个很实用的检查项:在Unity里选中模型,看Inspector里的“Mesh”信息里有没有提示超过4根骨骼影响的顶点。如果某个角色大面积出现这个警告,美术那边肯定是刷权重刷“糊”了,要回去精简。
还有一点,人物模型的风衣、头发、裙子这类部件,如果是飘动的,不要整块都用骨骼动画,代价很高。现在常用的方案是用Shader里的顶点动画做轻微的摆动,或者用Unity的Cloth组件搭配较低分辨率的碰撞体。像裙摆这种大面积布料,全骨骼驱动在移动端基本跑不动。
2.2 骨骼数量与动画曲线:隐性开销藏在你看不到的地方
骨骼数量这个问题藏得特别深。很多项目美术导出模型时,会把整个骨架原样带进来,动辄一两百根骨骼。但实际动画中真正参与变形的,往往只有几十根。
骨骼树在运行时是要每帧遍历的,每根骨骼都要计算世界矩阵、更新层级关系。骨骼数量越多,更新开销越大。另外Animator组件的动画状态机、IK、Root Motion这些功能也都会消耗CPU。
我遇到过项目里的一个角色,骨架里竟然有20多根手指骨骼,就为了做一个攥拳头的细节,结果整个动画更新开销比别的角色高出一倍。砍掉多余骨骼之后,效果完全不受影响。
动画曲线方面,很多人不知道动画文件里的曲线数量直接决定动画重采样和更新的成本。导入FBX时,Unity会把所有带关键帧的曲线都加载进来。如果一个动画文件包含大量无用骨骼的曲线,哪怕这些骨骼根本没动,也会有基础开销。所以导入设置里,把“Animation Compression”从“Off”改成“Optimal”,再勾选“Anim. Compression”的减少曲线选项,能在几乎无感的情况下降低内存和CPU占用。
2.3 LOD与多级模型:同屏角色的保帧利器
LOD这个东西在场景里用得很多,但人物上经常被忽略。不少人觉得角色又不能真的离镜头特别远,LOD意义不大。这个看法是错的。
一场战斗中,镜头在角色间切换很快。离镜头近的主角用高模,身后几个辅助角色完全可以用中模和低模代替。人眼对远景角色的细节敏感度很低,你根本分不清远处的角色是一万面还是两千面。
我的做法是每个角色准备三档模型:LOD0是高模,用于特写和近视角;LOD1是中模,顶点数砍掉40%左右;LOD2是低模,砍掉70%,同时关掉实时阴影。切换距离按项目相机的FOV和战斗场景大小去调,通常LOD0在2~5米内,LOD1在5~12米,LOD2在12米开外。
实现上不需要自己写复杂的逻辑,Unity的LOD Group组件就能解决。只要把三档模型挂进去,设置好每个档位的切换百分比就行。
3. 材质与Shader:让人物的“皮”更省电
3.1 从标准PBR到移动端皮肤Shader的取舍
UWA和Unity官方都反复说过一件事:Standard Shader是一个功能非常全面但开销也非常高的Shader。如果你做的是移动端游戏,还在给人物用Standard,那基本等于从一开始就在“负优化”。
Standard Shader默认带了一堆特性:全局光照、实时阴影、法线贴图、高光、反射探针等等。这些特性在PC编辑器里看不出问题,但到了移动端,GPU的带宽和着色器指令数都有限,任何一个特性都可能成为瓶颈。
皮肤渲染这件事,手游项目更常见的做法是写一个专门的移动端皮肤Shader。核心思路是保留PBR的观感,但降低计算量。
我自己的做法是:皮肤高光用Blinn-Phong或者简化版的GGX,不搞复杂的多次散射;环境光用一张预烘焙的粗糙度贴图配合光探针(Light Probe)来实现;法线贴图保留,但压缩成两张合并的贴图,减少采样次数。这样在移动端运行,皮肤观感依然有光泽,但性能比Standard好很多。
如果你不熟悉Shader编写,还有一个取巧的办法:把Standard Shader复制一份,把不需要的特性全部删掉。比如删掉“Detail Mask”“Height Map”“Parallax”这些不用的,只保留Albedo、Normal、Metallic、Smoothness。这样也能省不少指令。
3.2 头发、眼睛、描边的性能取舍
头发是人物渲染里最“吃”效果的一个部件。做二次元风格的时候,头发往往需要两层高光、边缘光、还有各向异性高光。全堆在实时计算里,一个小小头部就能把Shader指令数翻一倍。
我的折中方案是:头发的高光形状用贴图预制。也就是把高光的强度和范围画到头发贴图的Alpha通道里,Shader只做一次简单的采样和叠加,不搞程序化生成各向异性高光。这个方法在视觉上能保留90%的效果,但计算量只有原来的三分之一。
眼睛方面,关键是避免真实的眼球折射和反射。移动端上做眼球,用一张眼睛贴图加一个简单的UV偏移模拟注视方向,配合一个球面高光贴图就够了。如果角色会眨眼,给眼皮做个简单的骨骼旋转比做BlendShape更省。
描边在二次元渲染里是重头,但传统描边做法(法线外扩+背面绘制)在移动端很容易导致人物身上出现大量Overdraw。我见过一些项目整个游戏性能被描边拖垮,就是因为所有角色都用了4次Pass的描边Shader。
优化方式有几个:一是描边厚度放到顶点色里,通过模型预先烘焙,而不是Shader实时计算;二是用一个单独的描边相机,只渲染带描边的物体,输出到一张特定分辨率的目标纹理,再叠加到主画面。这样主相机不跑描边Pass,性能会好很多。
3.3 Shader变体和Keyword管理:打包里的隐形胖子
人物Shader一旦多了,就会遇到Shader变体膨胀的问题。每个材质勾选不同的Keyword,引擎就会编译出对应的变体。比如你给头发Shader开了“MAIN_LIGHT_SHADOWS”和“_RECEIVE_SHADOWS”两个Keyword,它会生成多个变体,打包时这些全都会被塞进包里。
很多人只会数贴图和模型,完全没意识到一个Shader几十个变体占掉几百MB内存的场景是真实存在的。要解决这个问题,我建议做三件事:
第一,用Unity的“Shader Variances”窗口定期检查变体数量,比如Window > Rendering > Shader Variants,把变体数量超过预期的Shader单独拎出来处理。
第二,在Shader里尽量用宏定义控制特性,不要把每个特性都做成Keyword。实在需要Keyword的,用[Toggle]属性控制,不要用[Enum],因为后者会生成大量不必要的变体。
第三,打包时手动指定Shader集合,用ShaderVariantCollection把真正用到的变体记录进去,勾选“Strip Unused”选项。这样做完之后,包体的小头能明显降下来。
另外,给角色开的是移动端渲染管线的话,记得在Render Pipeline Asset里把不用的Shader Pass去掉,比如把ShadowCaster里不需要的Pass删掉,能省不少构建时间。
4. 光照与阴影:人物身上最贵的“装修”
4.1 阴影设置:先看距离再看级联
人物渲染中最容易出大坑的就是阴影。很多项目为了效果把Shadow Distance开到80米甚至100米,结果一到中低端设备,光是阴影渲染就能把帧率打到全屏15帧。
我的调整思路非常简单直接:Shadow Distance砍到接近镜头的实际视野范围。如果是俯视角游戏,Shadow Distance可以保持40米;但如果是第三人称越肩视角,Shadow Distance开到20米就足够。超出这个距离的角色影子,人眼几乎不会注意到。
还有Shadow Cascades的设置。Cascade数量越高,近处阴影越清晰,但开销也越高。移动端我建议直接用Two Cascades,并且把Cascade比例调成“靠近镜头压缩”,这样近处仍然有足够清晰的阴影,远处视觉影响不大。
人物身上的阴影如果要保留,最好单独开一个只针对主要角色的小范围Directional Light,只影响主角,其余NPC用烘培好的假阴影或者干脆不给实时阴影。这种“混合照明”方案是我目前最推荐的,实际效果和性能之间能取得很好的平衡。
4.2 烘焙不是只在场景里用:动态人物也能“借光”
很多人觉得动态人物不能烘焙,只能吃实时灯光。这个想法有点片面。人物确实是动的,但它所处的环境是相对固定的,所以我们可以把环境光信息烘焙出来,人物通过光探针(Light Probe)来读取。
场景里把Light Probe Group铺好,人物材质只要开了“Receive GI”,移动时就会自动读取周围的光照信息。这样人物就不需要依赖实时点光源和区域光了。
这部分对性能的提升非常明显。我遇到的一个项目,角色身上原有两个实时点光源,每多一个光源就要多一次光源计算,而且Shader里的循环遍历光源是重型操作。换成了Light Probe之后,同屏光源数量减了两个,帧率直接涨了10帧。
关键点是Light Probe的密度要够。放得太稀疏,人物在两个探针之间走动时,光照过渡会不自然,甚至有肉眼可见的跳变。建议在人物经常走动的区域,每隔2到3米放一个探针。
4.3 半动态角色:用静态光照代替实时光照
有经验的开发者都知道,越是同屏人数多的游戏,越不能给每个角色都上实时光照计算。一个简单的优化大法,是把角色的光照计算结果写入diffuse信息里。
具体做法是:项目在出包前,会直接把主光方向、环境光色值、阴影信息烘焙成一张光照贴图,配合角色模型展好的UV,能让角色直接呈现“受光”的状态。这个方案在固定视角的游戏中效果很好,甚至可以做假阴影。
如果场景中有多个不同方向的光源,这种静态光照方案就会有点力不从心。但作为战斗场景里辅助角色的渲染方案,非常够用,毕竟玩家的注意力主要在主角身上。
5. 用Profiler和Frame Debugger把问题钉到现场
5.1 Profiler的CPU与GPU两维读法
优化做到后面,最缺的不是方案,而是定位能力。好多时候你感觉“人物一多就卡”,但到底是CPU卡还是GPU卡,完全不知道,那优化自然会碰运气。
Unity自带的Profiler提供了两套数据:CPU Usage和GPU Usage。如果CPU Usage里Rendering一项占了大头,说明CPU在提交渲染命令上卡了,通常是Draw Call太多、State Change太频繁;如果GPU Usage里Render Thread一直是满的,说明GPU着色负担重,往往是Shader太复杂、Overdraw严重或者分辨率太高。
我常用的定位方法是:帧率掉的时候,先打开Profiler看CPU和GPU的总耗时。如果GPU耗时接近16.7ms甚至更高,问题多半在Shader或Overdraw;如果CPU耗时高而GPU不忙,问题多半在动画更新、物理或者Draw Call提交。
记住,看不到GPU耗时的时候,用Frame Debugger也能直接数出来Draw Call数量,那是一个纯CPU提取的视角,能直观地确认是不是提交瓶颈。
5.2 Frame Debugger和RenderDoc的配合使用
Frame Debugger是Unity自带的神器,它能一帧一帧回溯渲染事件,让你看到每个物体的Draw Call、使用的材质、以及渲染顺序。排查“某个角色为什么会多出一个Pass”这类问题,用它最直接。
大体流程是:打开Frame Debugger,点Enable,然后逐帧看事件列表。找到角色相关的Draw Call,点进去看Shader Pass是什么、用了哪些Keyword、有没有意外的透明混合。遇到过很多次以为是优化好了的材质,结果Frame Debugger里发现它在Transparent队列里又画了一遍,这就是额外的Overdraw来源,修掉之后性能立刻改善。
RenderDoc更进阶,适合排查GPU端的渲染细节。它可以把整个帧的纹理资源、着色器编译结果导出来看。我一般是在Frame Debugger锁定问题后,再用RenderDoc看具体某个Pass的Shader代码和材质贴图,确认是不是贴图格式太大或者是采样次数超标。
移动端做GPU分析时,RenderDoc要先连上设备,用Vulkan或者GLES的捕获模式。很多人卡在这一步,其实只要确保设备开发者选项里打开USB调试,让Unity导出工程的时候选上“Development Build + Autoconnect Profiler”,基本都能连上。
6. 我自己踩过的坑和最后推荐的参数模板
6.1 三个血泪Bug记录
第一个坑:头发Shader的Keyword开太多了。美术那边为了头发受不同方向光照产生渐变效果,一口气开了5个Keyword,结果变体数量从60个爆到600多个。最后我逐个测试,发现只要保留“MAIN_LIGHT_CALCULATE_SHADOWS”和“_RECEIVE_SHADOWS”两个,就能达到90%的效果,直接砍掉3个Keyword,包体缩了将近80MB。
第二个坑:人物的布料用了真实的Cloth组件。刚开始布料飘动确实很好看,但一到战斗场景,多个角色同时穿布料,Cloth的碰撞检测和物理更新直接让CPU炸了。最后解决方案是去掉Cloth,改用Shader顶点动画模拟布料飘动,效果差不多,CPU开销降了一半。
第三个坑:模型导入时“Read/Write Enabled”没有关。这个选项如果勾上,Unity会保留一份CPU可读的模型数据副本,内存直接翻倍。人物模型多了以后,内存占比高到离谱,还不好排查。所有运行时不需要动态改动的模型,一定要把“Read/Write Enabled”关掉。
6.2 一套可以直接抄的优化参数模板
最后把我自己在项目中反复用的参数模板分享出来,当作一个起步点。具体数值要根据你的项目和机型微调,但方向不会变。
| 项目 | 参数建议 | 说明 |
|---|---|---|
| 角色顶点数 | 8千~3万 | 写实风格偏向2万以上,卡通风格1万以下足够 |
| 蒙皮权重 | 2~3根/顶点 | 不要默认用4,逐个模型确认 |
| 骨骼数量 | 50以下 | 手指骨骼若不影响核心体验,坚决砍掉 |
| LOD档位 | 3档 | LOD0高模、LOD1中模、LOD2低模 |
| Shadow Distance | 20~40米 | 第三人称建议20米,俯视角可以40米 |
| Shadow Cascades | 2级 | 不要开4级,移动端2级够清晰 |
| 角色所用Shader | 自研移动端Shader | 不要用Standard,浪费严重 |
| Keyword数量 | 2~3个 | 超出这个数,变体膨胀和编译时间都会失控 |
| 额外光源 | 主光1个,其余全部烘焙 | 动态光越少越好 |
| Animation Compression | Optimal | 能减少曲线数量,压内存和省CPU |
做人物渲染性能优化没有银弹,每步都要靠测量数据说话。我现在每做一个角色相关的新功能,都会先在Target平台上跑一遍Profiler,把CPU、GPU耗时和内存变化记录下来,再判断要不要上线。养成这个习惯之后,性能问题基本不会拖到后期才爆发。
如果你正好在做Unity项目,哪怕不是人物渲染相关的,这个“先定基线、拆瓶颈、挨个量化”的思路也完全适用。优化这件事,最怕的就是拍脑袋,有了数据,一切都好谈。