Unity纹理优化:缩小包体

Posted Iamzls

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity纹理优化:缩小包体相关的知识,希望对你有一定的参考价值。

android打包apk大小约:475M
查看打包日志:Console→Open Editor Log;
或者依赖第三方插件:build reports tool(在unity store里可以下载);

定位问题

经过排查后,发现项目中纹理占比很高(82.8%),而且美术说图片从tga换成了更小的png,包体理应会变小,但是替换后并没有变化,甚至还大了3M,就开始查一下是什么原因;

找了一张较大的贴图测试,在资源管理器中大小为0.7M,但是在Unity中却变成了5.3M,经查证后得出结论,图片进入unity后会根据一定格式进行压缩,导致两方大小不一致:

  • 贴图导入unity后会自动设置成压缩格式,它会先判断贴图是否有透明通道。

  • Android:不带透明通道压缩成ETC1,带透明通道压缩成ETC2,不被4整除的回退到RGBA32

  • ios: 不带透明通道压缩成RGB PVRTC,带透明通道压缩成RGBA PVRTC ,不是2的整数次幂回退到RGBA32

  • 所以,最终编译入包的大小和图片本身格式、大小关系都不大,而是取决于限制分辨率下的信息复杂度。

经排查后发现,这个项目中纹理大多是RGB16、RGBA 16、RGB32、RGBA32、ETC、ETC 2格式压缩的,而且很多贴图的size用的都是2048、4096,这太浪费了,导致纹理占比这么大。

解决方案

一:压缩方式换成ASTC
找出影响较大的图片,将压缩格式改为ASTC。
ASTC是在OpenGL ES3.0出现后在2012年中产生的一种业界领先的纹理压缩格式,它的压缩分块从4x4到12x12最终可以压缩到每个像素占用1bit以下,压缩比例有多种可选。ASTC格式支持RGBA,且适用于2的幂次方长宽等比尺寸和无尺寸要求的NPOT(非2的幂次方)纹理。

经测试,最后使用的ASTC方式:
(1)法线贴图:4x4即可,避免丢失过多数据
(2)细节处的贴图:大部分6x6,并没发现明显失真;一小部分追求细节的图片用的4x4
(3)一般的贴图:选择6x6或8x8
(4)无关紧要,但是尺寸特别大的图:可以考虑8x8,10x10,12x12,不然打包出来太大
(5)Sprite(UI):6x6即可

适配机型
iOS
苹果从A8处理器开始支持 ASTC,iPhone6(包含)、iPad mini 4(包含)以上iOS设备支持,2014年的iPhone 5s及iPad mini 3以前的设备不支持。

安卓
安卓主流压缩格式正在从ETC2转向ASTC,Unity官方对ASTC格式支持的说明( 跳转官方文档 )中提到GPU对ASTC的支持情况:所有支持OpenGL ES 3.1和部分支持OpenGL ES 3.0的GPU(截止到2021年GooglePlay上统计的支持ASTC的设备已有77%,国内可能更高,个人角度看ASTC压缩格式是可以普及使用的。)

二:修改图片 Size
项目中贴图的size基本都是2048的,一张就好几兆

但实际上移动平台并不用这么大就可以,改成了1024,经测试后发现,大部分图片都可以,有几张模糊的再改成2048即可。

优化后打出的apk大小:352M,这只是针对较大的纹理做出的处理,如果统一优化一下效果会更好。

结论

  • 尽量让美术出的图使用2的幂大小,以便于提高性能节省内存。否则平台或GPU不支持NPOT纹理带,Unity会对纹理进行缩放和填充已达到下一个2的幂的大小。
  • 纹理压缩方式可根据情况,改用ASTC压缩。
  • 减小 Max Size,视觉上可接受的结果的最低设置,可以更快的降低纹理内存。
  • 对于法线贴图,ETC2 4bits的压缩效果比ASTC 5x5好;而有透明通道的贴图,ETC2 8 Bits比ASTC 4x4更优的情况,根据实际情况进行选择;
  • 制作纹理图集 :将多个纹理放置到单个纹理中,可以减少绘制调用和加快渲染速度。使用 Unity 精灵图集 或第三方 Texture Packer 可以制作纹理图集。
  • 关闭 Read/Write Enabled 选项 :如果启用,此选项在 CPU 和 GPU 可寻址内存中都会创建副本,纹理会占用双倍内存。大多数情况下,应保持此选项为禁用状态。如果要在运行时生成纹理,请通过 Texture2D.Apply 强制执行,并且传入设置为 true 的 makeNoLongerReadable。
  • 禁用不必要的 Mip Map :对于在屏幕上大小保持不变的纹理(如 2D 精灵和 UI 图形),Mip Map 不是必需的,对于与摄像机的距离会变化的 3D 模型,请保留 Mip Map为启用状态。

相关资料:
unity 纹理压缩格式
ASTC纹理压缩格式详解
游戏中的纹理压缩格式

unity AB包体大小优化

1模型Texture 贴图优化

项目AB里面,角色的数量比较多,从而角色模型的AB体积也比较庞大,角色模型一方面要控制好顶点 三角面数量,另一方面就是模型贴图资源优化,美术做资源的时候,为了追求质量,经常会把模型贴图搞的特别大,就拿我们现在这个项目来说吧,美术用的的模型贴图都是4096*4096的,到了unity里面,肯定不能这样搞要,4096你让低端机还玩个鸟呀,所以限定了贴图 Max Size为2048。
原始的资源格式如下,一个模型里面包含3张这样格式的贴图,分别是uv贴图,法线贴图,和一张灰度贴图,打出来的AB资源是5.7M。


考虑到我们的项目是RPG 回合制游戏,角色只有在战斗场景中会存在多个,而且距离摄像机距离没有特别明显的变动,所以就建议去掉MipMap。然后打包AB,资源大小为4.1M。
接下来要处理的就是贴图压缩了,我这里主要测试了Android的,由于贴图都不带透明通道,所以UV贴图和灰度贴图采用了RGBCrunchedETC格式,法线贴图采用了RGBCompressETC4bit 方式,AB资源大小为2M
如果法线贴图也按照RGBCrunchedETC方式进行压缩的话,AB资源大小可以降到1.4M,这个根据根据显示效果而定。
还有就是 Max Size的设置,最好是根据模型显示效果进行分类控制

2TimeLine 资源优化

项目战斗技能,我们采用的是TimeLine制作的,今天查看了一下TimeLine资源,一个TimeLine 的prefab资源,竟然达到了1M多,有个别的都2M了,我的天呢,你们这群美术小伙伴都对TimeLine做了啥,于是我就用AssetBundleBorswer工具分析了一下00003号角色大招的 TimeLine AB资源,不看不知道,一看吓一跳,这些都是什么鬼呀。为什么会有一堆其他角色的animation和timeline资源。

然后我看了一眼 unity 编辑器中的原始资源


资源好像也没啥毛病,那这一堆乱七八糟的资源是从哪里来的,既然AB认为他们有引用关系,那肯定还是有问题的,只能从meta文件中在进行求证了。这次倒是没有让我失望,在m_SceneBindings属性下面看到了这一堆被引用的资源,无语了。

然后回到unity,开启debug 模式,重新审视资源,怪我太年轻,不懂人心险恶,差点儿就被你蒙混过关了,这106个资源引用,打出来的AB能小吗,哎!

我实际需要的资源,只有关闭debug模式的时候,显示的这三个资源呀

我该肿么办,既然是码农,那就发挥出搬砖精神吧,度娘度娘,我要搬砖了,然后度娘给了我一个链接
https://zhuanlan.zhihu.com/p/396526134
果然是同道中人,感谢楼主帮我们趟坑,然后复制粘贴,一个修改工具就做好了

    [MenuItem("Assets/CleanUpPlayableBind")]
    private static void CleanUpPlayableBind()
    
        GameObject gob = Selection.activeGameObject as GameObject;
        if (gob != null) 
            var playable = gob.GetComponent<PlayableDirector>();
            CleanUpBind(playable);
        
    

    public static void CleanUpBind(PlayableDirector playable)
    
        if (playable == null) return;
        Dictionary<UnityEngine.Object, UnityEngine.Object> bindings = new Dictionary<UnityEngine.Object, UnityEngine.Object>();
        foreach (var pb in playable.playableAsset.outputs)
        
            var key = pb.sourceObject;
            var value = playable.GetGenericBinding(key);
            if (!bindings.ContainsKey(key) && key != null)
            
                bindings.Add(key, value);
            
        

        var dirSO = new UnityEditor.SerializedObject(playable);
        var sceneBindings = dirSO.FindProperty("m_SceneBindings");
        for (var i = sceneBindings.arraySize - 1; i >= 0; i--)
        
            var binding = sceneBindings.GetArrayElementAtIndex(i);
            var key = binding.FindPropertyRelative("key");
            if (key.objectReferenceValue == null || !bindings.ContainsKey(key.objectReferenceValue))
                sceneBindings.DeleteArrayElementAtIndex(i);
        
        dirSO.ApplyModifiedProperties();
    

搞定!顺便提一嘴,TimeLine用到的animaton资源,记得压缩它们的浮点数精度值和scale通道,通常情况下我们用不到这么高的精度,之前打AB比较大,除了多余的资源,animation也没有压缩。最终这个AB资源从1.6M,被我降低到了132k,Nice。

Animation优化

核心代码

    [MenuItem("Assets/AnimationClipChange/Directory/降低精度并删除Scale通道")]
    private static void ChangeAnimFloatAndScaleDirectory()
    
        Object gob = Selection.activeObject;
        string path = AssetDatabase.GetAssetPath(gob);
        if (Directory.Exists(path))
        
            string[] udids = AssetDatabase.FindAssets("t:AnimationClip", new string[]  path );
            int index = 0;
            foreach (var item in udids)
            
                string itemPath = AssetDatabase.GUIDToAssetPath(item);
                EditorUtility.DisplayProgressBar("执行中..." + path, itemPath, (float)index / udids.Length);

                AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(itemPath);
                optmizeAnimationFloatAndScale(clip);
                index++;
            
            EditorUtility.ClearProgressBar();
            Debug.LogColor("AnimationClip change float finish!");
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        
        else
        
            Debug.LogError("select target is not a directory:" + path);
        

    


    static void optmizeAnimationFloatAndScale(AnimationClip targetAnimClip)
    
        optmizeAnimationScaleCurve(targetAnimClip);
        optmizeAnimationFloat(targetAnimClip);
        Resources.UnloadUnusedAssets();
        
    


    /// <summary>
    /// 优化浮点数精度
    /// </summary>
    static AnimationClip optmizeAnimationFloat(AnimationClip clip)
    
        //浮点数精度压缩到f3
        AnimationClipCurveData[] curves = null;
        curves = AnimationUtility.GetAllCurves(clip);
        Keyframe key;
        Keyframe[] keyFrames;
        for (int ii = 0; ii < curves.Length; ++ii)
        
            AnimationClipCurveData curveDate = curves[ii];
            if (curveDate.curve == null || curveDate.curve.keys == null)
            
                //Debug.LogWarning(string.Format("AnimationClipCurveData 0 don't have curve; Animation name 1 ", curveDate, animationPath));
                continue;
            
            keyFrames = curveDate.curve.keys;
            for (int i = 0; i < keyFrames.Length; i++)
            
                key = keyFrames[i];
                key.value = float.Parse(key.value.ToString("f3"));
                key.inTangent = float.Parse(key.inTangent.ToString("f3"));
                key.outTangent = float.Parse(key.outTangent.ToString("f3"));
                keyFrames[i] = key;
            
            curveDate.curve.keys = keyFrames;
            clip.SetCurve(curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve);
        

        return clip;
    

    /// <summary>
    /// 优化scale曲线
    /// </summary>
    static AnimationClip optmizeAnimationScaleCurve(AnimationClip clip)
    
        //去除scale曲线
        foreach (EditorCurveBinding theCurveBinding in AnimationUtility.GetCurveBindings(clip))
        
            string name = theCurveBinding.propertyName.ToLower();
            if (name.Contains("scale"))
            
                AnimationUtility.SetEditorCurve(clip, theCurveBinding, null);
            
        

        return clip;
    

以上是关于Unity纹理优化:缩小包体的主要内容,如果未能解决你的问题,请参考以下文章

unity 转微信小游戏 资源优化

干货:Unity游戏开发图片纹理压缩方案

unity AB包体大小优化

unity AB包体大小优化

unity AB包体大小优化

unity AB包体大小优化