Unity资源格式详解与自动化设置

发表于2018-11-06
评论0 5k浏览
内存的占用量对于手游项目开发来讲一直都是个重要的话题,很多项目往往需要在游戏上线之前做必要的优化,以达到运营平台的测试标准参数,比如我们腾讯的TDR技术测试标准。根据自己最近的几年的项目经验,游戏内存占用超标除了程序代码本身的缺陷之外(如冗余的资源占用没有释放等),资源文件格式设置是否合理也是一个很大的因素。比如纹理贴图的格式,模型资源格式等等。在以前的端游开发时,我们往往由于缺乏完善的引擎工具,项目中所用到的资源文件格式大多都是美术人员或相关的程序员自己导出到游戏资源目录中,基本上都是手动处理,或者自己写一个批量扫描图片纹理尺寸大小的工具程序,以免美术同学误处理,因为美术同学一般都是对游戏程序的性能不敏感的。

而在如今的手游开发中,Unity为我们提供了自定义资源导入设置过程的功能(AssetPostProcessor),可以实现导入时自动设置资源文件的格式等。我们完全可以通过程序自动处理的方式来处理纹理图片、模型文件、音效资源等文件的导入格式设置等。采用程序自动处理比我们手动处理更加可靠,因为人是有情感的、有情绪的、有大意的时候。如果因为不小心直接把一张2048的truecolor图片放入工程中使用,那就会直接占用16M的内存空间。因此作为Unity项目,我们在研发之初就应该规划好资源文件的分类,以及分类的格式设置属性。通过继承AssetPostProcessor类,编写Texture、FBX、动画、音效文件的自动导入设置处理函数,让程序替代人工,确保万无一失。

AssetPostProcessor类说明:
  //示例代码:
  public class MyAssetPostprocessor : AssetPostprocessor
   { 
      public void OnPreprocessModel() 
      { 
          ModelImporter modelImporter = (ModelImporter)assetImporter; 	
            if(modelImporter.isReadable) 
          { 
              modelImporter.isReadable = false; 	
                modelImporter.SaveAndReimport(); 
          }
       } 
  }
上图列举了AssetPostProcessor派生类可以自己定义处理的资源类型,如需要自定义纹理贴图的格式就需要增加一个void OnPostprocessTexture(Texture2D texture)函数。注意这里的处理函数是采用反射的机制,不是重载虚函数,和MonoBehaviour的void Start()、void Update()类似。下面我们详细几种比较典型的资源文件格式设置规范,部分格式说明参考Unity官方手册资料。

纹理贴图(Texture)

纹理贴图可以说是游戏中最占内存的一类资源,不管是功能UI界面,还是场景资源都会用到。一张1024*1024尺寸,32位truecolor格式的图片,尽管它的PNG文件原始大小可能只有几百K甚至更小,但在运行时会占用4M的内存空间(因为在程序通过Opengl图形接口创建Texture纹理时,会先把压缩文件解压,读出原始的像素信息数据用于创建GPU需要的Texture对象)。所以设置合理的纹理贴图资源格式对于内存优化是相当重要的,下面是Texture的部分设置属性说明:

Read/Write
启用 Read/Write会导致纹理占用两份内存,一份在GPU端,一份在系统内存中。因为启用 Read/Write说明CPU端逻辑程序可能在运行时读取或修改纹理像素信息(GetPixel32,SetPixel32),像素信息必须在系统内存中保留一份。而在项目中,绝大多数的图片都是只供GPU渲染引用的,所以如果没有特殊需要就禁用Read/Write。

Mipmaps
mipmap主要是用在3D场景中的模型贴图,渲染底层会根据物体在屏幕中呈现的实际渲染大小选择适当的一级mipmap。如果相机的远近距离对某些物体渲染到屏幕的大小没有任何影响(如正交相机),那么这些物体的贴图就应该禁用mipmaps,最典型的就是UI界面的贴图。这几乎可以降低接近1半的贴图内存占用。

AutoCompressed
压缩图片格式对于移动设备是非常有用的,因为相对于truecolor格式,这将大大减少内存的占用。注意,这里的压缩不是单指贴图文件的压缩,而是纹理贴图像素数据在内存中就是压缩格式的,渲染采样时,由GPU负责实时解压的。所以压缩格式与GPU的厂商是具有相关性的,Android和Apple的设备支持的压缩格式就不一样。这种压缩是有损的,以牺牲贴图的像素精度为前提的,所以需要我们研发人员根据实际美术效果需要自己把握。作为程序,建议纹理贴图都采用自动压缩格式,这样Unity能根据目标平台生成其对应的压缩格式纹理。根据之前的项目经验,3D模型的贴图、特效贴图、和部分UI贴图是非常适合采用压缩格式的。而部分对画质要求很高的图片可以使用truecolor,这个需要根据项目实际情况灵活运用。下图是各平台纹理压缩格式情况介绍。

1024 * 2014(ARGB)1024 * 2014(RGB)
ETC1(Android)不支持0.5M
ETC2(Android)1M0.5M
PVRTC(IOS)0.5M0.5M
DX1(Windows)不支持0.5M
DX5(Windows)1M不支持
TrueColor4M3M

Texture Size Limits
纹理贴图的尺寸规格对内存占用的影响是相当重要的,在项目初期应该给美术同学制定一份贴图规格说明文档,明确UI、特效、角色贴图、场景贴图等资源贴图的推荐尺寸和最大尺寸限制。

模型(Model)

read/write
这个和前面贴图的read/write类似,Unity默认是开启的。如果在运行时不需要通过程序动态的修改模型的顶点数据,那么就没必要打开这个选项,一般情况下都应该禁用。另外,如果模型的GameObject上需要MeshCollider组件,那么必须要开启该选项。

Rigs on non-character models
在导入一个FBX文件时,Unity默认情况下会为非角色动画的模型导出一个generic rig。这就导致FBX的prefab会多一个Animator组件,而这对于那些不需要动画的模型就是多余的,因为所有活动状态的Animator组件都会每帧tick一次。

Optimize Game Objects option
“Optimize Game Objects”选项对于带动画模型的运行性能是有很大影响的。如果该选项被禁用,那么模型在实例化时Unity会根据模型中的骨骼组织结构建立一个庞大的transform树状结构,每个骨骼bone对应一个transform。这样庞大的transform架构在update时是相当耗时的。而且还限制了Unity多线程处理顶点蒙皮和骨骼动画的运算能力。如果是某些骨骼挂点在程序中要用于挂接其它模型(比如角色手上的武器挂点等),那可以把这些骨骼挂点的名字在extra transform列表中单独注明,以免在优化时被误处理。

Mesh compression
启用Mesh compression将会减少用来表示float类型数据的位数,这是以降低浮点数的少量精度为前提的,所以应该让相关的美术同学审核一下最终的渲染效果,看看是否在可容忍的范围内。这个压缩是分等级的,比如我们可以选择只压缩法向量和切向量,而不压缩UV坐标与顶点位置。

Mesh Renderer Settings
当添加一个mesh renderer到prefab或GameObject时,默认情况下,Unity会启用“Shadow Cast”,“Receive Shadow”,如果我们并不需要这些,那么应该关掉这些选项,否则导致来冗余的渲染代价,增加渲染的消耗。因为渲染实时阴影需要增加额外的渲染pass。

最后是一份资源文件在导入时自动处理格式设置的程序示例:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
//资源文件导入自动设置程序
class MyAssetPostProcessor : AssetPostprocessor
{
    //贴图导出时处理
    void OnPreprocessTexture()
    {
        TextureImporter textureImporter = (TextureImporter)assetImporter;
        if (assetPath.StartsWith("Assets/Art/UI"))
        {
            //如果是UI贴图,禁用mipmap
            textureImporter.mipmapEnabled = false;
            textureImporter.textureType = TextureImporterType.Sprite;
            if (assetPath.StartsWith("Assets/Art/UI/Common"))
            {
                //通用UI控件(Button, Input)等复用率高,尺寸小的UI图片,直接用truecolor
                //这样能以最小的内存代价提升美术效果
                textureImporter.textureFormat = TextureImporterFormat.AutomaticTruecolor;
            }
            else
            {
                textureImporter.textureFormat = TextureImporterFormat.AutomaticCompressed;
            }
            //纹理贴图的最大尺寸限制为<=1024
            textureImporter.maxTextureSize = 1024;
            textureImporter.isReadable = false;
        }
        else if(assetPath.StartsWith("Assets/Art/3D/Texture/"))
        {
            //如果是3D模型的贴图,启用mipmap
            textureImporter.mipmapEnabled = true;
            textureImporter.textureFormat = TextureImporterFormat.AutomaticCompressed;
            //如果是法线贴图
            if (assetPath.Contains("_N"))
                textureImporter.textureType = TextureImporterType.Bump;
            //纹理贴图的最大尺寸限制为<=1024
            textureImporter.maxTextureSize = 1024;
            textureImporter.isReadable = false;
        }
        //对于不是2n次方的图片,自动适配到2n次方的大小
        textureImporter.npotScale = TextureImporterNPOTScale.ToNearest;
    }
    //模型导出时处理
    void OnPreprocessModel()
    {
        ModelImporter modelImporter = this.assetImporter as ModelImporter;
        modelImporter.isReadable = false;
        modelImporter.meshCompression = ModelImporterMeshCompression.Medium;
        if (assetPath.Contains("/AnimModel/"))
        {
            //如果是带动画的模型
            modelImporter.importAnimation = true;
            modelImporter.importBlendShapes = true;
            modelImporter.animationCompression = ModelImporterAnimationCompression.Optimal;
        }
        else if (assetPath.Contains("/CommonModel/"))
        {
            //如果是不带动画的模型
            modelImporter.importAnimation = false;
            modelImporter.importBlendShapes = false;
        }
    }
    //音效导入时处理
    public void OnPreprocessAudio()
    {
        AudioImporter audioImporter = this.assetImporter as AudioImporter;
        AudioImporterSampleSettings settings = audioImporter.defaultSampleSettings;
        if (assetPath.Contains("/Sound/"))
        {
            //如果UI事件音效(文件小,播放时间短)
            settings.loadType = AudioClipLoadType.DecompressOnLoad;
        }
        else if (assetPath.Contains("/Music/"))
        {
            //如果是背景音效(文件大,播放时间长)
            settings.loadType = AudioClipLoadType.Streaming;
        }
        audioImporter.defaultSampleSettings = settings;
    }
}
来自:https://blog.csdn.net/qq_14939027/article/details/79182105

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引