引言
上一篇文章描述了如何在不修改自定义渲染组件的前提下使用alpha分离的纹理来提升iOS的透明压缩纹理质量(见这里:https://indienova.com/indie-game-development/unity-alpha-separate/)。
在这个方案投入项目开始使用一段时间之后,UI同学又来找我抱怨了:虽然一些贴图的不透明部分不会显得脏了,但是为什么有些纹理的半透明部分表现这么奇怪呀?这就涉及到上次方案中透明通道贴图格式的选择问题了。在上一篇文章中,我们的透明通道贴图在iOS上使用了pvrtc4的压缩方式,这样的做法造成了部分贴图半透明部分显示上的效果降低,在一些特定的情况下效果甚至没法接受。那么如何解决这个问题呢?这一次我们要来说明另一种贴图质量低下的解决方案:使用ASTC格式纹理,并且解释针对目前部分硬件的限制所作出的调整。
ASTC
关于纹理格式的一些基础知识可以在上一篇文章中开头的基础中看到,在此不做赘述。这里简单说明一下ASTC这种格式。
1. 概述
ASTC(Adaptive Scalable Texture Compression)是一种基于块的有损纹理压缩格式,完整细节在2012年被提出。这种压缩格式有多种压缩率可选,同时有着较高的压缩率和较好的压缩质量。
2. 压缩率
在上面的表中展示了从ASTC格式从4x4到12x12每一个像素所占的比特数(bpp),可以看到最大(也就是4x4)的占用为每个像素一个字节,那么一张1024x1024的贴图,使用ASTC 4x4压缩后,他的大小就是1MB,这个大小和ETC2 RGBA 8bpp格式的大小相同。
3. 纹理质量
从结果来看,在相同的内存占用前提下,ASTC相较其他传统压缩格式的质量更优;而在相同的质量前提下,占用内存更小。关于ASTC的压缩类型选择和质量相关说明, 可以参考这里:https://developer.nvidia.com/astc-texture-compression-for-game-assets
4. 支持机型
iOS上,苹果从A8处理器开始支持ASTC格式(即从iphone6,iPad mini 4开始支持),而以前的iPhone 5s和iPad mini 3及之前的设备都不支持。
Android上,未来的压缩格式正在从ETC2转向ASTC。但是因为android机多样性,目前的市场普及率我暂不可知。
基本使用
现在在Unity中要使用ASTC格式非常简单:选择纹理导入设置,选择ASTC格式,完成!
这就结束了吗?显然事情并没有这么简单,上面提到过的硬件的限制使得我们无法这么简单的选择,因为在不支持的硬件上使用ASTC格式,会被软解为RGBA,从而数倍的增加其内存占用,让游戏瞬间被系统杀掉。
针对这些硬件限制,我们基于项目本身的特点一般有以下的考量:
- 在iOS上如果项目是个大型游戏,可以放弃低端机器的用户,那么就直接使用ASTC;
- 在android上,如果可以得到准确的市场占有率,并确定可以放弃这一部分市场的前提下,可以直接使用ASTC;
- 现在立项的移动端游戏,且未来会有开发时长2年以上的,可以考虑只使用ASTC;
- 否则,如果你想考虑低端用户,那么还是需要考虑ASTC和传统格式的兼容性问题,我们接下来就会讲到这套实现方案:根据硬件条件选择使用不同的纹理格式。
根据硬件条件选择使用不同纹理格式
1. 基本思路
首先,对于直接引用打入包内的方式是不适合本方案的。本方案基于通过AssetBundle加载资源的项目(包括动态加载和依赖加载)。
基本思路为:在打包时生成一份完整的AssetBundle包以后,再针对需要使用ASTC的纹理所在的AssetBundle包重新打一份ASTC版本的;在运行时根据硬件支持情况等信息来选择加载哪一份AssetBundle包(PVRTC/ASTC)。
这样在游戏运行过程中,如某个prefab引用了一个sprite,可以通过硬件情况选择不同的AssetBundle包,加载不同包中相同的Sprite,然后引用到不同压缩格式的纹理上,就达到了我们的目的。
2. 构建测试工程
基于上面的思路,我们来构建一个测试工程,测试工程需要包含以下内容:
- 打AssetBundle包流程
- 生成BundleMap,记录ASTC信息,供运行时使用
- 生成AtlasBundleMap,记录需要重新打ASTC版本AssetBundle包的信息,供打包时使用
- 运行时资源加载,根据硬件条件来选择不同的AssetBundle包
3. 实现细节
首先是打AssetBundle包流程,这块不会详细说,因为每个项目的实现都可能不同。在这里,我们的思路是:添加需要动态加载的AB包规则,最后计算出它们依赖的资源,按规则打成其他依赖AB包。下面的代码示意了这一过程。
var builder = new AssetBundleBuilder(); builder.AddSceneBundle("Assets/Scenes/Test.unity", "scenes_"); builder.AddSceneBundle("Assets/Scenes/Loading.unity", "scenes_"); builder.AddDirBundle("Assets/Textures/Dynamic", "", "textures_"); builder.UpdateSharedAssets(); builder.BuildAssetBundles(abPath, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
其中重点在UpdateSharedAssets()方法中,其中不仅计算了所有的依赖关系,同时记录了AtlasBundleMap和BundleMap。
下图为BundleMap的格式,最重要的几个字段是BundleName(记录AB包名),AstcVariant(是否有其ASTC版本),AstcVariantBundleName(ASTC版本的AB包名)。这个文件会打入最终的AB包内,在运行时加载资源时根据AstcVariant和AstcVariantBundleName来判断并加载对应的ASTC版本的包。
下图为AtlasBundleMap的格式。这个文件记录了需要再打一份ASTC版本AB包的包名列表,在后续的打包过程中会用到。
经过上面的过程,完整的AB包已经都打出来了,下面需要重新打一批ASTC版本的AB包。首先重新打一次图集:
EditorSettings.spritePackerMode = SpritePackerMode.BuildTimeOnly; Packer.SelectedPolicy = typeof(CustomSpritePackerPolicy).Name; CustomSpritePackerPolicy.MakeAstc = true; Packer.RebuildAtlasCacheIfNeeded(target, true, Packer.Execution.ForceRegroup);
上面需要注意的是:两次打图集需要有一个不同的标志:MakeAstc,第一次为false,第二次为true,在自定义的图集打包规则中才可以判断出两次需要打的是不同的图集。自定义图集规则中的实现为:
if (MakeAstc) { if (IsTransparentCompressed(settings.format)) { settings.format = Util.ASTC_RGBA_FORMAT; } else if (IsOpaqueCompressed(settings.format)) { settings.format = Util.ASTC_RGB_FORMAT; } }
更多的关于自定义图集规则可以参考官方文档:https://docs.unity3d.com/Manual/SpritePacker.html
之后使用同样的BuildAssetBundleOptions,根据AtlasBundleMap中信息对相应资源再打一次AB包即可,打完后改名拷贝到之前的文件夹中,现在的AB包目录就像是这样:
其中astc_开头的都是对应的AB包的ASTC版本。
接下来我们来到运行时。首先是判断硬件是否支持ASTC格式:
public static bool DeviceSupportAstc() { var support = true; support = support && SystemInfo.SupportsTextureFormat(ASTC_RGB_FORMAT); support = support && SystemInfo.SupportsTextureFormat(ASTC_RGBA_FORMAT); return support; }
其次是加载资源:
private AssetBundle LoadBundleCore(string bundleName) { if (Util.UseAstc && m_bundleMap != null) { BundleMapData bundleMapData = null; if (m_bundleMap.TryGetValue(bundleName, out bundleMapData)) { if (bundleMapData.AstcVariant) { bundleName = bundleMapData.AstcVariantBundleName; } } else { Debug.LogError("Cannot find bundle name in bundle map: " + bundleName); } } var bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + bundleName); return bundle; }
这是所有加载AB包时都会调用到的方法(包括动态加载和依赖加载)。其中的过程也很简单:判断了硬件情况,根据BundleMap找到需要加载的ASTC版本的AB包名,并去加载。
到这里以后我们的测试工程就已经完成了(详细的代码可以看文末的代码仓库一节)。我们可以看到在这个demo中,我们可以显示当前动态加载和依赖加载的纹理格式,并切换到另一种纹理,实现了ASTC和传统格式之间的选择性加载。
我们再使用Profiler工具来对比一下两种不同格式的纹理的内存占用:
上图为ETC2格式(为了测试方便,没有使用PVRTC,结果是一样的)的内存占用,64.2KB,即等于256*256*8/8。
上图为ASTC 6x6格式的内存占用,29.1KB,即等于256*256*3.56/8。
ASTC 6x6的内存占用比PVRTC 4bpp还小了10%,但是质量却要好的很多很多。
补充说明
1. 延伸一下,因为上面的方案多生成了一批图集相关的AB包,所以会让包体有所增大。如果你们项目中有资源更新,那么支持ASTC和不支持ASTC的设备就需要根据自己的情况来下载不同的资源包。如果要做的好的话这个问题也是需要考虑的。当然这里就不讨论了,有需要的同学可以自己研究。
2. 引言中提到的透明通道贴图PVRTC RGBA 4bpp格式造成了贴图的半透明区域显示质量差的问题。我之后也尝试使用Alpha8格式来代替,但是实际测试发现所有图都看不到了,通过看Unity的默认spriterenderer和DefaultETC1的shader才知道,他们都使用了alpha通道贴图的R通道,而Alpha8格式取不到,所以贴图都不显示了。要解决这个问题可能需要使用R8格式来代替Alpha8格式,但是因为我使用的Unity2017不支持,所以就没有继续尝试了。
结论
包括前一篇文章,在iOS上我们一共使用了3种贴图压缩格式的方案(android同理):
- 直接使用PVRTC
- 分离透明通道的PVRTC+PVRTC,或者PVRTC+Alpha8
- 根据硬件情况的不同来选择使用PVRTC还是ASTC
这几种方案各有优劣和使用场景,我下面就试着总结一下。
1. 方案优缺点对比
PVRTC:
- 优点:开发方便,不需要做额外处理,所有iOS设备统一支持
- 缺点:纹理质量很差
分离透明通道:
- 优点:不受硬件限制
- 缺点:开发繁琐(一次性),如果使用pvrtc+pvrtc方案,半透明区域质量不好;如果使用pvrtc+alpha8方案,内存占用会增加到12bpp,且可能不支持所有unity版本
根据硬件选择使用PVRTC还是ASTC:
- 优点:内存占用少,纹理质量高
- 缺点:开发繁琐(一次性),包体图集纹理占用存储空间多一倍
2. 方案适用场景对比
PVRTC:
- 适用于:不那么关注贴图质量,希望内存占用低,希望开发成本低
- 不适用于:有品质要求的项目
分离透明通道:
- 适用于:希望透明贴图的非透明部分不脏,不希望包体增大,2018以上版本unity项目,对于内存占用不那么敏感
- 不适用于:2018以下版本unity项目,希望内存占用低
根据硬件选择使用PVRTC还是ASTC:
- 适用于:对包体体积不那么敏感,有开发能力和时间,希望纹理质量高同时内存占用小
- 不适用于:对包体体积敏感,开发能力和时间有限
参考资料
ASTC纹理压缩格式详解:https://zhuanlan.zhihu.com/p/158740249
Using ASTC Texture Compression for Game Assets:https://developer.nvidia.com/astc-texture-compression-for-game-assets
代码仓库
https://github.com/RayRiver/UnityAstcSupportDemo
感谢分享哈哈,国内这种方案挺好的,主做海外特别是东南亚的化就gg了,东南亚小国家 对包体和下载流量有要求,人力充足的情况下 可以 搞一套完善的按需打包与下载的机制,除了图片格式,还可以增加 高清资源,还有按模块分包下载啥的