ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity图片资源优化:从导入设置到运行时加载的性能提升指南

Unity图片资源优化:从导入设置到运行时加载的性能提升指南

1. 为什么图片资源是Unity项目里最隐蔽的性能黑洞

做过几个Unity项目之后,你大概率会有这样一个体会:代码逻辑优化得再干净,DrawCall压得再低,只要美术资源一进来,帧率照样往下掉。而在所有美术资源里,图片资源是最容易被忽视、又最容易失控的一类。一个角色贴图从2048压到1024,包体可能直接少几MB;一批UI图集从RGBA32改成ASTC,显存占用能砍掉七成以上。这些数字听起来很诱人,但真正动手的时候,问题就来了——压到什么程度算合适?哪些图不能压?压缩之后画质崩了怎么办?

我在实际项目里见过太多这样的情况:项目前期没人管图片规格,美术同学导出什么就用什么,等到中后期发现包体超标、低端机闪退、加载慢得离谱,才开始回头做优化。这时候改动的成本已经非常高了,因为图片资源往往和预制体、材质、图集、AB包层层绑定,动一张图可能牵连一大片。所以图片资源优化这件事,最好的时机是项目启动阶段就定好规范,其次就是现在。

这篇文章主要面向有一定Unity使用经验、正在被包体或性能问题困扰的开发者。我会从图片资源的底层原理讲起,把导入设置、压缩格式、图集策略、运行时加载、常见踩坑这几个维度拆开来讲,尽量把每个参数背后的逻辑说清楚,让你不只是知道"要这样设",而是明白"为什么这样设"。文章里提到的具体数值和方案,都是基于常见项目实践总结出来的参考值,实际项目还需要根据你的目标平台和画质要求做调整。

2. 先搞懂Unity是怎么处理一张图片的

2.1 从源文件到GPU纹理的完整链路

很多人优化图片资源的时候,脑子里只有"压缩"两个字,但其实一张PNG从美术手里导出,到最终出现在屏幕上,中间经历了好几个阶段,每个阶段都有优化的空间。

第一个阶段是源文件阶段。美术导出的PNG、TGA、PSD,这些是磁盘上的原始文件。这个阶段影响的是版本库大小和首次导入时间,对运行时几乎没有影响,但如果源文件本身尺寸就不合理(比如一张UI图标做了4096×4096),后面所有环节都会跟着遭殃。

第二个阶段是导入设置阶段。Unity的Texture Importer会读取源文件,根据你设置的Max Size、Compression、Format等参数,生成一份平台相关的纹理数据。这份数据会被存进Library文件夹,最终打进AssetBundle或者随包发布。这个阶段是优化的主战场,因为绝大多数参数都在这里控制。

第三个阶段是运行时加载阶段。纹理被加载到内存,如果是压缩格式,GPU可以直接采样;如果是非压缩格式,还需要额外的解压或者直接以原始数据上传。这个阶段影响的是内存占用和加载耗时。

第四个阶段是渲染采样阶段。GPU从纹理里读取像素,进行过滤、Mipmap选择、各向异性过滤等操作。这个阶段影响的是渲染性能和画质表现。

理解这条链路的意义在于:不同阶段的优化手段解决的是不同的问题。源文件尺寸解决的是版本库和导入效率,导入设置解决的是包体和显存,运行时加载解决的是内存峰值和卡顿,渲染采样解决的是GPU开销。你不能指望改一个参数就把所有问题都解决了。

2.2 纹理内存到底是怎么算出来的

要优化,先得会算账。一张纹理占多少内存,公式其实很简单:

内存占用 = 宽 × 高 × 每像素字节数 × Mipmap系数

每像素字节数取决于纹理格式。常见的几种:

格式每像素字节说明
RGBA324未压缩,画质最好,内存最大
RGB243去掉Alpha通道
RGBA44442每通道4位,画质损失明显
RGB5652无Alpha,绿色通道精度更高
ASTC 4x41压缩格式,块大小4x4
ASTC 6x60.44压缩率更高,画质略降
ASTC 8x80.25压缩率很高,适合远景
ETC2 4bit0.5移动端常用
ETC2 8bit1画质更好

Mipmap系数:如果开启了Mipmap,总内存会增加约33%。因为Mipmap会生成一系列逐级减半的纹理,总大小是原始大小的1/3左右。

举个例子,一张1024×1024的RGBA32纹理,不开Mipmap是1024×1024×4 = 4MB;开Mipmap大约是5.33MB。如果换成ASTC 6x6,不开Mipmap大约是1024×1024×0.44 ≈ 0.44MB,差了十倍以上。这就是为什么压缩格式的选择如此关键。

我建议你在项目里养成一个习惯:拿到任何一张图,先算一下它在目标平台上的内存占用。如果一张UI背景图在手机上占了8MB,那它大概率是没压缩的RGBA32,这时候你就知道该动哪里了。

2.3 压缩格式不是随便选的

Unity支持的纹理压缩格式很多,但不同平台支持的格式不一样,这是很多人踩坑的地方。

PC平台主要用DXT系列(也叫S3TC)。DXT1每像素0.5字节,适合没有Alpha或者只有1位Alpha的图;DXT5每像素1字节,适合有渐变Alpha的图。PC上一般不需要考虑ASTC,因为不是所有显卡都支持。

Android平台情况最复杂。ETC2是OpenGL ES 3.0的标准,基本所有现代安卓机都支持;ASTC是OpenGL ES 3.1以上才支持,覆盖率现在也很高了,但极老的设备可能不支持。另外还有PVRTC,那是iOS专属的,安卓上用不了。

iOS平台从A8芯片开始支持ASTC,现在基本可以放心用。PVRTC虽然也支持,但画质和压缩率都不如ASTC,新项目不建议再用。

WebGL平台比较特殊,通常用DXT或者ETC,取决于浏览器和硬件支持。

这里有个实操建议:在Unity的Texture Importer里,不要只设一个Default格式就完事,要针对不同平台分别设置Override。比如Android上设ASTC 6x6作为默认,对老设备再单独设一个ETC2的Fallback。这样既能保证新设备的画质和压缩率,又能兼容老设备。

3. 导入设置里那些真正影响性能的选项

3.1 Max Size设多少才合理

Max Size是导入设置里最直观的一个参数,它决定了纹理被导入后的最大边长。比如你设成1024,一张2048×2048的图会被自动缩到1024×1024。

这个参数的核心逻辑是:纹理在屏幕上的实际显示尺寸决定了你需要多大的分辨率。一张UI背景图如果最终在屏幕上只占512×512像素,那你给它2048的分辨率就是浪费,多出来的像素GPU根本采样不到,白白占内存。

但这里有个坑:Max Size是2的幂次,Unity只接受2的幂次值(32、64、128、256、512、1024、2048、4096)。如果你设成1000,Unity会自动向上取到1024。所以设置的时候直接选2的幂次就好。

具体怎么定?我的经验是分场景:

  • UI元素:按设计分辨率来。如果项目是1920×1080的设计分辨率,那全屏背景图最大2048就够了,普通按钮图标256或512足够。
  • 角色贴图:看角色在屏幕上的占比。主角一般1024或2048,NPC和远景角色512或1024。
  • 场景贴图:地面、墙面这种大面积但细节要求不高的,1024或2048;小物件512足够。
  • 特效贴图:一般256或512,因为特效本身变化快,玩家不会盯着看细节。
  • 字体图集:看字号和字符集大小,一般1024或2048。

注意:Max Size改小之后,一定要在Scene视图里实际看一下效果。有些图缩小之后边缘会出现锯齿或者模糊,特别是带细线条的UI图标。如果画质不可接受,要么保持原尺寸,要么让美术重新设计更简洁的版本。

3.2 Compression选项的取舍

Compression这一栏有三个选项:None、Low Quality、Normal Quality、High Quality。很多人以为Quality越高越好,其实不是。

None就是不压缩,纹理以原始格式存储。这个选项只适合极少数情况,比如需要精确像素操作的图(某些Shader会读取纹理像素做计算),或者压缩后画质实在无法接受的图。绝大多数情况下不要选None。

Low Quality压缩速度快,但画质损失大,压缩率也一般。适合对画质要求极低、但数量巨大的图,比如某些粒子特效的噪声图。

Normal Quality是默认选项,平衡了画质和压缩率,大多数图用这个就行。

High Quality压缩速度慢,但画质更好,压缩率也略高。适合那些对画质敏感、但数量不多的图,比如UI主界面背景、角色脸部贴图。

这里有个经验:Compression Quality影响的是压缩算法本身的精度,不是最终纹理格式。也就是说,你选了ASTC 6x6,不管Quality选什么,最终格式都是ASTC 6x6,只是压缩过程中的误差大小不同。所以不要指望把Quality调到High就能把ASTC 6x6变成ASTC 4x4的画质。

3.3 Mipmap不是所有图都该开

Mipmap是一组逐级减半的纹理副本,用于在物体远离摄像机时自动切换到更低分辨率的版本。它的好处是减少远处物体的纹理闪烁(摩尔纹),同时降低GPU采样开销。坏处是内存增加约33%。

哪些图该开Mipmap?

  • 3D场景里的物体贴图:该开。因为物体会远近变化,不开Mipmap远处会闪烁。
  • 角色贴图:该开。角色会跑远跑近。
  • UI贴图:一般不开。UI元素通常以固定尺寸显示,不会因为"距离"变化而需要不同分辨率。开了反而浪费内存。
  • 天空盒:该开。天空盒虽然远,但覆盖屏幕面积大,不开会有明显闪烁。
  • 粒子特效:看情况。如果粒子会在3D空间里远近变化,该开;如果是屏幕空间的UI特效,不开。

还有一个参数叫Mipmap Filtering,控制Mipmap之间的过渡方式。Point是直接切换,Bilinear是线性插值。一般用Bilinear就行,Point在某些风格化渲染里有用。

3.4 Read/Write Enabled的隐藏代价

这个选项默认是关闭的,但很多人为了在代码里读取纹理像素(比如做像素级碰撞检测、动态修改纹理),会把它打开。打开之后,Unity会在内存里保留一份纹理的CPU可读副本,这意味着内存占用直接翻倍。

更麻烦的是,这份CPU副本在纹理上传到GPU之后其实就不需要了,但Unity不会自动释放,除非你手动调用Texture2D.Apply(false, true)把CPU副本丢掉。很多人不知道这一点,开了Read/Write之后内存一直居高不下。

我的建议是:除非真的需要在运行时读取像素,否则一律关闭。如果确实需要,用完立刻释放CPU副本。另外,如果只是偶尔需要读取,可以考虑在需要的时候临时用RenderTexture或者Texture2D的GetPixels从GPU读回,而不是一直开着Read/Write。

3.5 Generate Physics Shape和Alpha Source的细节

这两个参数容易被忽略,但在特定场景下会影响性能。

Generate Physics Shape是给2D物理用的,会为Sprite生成碰撞形状。如果你不用2D物理,关掉它,能省一点导入时间和内存。

Alpha Source有三个选项:None、Input Texture Alpha、From Gray Scale。如果你确定一张图不需要Alpha通道,选None,Unity会把它当成RGB处理,内存直接省25%。很多美术导出的图其实Alpha通道全是255(完全不透明),这种图就应该把Alpha Source设成None。

还有一个相关的参数是Alpha Is Transparency,开启后Unity会对Alpha边缘做处理,减少黑边。这个对UI图很有用,但会增加一点导入时间。

4. 图集策略:省DrawCall还是省内存

4.1 图集到底解决了什么问题

图集(Atlas)的核心价值是减少DrawCall。在UI渲染里,如果每个按钮、每个图标都是独立的纹理,那每个纹理至少一个DrawCall,一个界面几十个元素就是几十个DrawCall。把这些小图打包成一张大图之后,它们共享同一个材质和纹理,DrawCall就能合并成几个。

但图集不是没有代价的。首先,图集本身是一张大纹理,如果打包策略不当,会浪费大量空白区域。其次,图集里的图如果分辨率不一致,会被统一到图集的最大尺寸,小图可能被放大。最后,图集更新时,整张图集都要重新加载,可能造成卡顿。

4.2 什么时候该用图集,什么时候不该用

我的经验是分这几类:

该用图集的:

  • UI界面的小图标、按钮背景、边框
  • 同一角色的多个部位贴图(如果它们总是同时出现)
  • 同一场景的多个小物件贴图

不该用图集的:

  • 大尺寸背景图(单独一张就很大,打进图集浪费空间)
  • 不同场景/不同界面才用到的图(打包在一起会导致加载冗余)
  • 需要独立控制压缩格式的图(图集只能有一种格式)
  • 需要独立设置Mipmap的图

这里有个常见的误区:很多人觉得图集越大越好,把所有UI图打进一张4096×4096的图集。结果就是,打开任何一个界面都要加载整张图集,内存峰值很高,而且图集里大部分图当前界面根本用不到。正确的做法是按功能模块拆分图集,比如登录界面一个图集、主界面一个图集、战斗界面一个图集,每个图集控制在1024或2048以内。

4.3 Sprite Atlas的配置要点

Unity现在推荐用Sprite Atlas(而不是旧的Sprite Packer)。配置的时候有几个关键点:

Type选Master还是Variant?Master是主图集,Variant是变体。变体可以基于主图集生成不同分辨率或不同压缩格式的版本,适合做高清/低清两套资源。但变体会增加管理复杂度,小项目可以不用。

Include in Build:如果勾选,图集会自动打进包体;如果不勾,需要手动加载。对于按需加载的图集,建议不勾,用Addressables或AssetBundle管理。

Allow Rotation:允许图集内的图旋转以节省空间。对UI图一般关掉,因为旋转后的图在代码里取出来还要转回去,容易出问题。对纯装饰性的图可以开。

Tight Packing:紧密打包,按图的实际轮廓而不是矩形边界来排列。能省空间,但可能增加图集生成的复杂度。UI图一般关掉,因为UI图通常有透明边距,紧密打包反而可能出问题。

Padding:图与图之间的间距。一般设2到4像素,防止采样时相邻图互相干扰(特别是开了Mipmap或者用了Bilinear过滤的时候)。

4.4 图集拆分的实操判断标准

怎么判断一个图集该不该拆?我通常看这几个指标:

  • 图集利用率:Unity在打包后会显示图集的填充率。如果低于70%,说明有优化空间,要么把更多图加进来,要么把图集尺寸调小。
  • 加载频率:如果图集里有些图是A界面用的,有些是B界面用的,而A和B不会同时出现,那就该拆。
  • 内存峰值:如果一张图集加载后内存增加超过10MB,就要考虑拆了。移动端尤其敏感。
  • 更新频率:如果图集里有些图经常改,有些很稳定,拆开可以避免每次改图都重新打包整张图集。

5. 运行时加载与卸载的坑

5.1 Resources文件夹为什么被诟病

Unity的Resources文件夹用起来很方便,Resources.Load一行代码就能加载资源。但它有几个致命问题:

第一,Resources文件夹里的所有资源都会被无条件打进包体,不管你有没有用到。这意味着你放进去一张图,哪怕从来没加载过,它也会占包体空间。

第二,Resources文件夹的资源无法按需更新。它们被编进了一个特殊的序列化文件里,要更新只能整个包更新。

第三,Resources.Load的加载是同步的,大图加载会造成明显卡顿。

第四,Resources文件夹的资源卸载依赖Resources.UnloadUnusedAssets,这个函数会扫描整个场景,开销很大,而且时机不好控制。

所以我的建议是:新项目尽量不要用Resources文件夹,改用Addressables或者自己管理的AssetBundle。如果老项目已经在用,逐步迁移出来。

5.2 Addressables的引用计数陷阱

Addressables是Unity现在主推的资源管理方案,它用引用计数来管理资源生命周期。加载时计数加一,释放时计数减一,计数归零才真正卸载。

这个机制本身没问题,但坑在于:很多人只记得加载,忘了释放。比如在一个界面打开时加载了图集,界面关闭时没有调用Release,那这个图集的引用计数永远不归零,内存就一直占着。

更隐蔽的是,同一个资源被多个地方加载时,每次加载都会增加引用计数。如果你在A处加载了一次,在B处又加载了一次,那必须释放两次才能卸载。很多人只释放一次,导致资源泄漏。

我的做法是:封装一层资源管理类,统一管理加载和释放。每次加载返回一个句柄,句柄销毁时自动释放。这样就不会漏掉。

5.3 纹理卸载的时机与方式

纹理卸载有两种方式:Resources.UnloadAsset和Resources.UnloadUnusedAssets。

UnloadAsset是精确卸载指定的资源,开销小,但要求你明确知道要卸载哪个。适合自己管理资源生命周期的项目。

UnloadUnusedAssets是扫描所有资源,卸载没有被引用的。开销大,会卡顿,一般只在场景切换或者Loading界面时调用。

这里有个关键点:纹理被卸载后,如果材质还在引用它,材质会变成粉色(丢失纹理)。所以卸载纹理之前,一定要确保没有材质、SpriteRenderer、Image等组件还在引用它。

另外,AssetBundle的卸载和纹理卸载是两回事。AssetBundle.Unload(false)只卸载AB文件本身,不卸载从它加载出来的资源;AssetBundle.Unload(true)会强制卸载所有从该AB加载的资源,包括还在使用的,这会导致引用丢失。一般用Unload(false),然后手动管理资源释放。

5.4 异步加载与内存峰值控制

大图集加载时,如果同步加载,会造成明显卡顿。用异步加载可以缓解,但要注意内存峰值。

异步加载的过程中,纹理数据会先被读进内存,然后上传到GPU,最后释放CPU副本。这个过程中,内存里同时存在CPU副本和GPU副本,峰值是正常情况的两倍。如果同时加载多张大图,峰值会更高,可能导致低端机OOM(内存溢出)。

控制峰值的方法:

  • 限制同时加载的纹理数量,用队列逐个加载。
  • 加载完成后立即释放CPU副本,调用texture.Apply(false, true)。
  • 在Loading界面做加载,给玩家一个预期,避免游戏中途卡顿。
  • 监控内存,用Profiler或者Profiler.GetTotalAllocatedMemoryLong实时观察。

6. 那些年我踩过的图片资源坑

6.1 压缩后画质崩了的排查思路

压缩后画质崩了,通常有几个原因:

原因一:格式选错了。比如一张带渐变Alpha的图用了ETC2(ETC2的Alpha是1位的,只有全透明和全不透明),渐变部分会变成硬边。这种图应该用ASTC或者RGBA32。

原因二:压缩块大小太大。ASTC 8x8的压缩率很高,但对细节丰富的图(比如文字、细线条)会造成明显模糊。这种图应该用ASTC 4x4或6x6。

原因三:Mipmap导致的模糊。如果一张图在屏幕上显示得比它的Mipmap级别更清晰,GPU会采样到模糊的Mipmap。这种情况要么关掉Mipmap,要么调整Mipmap Bias。

原因四:Filter Mode设置不当。Point过滤会导致像素化,Bilinear会导致模糊,Trilinear更模糊但过渡更平滑。UI图一般用Bilinear,像素风游戏用Point。

排查的时候,我通常这样做:先在Texture Importer的预览窗口里看压缩后的效果,Unity会显示压缩前后的对比。如果预览里就有问题,那就是格式或参数的问题;如果预览没问题但游戏里有问题,那可能是Mipmap或者Filter Mode的问题。

6.2 图集打包后出现黑边或缝隙

这是图集最常见的问题之一。原因通常是Padding不够或者Filter Mode设置不当。

当GPU采样一张图的边缘时,如果Filter Mode是Bilinear或Trilinear,它会采样到相邻像素。如果这张图旁边紧挨着另一张图,就会把另一张图的像素采样进来,形成黑边或缝隙。

解决方法:

  • 增加Padding,一般2到4像素。但Padding太大会浪费图集空间。
  • 把Filter Mode改成Point,但这样画质会变硬。
  • 在图的边缘留透明边距,让美术导出图的时候多留几个像素的透明区域。
  • 使用Sprite Atlas的Padding设置,Unity会自动在图之间加间距。

我一般用Padding 4 + Bilinear,大多数情况没问题。如果还有黑边,就检查一下图本身是不是有非透明的边缘像素。

6.3 低端机上的纹理内存溢出

低端机的纹理内存通常很有限,可能只有几百MB。如果纹理没压缩好,很容易OOM。

我遇到过一次典型情况:一个2D游戏,所有UI图都是RGBA32,图集是4096×4096。在高端机上跑得好好的,一到低端机就闪退。算一下内存:4096×4096×4 = 64MB,一张图集就64MB,加上其他纹理,轻松超过低端机的内存上限。

解决方案:

  • 所有UI图改用ASTC或ETC2,内存直接降到1/8到1/4。
  • 图集尺寸降到2048,内存再降一半。
  • 按界面拆分图集,不要一张图集装所有UI。
  • 低端机用低分辨率版本,通过Quality Settings或者AssetBundle Variant实现。

6.4 纹理格式在Editor和真机上不一致

这是一个非常隐蔽的坑:Unity在Editor里显示的纹理格式,和真机上可能不一样。

原因是Editor平台(Windows/Mac)支持的纹理格式和移动平台不一样。你在Editor里看到的是DXT格式,但真机上可能是ETC2或ASTC。如果你只在Editor里测试,可能发现不了真机上的画质问题。

解决方法:用Build后的真机测试,或者用Unity的Frame Debugger查看真机上的实际纹理格式。另外,在Texture Importer里设置平台Override的时候,要确保每个目标平台都设了正确的格式,不要依赖Default。

6.5 动态加载纹理时的内存碎片

频繁加载和卸载纹理会导致内存碎片,最终即使总内存够用,也可能因为找不到连续的大块内存而OOM。

避免碎片的方法:

  • 尽量复用纹理,不要频繁创建和销毁。
  • 用对象池管理纹理,需要时从池里取,不用时还回去。
  • 避免频繁调用UnloadUnusedAssets,它会产生大量碎片。
  • 大图集尽量在Loading时一次性加载,不要边玩边加载。

7. 一套可落地的图片资源优化流程

7.1 项目启动阶段的规范制定

在项目刚开始的时候,就应该定好图片资源的规范。我通常会和美术、策划一起确定这几件事:

命名规范:比如ui_开头的是UI图,char_开头的是角色图,env_开头的是场景图。这样在AssetBundle打包或者Addressables分组的时候,可以按前缀自动分组。

尺寸规范:规定不同用途的图的最大尺寸。比如UI图标不超过256,UI背景不超过2048,角色贴图不超过2048。美术导出的时候就按这个来,避免后期返工。

格式规范:规定不同用途的图的压缩格式。比如UI图用ASTC 6x6,角色图用ASTC 4x4,场景图用ASTC 6x6。这样导入设置可以批量处理。

Alpha规范:规定哪些图需要Alpha通道,哪些不需要。不需要Alpha的图,美术导出时就删掉Alpha通道,省内存。

7.2 导入设置的批量处理

Unity的Texture Importer支持Preset(预设),可以把一套设置保存成预设,然后批量应用到多张图。这是提高效率的关键。

创建Preset的步骤:

  1. 选中一张图,在Inspector里设好所有参数。
  2. 点击Inspector右上角的Preset图标,选择Save Preset。
  3. 给Preset起个名字,比如UI_Icon。
  4. 以后选中其他图,在Preset下拉里选这个预设,一键应用。

更高效的方式是用AssetPostprocessor脚本,在图片导入时自动应用设置。比如根据文件名前缀判断类型,自动设置对应的格式和尺寸。这样美术同学导出图之后,不需要手动设置,Unity会自动处理。

using UnityEditor; using UnityEngine; public class TextureImportProcessor : AssetPostprocessor { void OnPreprocessTexture() { TextureImporter importer = assetImporter as TextureImporter; if (importer == null) return; string path = assetPath.ToLower(); if (path.Contains("/ui/")) { importer.textureType = TextureImporterType.Sprite; importer.maxTextureSize = 1024; importer.mipmapEnabled = false; importer.textureCompression = TextureImporterCompression.CompressedHQ; TextureImporterPlatformSettings androidSettings = new TextureImporterPlatformSettings(); androidSettings.name = "Android"; androidSettings.overridden = true; androidSettings.format = TextureImporterFormat.ASTC_6x6; androidSettings.maxTextureSize = 1024; importer.SetPlatformTextureSettings(androidSettings); TextureImporterPlatformSettings iosSettings = new TextureImporterPlatformSettings(); iosSettings.name = "iPhone"; iosSettings.overridden = true; iosSettings.format = TextureImporterFormat.ASTC_6x6; iosSettings.maxTextureSize = 1024; importer.SetPlatformTextureSettings(iosSettings); } else if (path.Contains("/char/")) { importer.textureType = TextureImporterType.Default; importer.maxTextureSize = 2048; importer.mipmapEnabled = true; importer.textureCompression = TextureImporterCompression.CompressedHQ; TextureImporterPlatformSettings androidSettings = new TextureImporterPlatformSettings(); androidSettings.name = "Android"; androidSettings.overridden = true; androidSettings.format = TextureImporterFormat.ASTC_4x4; androidSettings.maxTextureSize = 2048; importer.SetPlatformTextureSettings(androidSettings); } } }

这个脚本会在图片导入时自动根据路径设置参数,省去手动操作。注意路径判断要用小写,因为不同操作系统的大小写规则不一样。

7.3 用Profiler定位纹理问题

Unity Profiler是定位纹理问题的利器。在Profiler的Memory区域,可以看到当前所有纹理的内存占用。按大小排序,找出占用最大的几张图,重点优化。

另外,Frame Debugger可以查看每一帧的DrawCall和纹理绑定情况。如果发现某个DrawCall绑定了很多纹理,说明图集没打好;如果发现纹理格式是RGBA32,说明压缩没生效。

还有一个工具是Memory Profiler(需要单独安装Package),可以抓取详细的内存快照,看到每个纹理的引用链。这对于排查"为什么这张图没被卸载"特别有用。

7.4 打包后的资源审计

每次打包之后,都应该做一次资源审计。Unity在Build Log里会输出资源列表,包括每个资源的大小和来源。分析这个Log,可以找出:

  • 哪些纹理占用了最多包体空间
  • 哪些纹理被打进了不该进的包
  • 哪些纹理的格式不符合预期

我通常会把Build Log导入到Excel或者用脚本分析,按大小排序,找出Top 20的纹理,逐个检查它们的导入设置是否合理。这个习惯能帮你发现很多隐藏的问题。

7.5 持续监控与迭代

图片资源优化不是一次性的工作,而是持续的过程。项目进行中,美术会不断加新图,策划会不断改UI,每次改动都可能引入新的问题。

我的做法是:

  • 每周做一次资源审计,检查新增的图是否符合规范。
  • 在CI流程里加入资源检查,如果发现不符合规范的图,自动报警。
  • 在真机上定期测试,特别是低端机,观察内存和帧率变化。
  • 建立资源规范文档,新加入的成员必须阅读并遵守。

8. 一些容易被忽略的细节和技巧

8.1 纹理的sRGB选项

Texture Importer里有一个sRGB选项,默认是勾选的。这个选项影响的是纹理的颜色空间。如果项目用的是Linear颜色空间(现代项目一般都推荐用Linear),那颜色纹理(比如漫反射贴图)应该勾选sRGB,数据纹理(比如法线贴图、粗糙度贴图)应该取消勾选。

勾错了会导致颜色偏暗或者偏亮,而且这种问题在Editor里可能不明显,真机上才暴露出来。所以导入的时候一定要确认这个选项。

8.2 法线贴图的特殊处理

法线贴图不能用普通的压缩格式,因为法线贴图存储的是向量数据,不是颜色数据。压缩算法对颜色数据的误差容忍度高,但对向量数据的误差会导致光照计算错误。

Unity对法线贴图有专门的格式:Normal Quality会把它转成DXT5nm或者ASTC的法线专用格式。导入的时候,Texture Type要选Normal Map,Unity会自动处理。

另外,法线贴图不要开sRGB,不要用Mipmap(或者用但要注意),不要用Bilinear过滤(用Trilinear更好)。

8.3 纹理数组和虚拟纹理

对于大量相似的小纹理(比如地形贴图、角色皮肤变体),可以考虑用Texture Array或者Virtual Texturing。

Texture Array把多张同尺寸的纹理打包成一个数组,Shader里用索引访问。好处是减少DrawCall和纹理绑定次数,坏处是要求所有纹理尺寸和格式一致。

Virtual Texturing是更高级的技术,把大纹理分成小块,按需加载。适合超大世界或者超高分辨率纹理。但Unity的Virtual Texturing支持有限,需要自己实现或者用第三方插件。

8.4 纹理的Streaming

Unity有一个Texture Streaming功能,可以在运行时按需加载Mipmap级别,而不是一次性加载所有Mipmap。这对于大世界游戏很有用,可以显著降低内存峰值。

开启方式:在Quality Settings里开启Texture Streaming,然后设置Memory Budget和Max Reduction。Unity会根据摄像机距离和内存预算,动态调整每个纹理的Mipmap级别。

但Texture Streaming也有坑:它会导致纹理在远处模糊,如果Budget设得太小,近处也会模糊。另外,它和AssetBundle的配合需要额外配置。建议在项目后期、内存确实紧张的时候再考虑开启。

8.5 用Shader做纹理压缩的补充

有些情况下,压缩格式的画质无法接受,但又不能用未压缩格式(内存太大)。这时候可以考虑用Shader做运行时解压或者重建。

比如,把一张图的RGB通道和Alpha通道分开存成两张ETC2纹理,Shader里再合并。这样虽然多了一张纹理,但每张都是压缩的,总内存还是比RGBA32小。

或者,用一张低分辨率的颜色图加一张高分辨率的细节图,Shader里叠加。这样可以用较小的内存获得较好的画质。

这些技巧需要根据具体场景设计,不是万能的,但在特定情况下能解决棘手的问题。

8.6 纹理的异步上传

在支持多线程渲染的平台上,纹理上传到GPU可以在渲染线程异步进行,避免阻塞主线程。Unity的Texture2D.Apply有一个makeNoLongerReadable参数,设为true可以释放CPU副本,同时让上传异步化。

但异步上传有个问题:如果纹理还没上传完就被渲染,会显示为黑色或者粉色。所以需要配合AsyncGPUReadback或者自己管理上传状态。

对于大图集,我通常会在Loading界面预加载,确保上传完成后再进入游戏。这样虽然Loading时间长一点,但游戏内不会卡顿。

9. 关于图片资源优化,我个人的几条经验

做了这么多项目,关于图片资源优化,我有几条体会想分享。

第一条:优化要趁早。项目前期定好规范,比后期返工省事十倍。我见过太多项目,前期不管,后期发现包体超标,然后花几周时间回头改图,改完还要重新测试所有界面,成本极高。

第二条:不要追求极致压缩。压缩率和画质是一对矛盾,压得太狠画质崩了,玩家体验差,反而得不偿失。找到平衡点,在可接受的画质下尽量压缩,而不是压到极限。

第三条:用数据说话。不要凭感觉判断哪张图该优化,用Profiler和Memory Profiler看实际数据。有时候你以为是纹理的问题,其实是Mesh或者动画的问题。

第四条:建立自动化流程。手动设置每张图的导入参数既费时又容易出错,用AssetPostprocessor和Preset自动化,能省大量时间,也能保证一致性。

第五条:真机测试不可少。Editor里一切正常,真机上可能完全不是一回事。特别是纹理格式和内存,Editor和真机差异很大。每次优化之后,一定要在目标真机上验证。

第六条:文档和沟通。图片资源优化不是一个人的事,需要美术、策划、程序一起配合。把规范写成文档,让所有人都知道该怎么做,比一个人埋头优化有效得多。

最后分享一个小技巧:如果你不确定一张图该用什么格式,可以先用Unity的预览功能看压缩效果,然后在真机上跑一下,用Frame Debugger看实际格式。如果画质可接受、内存也降下来了,那就是对的。如果画质崩了,就换更高质量的格式或者降低压缩率。这个过程可能需要试几次,但试过之后你就对每种格式的效果有感觉了,以后设置起来就快了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进