首页 > 系统相关 >Unity网格内存优化

Unity网格内存优化

时间:2022-11-17 11:23:16浏览次数:69  
标签:rendererLods0 网格 Unity Mesh 内存 卸载 lods LOD0 加载

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

 

 

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_MeshStreaming/3.mp4 

 

分包方式

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)

标签:rendererLods0,网格,Unity,Mesh,内存,卸载,lods,LOD0,加载
From: https://www.cnblogs.com/uwatech/p/16898815.html

相关文章

  • Java IO流--使用FileReader字符输入流读入数据到java程序或者内存的基本操作
    ​前言:1、流的分类:1.操作数据单位:字节流、字符流2.数据的流向:输入流、输出流3.流的角色:节点流、处理流2、流的体系结构:二、流的体系结构抽象基类节点流(或文件流)......
  • Linux内存泄露案例分析和内存管理分享
    作者:李遵举一、问题近期我们运维同事接到线上LB(负载均衡)服务内存报警,运维同事反馈说LB集群有部分机器的内存使用率超过80%,有的甚至超过90%,而且内存使用率还再不停的增长......
  • 手动释放Linux内存的方法
    Linux释放内存的命令:syncecho1>/proc/sys/vm/drop_caches drop_caches的值可以是0-3之间的数字,代表不同的含义:0:不释放(系统默认值)1:释放页缓存2:释放dentries和ino......
  • CTF取证总结(内存取证,磁盘取证)以及例题复现
    内存取证经常利用volatility分析取证文件后缀.raw、.vmem、.img常用命令(imageinfo,pslist,dumpfiles,memdump)可疑的进程(notepad,cmd)和磁盘取证结合起来考察了解部分操作系统......
  • Java 内存模型(JMM)
    1.为什么要有内存模型?要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。1.1.硬件内存架构(1)CPU去过机房的同学都知道,一般在大型服务器上......
  • Linux下进程间通信方式之管道、信号、共享内存、消息队列、信号量、套接字
    /*1,进程间通信(IPC)Inter-ProcessCommunication比较好理解概念的就是进程间通信就是在不同进程之间传播或交换信息。2,linux下IPC机制的分类:管道、信号、共享内存、......
  • 内存映射MMF
    usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.IO.MemoryMappedFiles;usingSystem.Linq;usingSystem.Text;namespa......
  • JVM堆内存分析,分析工具jmap heap
    一、查看堆信息jmap-heap33146Debuggerattachedsuccessfully.Servercompilerdetected.JVMversionis25.251-b08usingthread-localobjectallocation.Garb......
  • new 在堆内存中开辟内存空间,delete 释放开辟的内存空间
    intmain(){int*p=newint(10);cout<<*p<<endl;deletep;cout<<*p<<endl;int*arr=newint[10];......
  • C++ 内存分区模型
    代码区:存放函数的二进制代码,由操作系统管理全局区:存放全局变量、静态变量以及常量。栈区:由编译器自动分配释放,存放函数的参数值,局部变量等堆区:由程序员分配和释放,若......