做游戏开发的朋友应该都有类似体验:项目做了一两个月,包体悄悄变大,游戏启动时加载越来越慢,到了真机上显存告急、掉帧频发。检查来检查去,DrawCall 压了、Shader 优化了、LOD 也做了,最后才发现罪魁祸首往往是纹理资源。
纹理是游戏画面里体积最大、数量最多的资源之一。一张 2048×2048 的 RGBA 32 位贴图,原始数据就有 16MB;一个场景里几十张贴图,光显存占用就是几百 MB。纹理压缩,就是在这样的背景下必须掌握的一项基本功。
本文会围绕纹理压缩的原理、主流格式、Unity 工程里的实战配置和批处理脚本展开,帮助你理清“什么时候该用哪种压缩格式”“为什么内存会不降反升”“ASTC 和 ETC2 到底怎么选”这些问题。适合做 Unity 客户端、移动端游戏、或者刚接触资源优化的开发者阅读。
1. 为什么要关注纹理压缩
1.1 一个容易被忽视的性能瓶颈
很多初学 Unity 的开发者会把注意力集中在 Shader 和脚本逻辑上,很少关注纹理本身的存储形态。但纹理在游戏运行时的影响是系统性的:
- 包体大小:纹理资源是安装包体积的主要来源之一。未压缩的 2048 纹理一张就是十几 MB,稍微多一点美术资源,包体立刻膨胀。
- 加载速度:纹理数据越大,从磁盘读取、解压、上传到 GPU 的时间就越长。首次进入场景的卡顿,很大概率是纹理加载引起的。
- 内存和显存占用:运行时 RGBA 原始纹理需要占用大量内存。移动端 GPU 和内存本身就很紧张,纹理一旦失控,掉帧和闪退几乎是必然的。
- 带宽压力:即使纹理已经上传到 GPU,绘制时 GPU 仍需要从显存中取数据。未压缩纹理的带宽占用远高于压缩纹理,会影响填充率和整体帧率。
所以,纹理压缩并不只是一个“省包体”的操作,它同时影响 CPU、内存、显存、带宽和加载速度五个维度。
1.2 纹理压缩解决什么问题
纹理压缩解决的问题非常直接:把纹理数据在存储和传输阶段压缩,同时在显存里保持 GPU 可以直接采样的形式。
它的目标不是“让图片看起来更小”,而是:
- 减少包体中的资源体积。
- 减少运行时显存占用。
- 减少 GPU 采样时的带宽消耗。
- 保证 GPU 可以在绘制时直接读取压缩后的数据,并完成实时解码。
这里的关键点是“GPU 直接采样”。普通的图片压缩格式(比如 PNG、JPG)在 GPU 中是不能直接使用的。它们需要先被 CPU 解压还原成 RGBA 像素,再上传到显存。而 GPU 纹理压缩格式,例如 BC 系列、ETC、ASTC,可以直接放在显存中,由 GPU 的专用硬件解码单元在采样时实时解压。
这正是纹理压缩和普通图片压缩最本质的区别。
1.3 纹理压缩不是“把图片压成 JPG”
这里需要先做一个概念区分:
- 存储压缩:PNG 无损耗、JPG 有损耗,都是为了减小文件体积。它们适合 UI、图集、美术原图这些“需要再编辑”的场景。
- GPU 纹理压缩:BC1、BC3、BC7、ETC2、ASTC 等格式,除了减小文件体积,更重要的是让 GPU 能在渲染时直接采样,不需要先解压成完整 RGBA。
在 Unity 中,Texture import 面板里的 Compression 设置就是 GPU 纹理压缩。它与我们平时用 PhotoShop 导出 Web 格式不是一回事。
2. 纹理压缩的核心原理
2.1 分块压缩与调色板思想
GPU 纹理压缩的主流方案,基本都是基于“分块”的思想。
简单理解:把一张纹理切分成固定大小的像素块,例如 4×4 像素为一个块。每个块内部,记录一个基准颜色、一个颜色范围,然后块内的每个像素根据基准颜色和范围插值出近似颜色。这样就不需要为每个像素单独存储 RGBA 值。
这种思路和调色板有些相似:一块区域里颜色变化通常不会太大,用少量颜色信息加上插值规则,就能表达大部分像素。
比如 BC1 格式,一个 4×4 的块只占 64 位(8 字节)数据。如果没有压缩,这 16 个像素用 RGBA 32 位表示是 64 字节,压缩比达到 8:1。
2.2 压缩格式的固定块大小
不同纹理压缩格式的区别,主要在于块大小、颜色精度、是否支持 Alpha、是否有四分量独立压缩等。
常见的格式块大小:
| 格式 | 块大小 | 压缩比(RGBA8 基准) | 质量特点 |
|---|---|---|---|
| BC1 | 4×4,64 bit | 8:1 | 不支持高质量 Alpha,适合不透明贴图 |
| BC3 | 4×4,128 bit | 4:1 | 支持 Alpha,适合法线、透贴 |
| BC7 | 4×4,128 bit | 4:1 | 高质量,支持高动态范围,适合高质量贴图 |
| ETC2 | 4×4,128 bit | 4:1 | Android 兼容性强,支持 Alpha |
| ASTC 4x4 | 4×4,128 bit | 4:1 | 高质量,移动端最强 |
| ASTC 8x8 | 8×8,128 bit | 16:1 | 压缩比大,适合背景、远景 |
这里的压缩比是相对的,具体视觉效果还需要结合纹理内容判断。ASTC 的灵活性在于块可以从 4×4 到 12×12 任意选择,开发者能在质量和体积之间做非常细的调节。
2.3 GPU 硬件解码与 CPU 解压的区别
前面提到,GPU 纹理压缩可以直接被硬件采样。这里再稍微展开一下:
- 使用 PNG、JPG 这类图片格式时,Unity 会在加载阶段通过 CPU 解压,把图片转换成 RGBA 原始数据,然后通过 API 上传到显存。这个过程既费 CPU,又占额外的临时内存。
- 使用 DXT、ETC2、ASTC 等格式时,纹理在磁盘上就是 GPU 压缩后的数据。加载到显存后,GPU 在 Fragment Shader 采样时,会通过纹理单元的硬件解码器实时解压,不占用 CPU 计算资源,也不需要维持一份完整的 RGBA 转存。
所以,纹理压缩不仅仅是节省存储空间,它真正改变了纹理资源在运行时的生命周期。
3. 主流纹理压缩格式对比与选型
3.1 DXT/BC 系列:PC 老牌压缩格式
DXT 系列是 DirectX 时代的纹理压缩格式,后来在 GPU 业界统一扩展为 BC(Block Compression)命名。
- BC1:对应 DXT1,适合不透明的颜色贴图。
- BC3:对应 DXT5,具备独立的 Alpha 通道,适合普通贴图和透贴。
- BC5:适合法线贴图,两个通道精度更高。
- BC7:高质量格式,用于高质量纹理,PC 和 Xbox 平台支持度高。
在 PC 端开发时,BC 系列是默认选择。Unity 的 Standalone 平台默认纹理格式通常是 BC1/BC3,BC7 则在高压缩质量项目中很常见。
3.2 ETC 系列:Android 高清纹理的主流
ETC1 是安卓平台早年常用的格式,但它不支持 Alpha 通道,所以当时开发者常常把一张纹理拆成颜色贴图和 Alpha 贴图两张来处理,带来不少麻烦。
ETC2 是 OpenGL ES 3.0 之后强制支持的格式,兼容性和表现都比 ETC1 好很多,支持 Alpha,压缩质量也更高。Unity 打包 Android 时,如果设备支持 OpenGL ES 3.0,默认就会使用 ETC2。
但 ETC2 也有局限:压缩质量相对粗糙,在颜色渐变、高对比度区域容易出现色块和噪点。对于要求比较高的贴图,ETC2 不一定够用。
3.3 ASTC:移动端全能型选手
ASTC(Adaptive Scalable Texture Compression)是 ARM 推出的纹理压缩格式,支持从 4×4 到 12×12 的块尺寸,兼容度极高。
它的优势非常明显:
- 单格式适配 Android 和 iOS,无需分别导出两套资源。
- 块尺寸可调,质量与体积之间能灵活平衡。
- 支持 RGBA、HDR 甚至 3D 纹理。
- 在相同压缩比下,画质通常优于 ETC2 和 BC 系列。
现在主流的移动设备基本都支持 ASTC。Unity 在 Android 平台推荐的纹理压缩格式就是 ASTC。
3.4 格式选择速查表
| 目标平台 | 推荐格式 | 说明 |
|---|---|---|
| Android 主流设备 | ASTC 6x6 / ASTC 8x8 | 平衡画质与体积 |
| Android 老设备 | ETC2 | 兼容性兜底 |
| iOS | ASTC 4x4 / 6x6 | 支持良好 |
| PC Standalone | BC7(高质量)/ BC1(普通) | 根据项目画质选择 |
| UI 图集 | RGBA32 / RGBA16 也可,必要时使用 ETC2/ASTC | UI 对画质较敏感,谨慎压缩 |
需要说明的是,这里的格式选择要结合项目实际设备池决定。如果主打 iOS 平台,ASTC 是首选;如果面向低端安卓机,可能需要用 ETC2 或者同时保留多套资源。
4. 实战环境准备
4.1 使用 Unity 作为示例
纹理压缩在不同的引擎中配置方式不同,但最直观、最常见的还是在 Unity 中配置。所以本文以 Unity 为例,讲解纹理导入、平台覆盖和脚本批处理。
需要说明的是,Unity 版本和 API 在持续变化。本文示例代码基于 Unity 2021 LTS / 2022 LTS 时代的常用 API,新版本如果遇到 API 调整,请以项目实际版本的帮助文档为准。
4.2 准备测试纹理
建议准备一张 2048×2048 的 RGBA 贴图和一张带有透明通道的表情贴图,放入项目Assets/Textures/目录下。
测试纹理可以来自美术资源,也可以自己创建一张带渐变色、文字、网格线的测试图。有文字和网格线更容易发现压缩后的锯齿和色块问题。
4.3 项目目录结构
为了配合后面的批处理脚本,建议目录结构如下:
Assets/ ├── Editor/ │ └── TextureCompressionBatchTool.cs └── Textures/ ├── character_color.png └── item_icon.pngEditor目录下的脚本只会影响编辑器,不会打包进最终项目。
5. Unity 纹理导入与平台设置
5.1 单张纹理的设置路径
在 Unity 中选中一张纹理后,可以在 Inspector 面板看到 Texture Import 设置:
- Texture Type:纹理类型选择 Texture、Sprite、Normal Map 等。
- Max Size:最大导入尺寸,例如 2048、1024。
- Format:平台纹理格式,例如 RGBA32、Compressed、ASTC 等。
- Compression:Faster / Normal / Best Quality 的滑块,影响压缩速度和质量。
- Use Crunch Compression:是否使用 Crunch 压缩,适合优化包体大小,但加载时会解压到其他 GPU 格式。
对单张纹理做平台设置时,最直观的方法是切换到平台覆盖面板,在右边选择 Standalone、iOS、Android 等平台分别设置。
5.2 按平台覆盖:Standalone、iOS、Android
默认情况下,一张纹理在 PC、Android、iOS 三个平台可能使用不同的格式:
- Standalone 可以选 BC7 或 BC1。
- Android 选 ETC2 或 ASTC。
- iOS 选 ASTC 4x4 或 6x6。
在 Inspector 中点击“Override for Android”可以单独设置安卓平台的 Max Size 和压缩格式。这个能力很关键,因为美术资源往往以 PC 画质为标准制作,移动端需要降规格。
5.3 Mipmap、图集与贴图尺寸
- Mipmap:用于场景中随距离变化而逐渐降低分辨率的纹理。如果不开启,远处的纹理会出现闪烁和摩尔纹,但如果用不到又不关,会额外增加约三分之一的显存占用。
- 图集:小纹理尽量合并到图集,减少 DrawCall。
- Max Size:根据贴图实际用途设置。UI 大图一般 2048 够用,普通场景贴图建议不超过 1024。
这里需要提醒的是,Mipmap 和纹理压缩是两件事,不要混为一谈。Mipmap 解决的是采样细节问题,纹理压缩解决的是数据体积问题。
6. 脚本批处理纹理压缩格式
6.1 为什么需要批处理脚本
项目美术资源通常有成百上千张纹理,如果靠手动在 Inspector 里逐张点击修改,效率太低,也容易漏。更合理的做法是写一个 Editor 脚本,在导入后统一设置平台覆盖,并在 CI 或提交前批量校验。
下面我们写一个TextureCompressionBatchTool编辑器工具,它会扫描Assets/Textures目录下的所有纹理,并给 Android 平台设置 ASTC 6x6。
6.2 创建 Editor 工具脚本
新建文件Assets/Editor/TextureCompressionBatchTool.cs,粘贴以下代码:
using UnityEditor; using UnityEngine; public static class TextureCompressionBatchTool { private const string TextureRoot = "Assets/Textures"; private const string AndroidPlatform = "Android"; private const string IOSPlatform = "iPhone"; [MenuItem("Tools/Texture/Apply Android ASTC")] public static void ApplyAndroidASTC() { string[] guids = AssetDatabase.FindAssets("t:Texture", new[] { TextureRoot }); int changedCount = 0; foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; if (importer == null) { continue; } bool dirty = false; TextureImporterPlatformSettings androidSettings = importer.GetPlatformTextureSettings(AndroidPlatform); if (androidSettings.format != TextureImporterFormat.ASTC_6x6) { androidSettings.overridden = true; androidSettings.format = TextureImporterFormat.ASTC_6x6; importer.SetPlatformTextureSettings(androidSettings); dirty = true; } if (dirty) { importer.SaveAndReimport(); changedCount++; } } Debug.Log($"[TextureCompressionBatchTool] 扫描纹理 {guids.Length} 张,修改 {changedCount} 张。"); } [MenuItem("Tools/Texture/Print Texture Info")] public static void PrintTextureInfo() { string[] guids = AssetDatabase.FindAssets("t:Texture", new[] { TextureRoot }); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; if (importer == null) { continue; } TextureImporterPlatformSettings androidSettings = importer.GetPlatformTextureSettings(AndroidPlatform); Debug.Log($"{path} -> Android Format: {androidSettings.format}, Overridden: {androidSettings.overridden}"); } } }6.3 代码说明
AssetDatabase.FindAssets("t:Texture", ...)用于按类型查找指定目录下的纹理资源。GetPlatformTextureSettings("Android")获取安卓平台的覆盖设置。如果之前没有覆盖,Unity 也会返回一个默认对象。overridden = true表示显式覆盖该平台的格式。format = TextureImporterFormat.ASTC_6x6指定压缩格式。SaveAndReimport()会应用修改并重新导入资源。
注意,这里是基于 Unity 的公共编辑器 API,在绝大多数 LTS 版本中均可使用。个别旧版本对TextureImporterPlatformSettings的支持不同,如果编译报错,建议优先升级项目版本或查阅对应版本的 Scripting API。
6.4 运行与验证
在 Unity 编辑器菜单栏中,点击Tools > Texture > Apply Android ASTC,执行完毕后在 Console 中查看输出。
再点击Tools > Texture > Print Texture Info,检查各纹理的 Android 平台格式是否变成 ASTC 6x6。
如果需要给 iOS 设置 ASTC 4x4,只要把ApplyAndroidASTC方法中的平台名和格式换成:
TextureImporterPlatformSettings iosSettings = importer.GetPlatformTextureSettings(IOSPlatform); if (iosSettings.format != TextureImporterFormat.ASTC_4x4) { iosSettings.overridden = true; iosSettings.format = TextureImporterFormat.ASTC_4x4; importer.SetPlatformTextureSettings(iosSettings); }这样就能在多个平台之间保持清晰的覆盖逻辑。
7. 纹理压缩后如何验证效果
7.1 在 Editor 里查看内存与包体
Unity 的 Build Report 窗口(Window > Analysis > Build Report)可以查看打包后各资源的大小。
在编辑器里,也可以通过选中纹理 resource 文件,在 Inspector 顶部查看导入后的存储大小。需要注意的是,Inspector 显示的是资源源文件大小,和运行时内存占用并不完全一样,但能够反映包体中的资源占比。
另一个实用方法是在Build Settings里打包一次,然后在Build Report中按“Asset”列表查看哪些纹理占用了最多空间,再针对性地调整 Max Size 或压缩格式。
7.2 真机 Profile 注意事项
真机上的显存占用和编辑器里看到的不一样。建议使用 Unity Profiler 的 Memory 模块并连接真机运行,在 Profiler 中查看纹理资源。
重点关注:
- 哪些纹理占用显存超过 10MB。
- 场景贴图是否存在“同贴图多份拷贝”的情况。
- 是否存在未压缩的 RGBA 纹理被大体积加载。
如果某张纹理显示为 RGBA32 而不是预期的 ASTC 或 ETC2,说明它的导入设置没有覆盖成功,或者该项目使用的打包流程没有应用平台覆盖。
7.3 视觉质量对比方式
压缩后一定要做视觉对比,不能只看数据。推荐方法:
- 把压缩前后的纹理截图并排对比。
- 在 Scene 视图或者通过临时 Material 查看纹理贴到模型上的效果。
- 重点检查渐变天空、文字边缘、法线贴图细节、透明边缘。
ASTC 8x8 虽然压缩比很高,但遇到渐变天空和 UI 描边会出现明显色斑。遇到这类资源时,可以单独把格式调成 ASTC 6x6 或 ASTC 4x4。
8. 常见问题与排查
8.1 压缩后发白、变模糊
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 纹理压缩后整体发灰偏白 | 使用了不支持 RGB 的格式,例如把带透明通道的图压成 BC1 | 换成 BC3 / ETC2 / ASTC 并确认 Alpha |
| 纹理变糊、细节丢失 | 压缩比过大,例如 ASTC 10x10 | 改用更高精度的块尺寸 |
| 颜色边缘出现色斑 | 压缩格式精度不足 | 针对渐变贴图单独提高格式精度 |
这里尤其需要注意带 Alpha 的 UI 和多边形贴图。BC1 只有 1 bit 的 Alpha,基本等于不支持透明,很容易把半透明区域变成锯齿状。
8.2 内存没降反增
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 压缩后包体变小,内存却变大 | 开启了 Crunch Compression,加载时解压到普通 RGBA/ETC 格式 | 检查 Crunch 只在打包阶段使用还是运行时也解压 |
| 场景进入后内存峰值仍然高 | 没有正确启用 mipmap 或图集上有重复纹理 | 开启 mipmap 或检查是否有两张同图不同尺寸的拷贝 |
| 动态加载的纹理成为驻留资源 | 资源加载后没有释放 | 检查 AssetBundle 或 Addressables 的释放流程 |
Crunch Compression 是非常典型的“包体小但内存大”的场景。Crunch 格式在磁盘上非常小,但它在加载时会被后台转换为其他 GPU 格式,因此运行时占用的显存可能和普通格式没有本质区别。如果目标是降低内存,重点看 format 而不是磁盘上的 Crunch 大小。
8.3 部分安卓设备不支持 ASTC
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 低端安卓设备上一部分贴图花屏 | 设备不支持 ASTC,但导入时强制设为 ASTC | 增加 ETC2 降级,或维护两套纹理资源 |
| 老机型显示异常 | OpenGL ES 版本过低 | 升级游戏最低 API Level,或对老设备单独打包 ETC2 |
ASTC 是现在的主流,但确实还存在一部分老旧设备不完全支持。稳妥的做法是在打包流程中判断目标设备的 GPU 能力,在低端设备上使用 ETC2 资源包,在主流设备上使用 ASTC 资源包。
8.4 图集与压缩格式冲突
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 图集打包后单个贴图内存翻倍 | 图集引入后改变了纹理尺寸,导致平台格式失效 | 确认图集最终尺寸是否仍满足压缩格式的块对齐要求 |
| UI 图集压缩后文字变模糊 | UI 图集压缩比太高 | UI 图集单独设置高精度格式 |
图集是常见的联合使用方案,但它会导致纹理尺寸变成多张子图加边距后的总和。如果一张 1024×1024 的 UI 图集被设置成 ASTC 8x8,文字清晰度会受影响。UI 贴图建议单独使用较低压缩比的格式,甚至使用 RGBA16/RGBA32。
9. 最佳实践与工程建议
9.1 纹理规范与目录约定
在项目中尽早制定纹理规范:
- 场景贴图统一放在
Assets/Textures/Scenes/下。 - UI 图片统一放在
Assets/Textures/UI/下。 - 角色、特效贴图单独建目录。
这样做的最大好处是:批处理脚本可以直接按目录设置默认格式,不需要维护一份庞大的“纹理清单”。
比如场景目录统一用 ASTC 8x8,角色贴图用 ASTC 6x6,UI 贴图用 ETC2 或 RGBA16,脚本逻辑会更清晰。
9.2 按纹理用途分流
同一种纹理格式不能覆盖所有使用场景,建议按用途分流:
| 纹理用途 | 建议格式 |
|---|---|
| 角色皮肤、脸部贴图 | ASTC 4x4 / BC7 |
| 场景地表、墙面 | ASTC 8x8 / BC1 |
| 法线贴图 | BC5 / ASTC 6x6 |
| UI 图标、文字相关 | RGBA16 / RGBA32 |
| 透明特效 | ASTC 4x4 / ETC2 |
这个表格不是硬性标准,但它反映了一个重要原则:质量敏感型贴图多花一点体积,普通背景贴图尽可能压缩。
9.3 自动化 CI 检查
有条件的情况下,可以把纹理压缩检查接入打包前的 CI 流程中。
可以在打包脚本里扫描所有纹理资源,检查是否存在以下问题:
- 是否仍有未压缩的 RGBA32 大纹理。
- 是否所有纹理都配置了 Android、iOS 平台覆盖。
- 是否存在尺寸超过 4096 的纹理。
- 是否在 UI 目录中误使用了高压缩比格式。
一旦发现异常,直接让构建失败并输出具体资源路径。这样比靠美术同学人肉检查可靠得多。
我建议在项目里保留一份类似的检查脚本,至少确保新提交的资源符合规范:
[MenuItem("Tools/Texture/Validate Texture Compression")] public static void ValidateTextureCompression() { string[] guids = AssetDatabase.FindAssets("t:Texture", new[] { TextureRoot }); int errorCount = 0; foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; if (importer == null) { continue; } TextureImporterPlatformSettings androidSettings = importer.GetPlatformTextureSettings(AndroidPlatform); if (!androidSettings.overridden) { Debug.LogError($"[TextureValidate] 未设置 Android 平台覆盖:{path}"); errorCount++; } } if (errorCount == 0) { Debug.Log("[TextureValidate] 所有纹理检查通过。"); } }9.4 性能预算判断
纹理压缩并没有“绝对正确”的方案,关键是建立性能预算。
在项目初期就明确以下数字:
- 单场景纹理显存预算,例如 150MB。
- 角色贴图预算,例如一个角色全部贴图不超过 8MB。
- UI 图集预算,例如整个 UI 不超过 50MB。
然后针对实际项目调整格式和尺寸。预算一旦确定,就可以在 Review 时快速否决不合规的资源,而不是反复争论“这张图能不能再压缩一点”。
10. 总结与下一步学习建议
纹理压缩看起来只是 Unity 导入面板里的一个下拉选项,但真正用好的时候,它涉及压缩原理、平台兼容、资源规范、打包流程多个层面。
如果你是在项目中期才开始关注纹理体积,建议先做三件事:
- 用 Build Report 统计当前包体里纹理占比,找出前 20 张大纹理。
- 给这些纹理设置合理的 Max Size 和压缩格式。
- 用真机 Profile 对比优化前后的内存和加载时间。
关于下一步学习,可以继续研究图集合并策略、AssetBundle 的纹理分离与变体、Addressables 的资源生命周期,以及不同平台的 Graphics API 能力。纹理优化只是一部分,和资产加载策略、渲染管线配合起来,才是一套完整的游戏性能优化体系。
如果本文对你有帮助,建议收藏备用。实际项目中遇到纹理压缩相关的奇怪问题,也欢迎在评论区把现象发出来,大家一起交流排查思路。