7

Unity网格内存优化 - UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技

 1 year ago
source link: https://blog.uwa4d.com/archives/USparkle_MeshStreaming.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在渲染场景时,为了降低三角形渲染面片数,往往会使用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份的。

1.png

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

2.png

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

分包方式

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

4.png
8个 物件Prefab

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

5.png

6.png

主要代码

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

7.png

为了AssetBundle缓存设计一个类型

这里就是主要的加载/卸载逻辑,就是用rendererLods0[

具体加载LOD0 Mesh过程

很常规的一种AssetBundle与Asset异步加载机制,同时解决并发冲突。就是有某个AssetBundle,如果别人已经加载完我就用它loadAsset,如果没人启动加载它我就加载它。另外特殊情况,如果别人已经加载中,我就等,等完再用。这里的特殊点是 lods[

具体卸载LOD0 Mesh过程

同样卸载时,会给lods[

分帧更新策略

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

11.png

红框中为按距离分帧逻辑

12.png
每帧执行的性能

13.png
分帧策略下执行的性能

另外我写了自定义计算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)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK