Unity网格内存优化

Unity网格内存优化

在渲染场景时,为了降低三角形渲染面片数,往往会使用LOD来实现不同距离下使用不同细节的Mesh来渲染物体,但是这样会造成多份Mesh在内存中同时存在,最终导致Mesh内存占用偏高的问题,针对这个问题,本篇文章给出了一个具体的解决方案。

功能简介

Unity网格渲染基础的优化由LODGroup提供,但是这个组件在做大世界海量物件渲染时存在3大缺陷。为了简化描述,以下用“内存”这个词来代表“内存(主存)+显存”。

  1. 只对单个Prefab做LOD,远处Mesh渲染顶点数减少,但对象数量没有减少,DrawCall或者说GPU状态切换并没减少。
  2. 在远处的长期只渲染LOD3的甚至Culled的Prefab,他的LOD0、LOD1和LOD2也一次性加载到内存。
  3. LOD的当前级别计算,每帧都会计算,实际上一般项目不需要如此精确地更新频率。根据距离不同,近处每帧计算是否切换LOD,而100米处1秒更新一次都可以,晚1秒从LOD3变到LOD2关系不大的。

针对1,我们做了HLOD来满足渲染性能,这个功能比较庞大这里不讨论。

这里就针对2实现LOD0的Mesh引用计数与动态加载卸载,因为LOD0 Mesh占用内存最多,可扩展到多个LOD加载卸载。同时用依赖距离的分帧计算优化下3。先看下最终效果对比。这里复制出8份不同的模型,模拟多种不同Mesh的情况,只是看起来一样,每种有8个实例,也就是Mesh内存是有8份的。


显示LOD1时,Assets只有2824,内存只有4.7MB


显示LOD0时,Assets有2896,内存有14MB

分包方式

Unity的AssetBundle有较多限制,比如:无法在不全局GC卡顿下卸载一个AssetBundle 内的Asset,强行这样操作,引用也会丢失。再次加载Asset后,比如一个Prefab就会丢失他的材质球引用,所以一般比较干净又不卡顿的卸载方式是直接卸载这个AssetBundle。这里对每个Asset单独一个AssetBundle来实现功能,具体项目会规划好一定颗粒度。物件Prefab是8个含有一个LODGroup的,但是他们LOD0的MeshFilter里要设置为空,这样打包的时候不会带有LOD0的数据,否则省不了内存。


8个 物件Prefab

写一个ScriptableObject来存放LOD0的Mesh,虽然用一个MeshFilter组件也能持有Mesh引用,但一些Prefab的LOD0有多个Renderer时候就比较麻烦,所以还是用ScriptableObject。然后创建8个MeshData实例,设置不同的8个LOD0的Mesh。

主要代码

因为场景物件难免同时存在多个实例,所以一般不会加载完一个就卸载AssetBundle ,而是长期缓存起来。这里加载LOD0 Mesh的AssetBundle也是这样,但要做个引用计数,当引用为0时再卸载。为了避免同时去加载,所以做个isLoading状态。一般最简单AssetBundle缓存就是这3个变量。


为了AssetBundle缓存设计一个类型

这里就是主要的加载/卸载逻辑,就是用rendererLods0[ 0 ].isVisible来获取是否需要渲染LOD0,如果需要并且LOD0 Mesh又不存在,那么去加载load0mesh。如果不需要显示LOD0,但load0mesh又存在,那么就卸载他,加载与卸载后都会更新existLod0的值。


LOD0 Mesh的主要加载与卸载逻辑

具体加载LOD0 Mesh过程

很常规的一种AssetBundle与Asset异步加载机制,同时解决并发冲突。就是有某个AssetBundle,如果别人已经加载完我就用它loadAsset,如果没人启动加载它我就加载它。另外特殊情况,如果别人已经加载中,我就等,等完再用。这里的特殊点是 lods[ 0 ].renderers = rendererLods0; ,为什么加载完要给LOD0指定为LOD0原来的Renderers。这是因为rendererLods0[ 0 ].isVisible的时机问题,因为这时候引擎这帧已经不渲染LOD1了,而LOD0我们又在加载中,所以Prefab会消失一下。为了避免消失,有2种做法:一种是自己做LOD计算并通过Forcelod来控制。就是LOD0 Mesh加载过程中也用LOD1先代替几帧渲染。这个完整LOD当前等级计算代码量又多起来,所以选了一种更简便的做法。就是平时让lods[ 0 ].renderers存放LOD1+LOD0(空),这样引擎切换到LOD0时 我们还没加载也能看到LOD1,不会闪一下。


加载LOD0 Mesh过程

具体卸载LOD0 Mesh过程

同样卸载时,会给lods[ 0 ].renderers = rendererLods0_1;,也就是放入LOD0和LOD1。另外引用次数为0时,会卸载AssetBundle实现内存的回收。另外有一个小技巧,是LOD0不存在时,要用LOD1的Mesh设置给LOD0的MeshFilter,并用不可见材质球。这是因为Unity的API没开放LOD Group的AABB设置。我们一旦让LOD0的Mesh为null,引擎自己计算的LOD等级结果就不同,认为AABB的size为0。


卸载LOD0 Mesh过程

分帧更新策略

分帧更新几乎是所有大世界游戏的通用策略,因为资源多又不想卡顿还不想提前等太久,所以都可以接受分帧了,比如一转头从模糊到清晰的RVT,SVT与TextureStreaming,以及UE新的VirtualShadowMap等。因为当我们把测试实例增加到800个,那么同时执行这份逻辑性能很差,需要1.65ms,而分帧后每帧只执行几个只需要0.02ms。


红框中为按距离分帧逻辑


每帧执行的性能


分帧策略下执行的性能


另外我写了自定义计算LOD当前等级配合forceLOD的做法,就不需要上面2处小技巧,整体更清晰合理。但严格的LOD计算,性能不如底层C++的计算,所以不建议那样做。

完整的逻辑类文件:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;

public class StreamLodMesh : MonoBehaviour {
    class SharedAssetBundle {
        internal    bool isLoading = false;
        internal AssetBundle ab =null;
        internal int refCount =0;
    }
    static Dictionary<string, SharedAssetBundle> sharedAssets=new Dictionary<string, SharedAssetBundle>();
    public string abName;
    LODGroup lODGroup;
    LOD[] lods;
    bool existLod0 = false;
    Renderer[] rendererLods0;
    Renderer[] rendererLods1;
    Renderer[] rendererLods0_1;
    SharedAssetBundle sab;

    void Start () {
        lODGroup = GetComponent<LODGroup>();

        lods = lODGroup.GetLODs();
        rendererLods0 = lods[0].renderers;


        rendererLods1 = lods[1].renderers;
        rendererLods0_1 = new Renderer[rendererLods0.Length + rendererLods1.Length];
        rendererLods0.CopyTo(rendererLods0_1, 0);
        rendererLods1.CopyTo(rendererLods0_1, rendererLods0.Length);
        lods[0].renderers = rendererLods0_1;
        for (int i = 0, len = rendererLods0.Length; i < len; i++)
        {
            rendererLods0[i].GetComponent<MeshFilter>().sharedMesh = rendererLods1[i].GetComponent<MeshFilter>().sharedMesh; ;
        }
        lODGroup.SetLODs(lods);
        StartCoroutine(loop());

    }


    IEnumerator loop()
    {
         float stepTime = 0.1f;


        while (true)
        {
            yield return new WaitForSeconds(stepTime);
            if (Camera.current == null)
            {
                yield return 0;
                continue;
            }
            float dis = Vector3.Distance(Camera.current.transform.position, transform.position);
             stepTime = Mathf.Clamp(dis* 0.01f, 0.05f,10);

            if (rendererLods0[0].isVisible)
            {
                if (!existLod0)
                yield return    StartCoroutine(loading());
            }
            else
            {
                if (existLod0)
                {
                    unload();
                }
            }
        }
    }


    private void unload()
    {

        existLod0 = false;
        for (int i = 0,len= rendererLods0.Length; i < len; i++)
        {

           rendererLods0[i].GetComponent<MeshFilter>().sharedMesh = rendererLods1[i].GetComponent<MeshFilter>().sharedMesh;

        }

        sab.refCount--;

        if (sab.refCount == 0) {
            sab.ab.Unload(true);
            sharedAssets.Remove(abName);
        }
        lods[0].renderers = rendererLods0_1;
        lODGroup.SetLODs(lods);

    }

    private IEnumerator loading()
    {

        if (sharedAssets.TryGetValue(abName, out sab)) {

            sab.refCount++;
            //如果已经正在加载 等加载完毕
            while (sab.isLoading)
            {
                yield return 0;
            }
        }
        else
        {
            //如果不存在 也不在加载中 创建一个开始加载
            sab = new SharedAssetBundle() { isLoading = true ,refCount=1};
            sharedAssets.Add(abName, sab);
            var rq_ab = AssetBundle.LoadFromFileAsync(@"E:\temp\" + abName);
            yield return rq_ab;
            sab.ab = rq_ab.assetBundle;
            sab.isLoading = false;
        }

         var rq_as= sab.ab.LoadAssetAsync<MeshData>(abName);
        yield return rq_as;
        var meshs= (rq_as.asset as MeshData).lod0Meshs;
        for (int i = 0,len= rendererLods0.Length; i < len; i++)
        {
            rendererLods0[i].GetComponent<MeshFilter>().sharedMesh = meshs[i];
        }

        lods[0].renderers = rendererLods0;
        lODGroup.SetLODs(lods);
        existLod0 = true;


    }



}

这是侑虎科技第1246篇文章,感谢作者偶尔不帅供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/jackie-93-85-85

再次感谢偶尔不帅的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)