你爹临死前 发表于 2020-11-24 09:41

Unity大世界超多物体渲染---BatchRendererGroup

现在手游是越做越大,大世界似乎已经成为MMO游戏的基本配置了。然而,大世界需要解决的东西非常多,网上的资料也比较少,像Houdini这样制作大地形工作流这类的资料都非常少。前两年在知乎上写的那两篇大地形渲染的文章最近居然浏览量上涨,但是其实那些技术以及过时了,Unity近两年更新了很多很好用的功能。关于大地形这块的东西太多了,我只能挑些细枝末节的,网上资料比较少的东西写写。正好最近被关在家里,有时间研究一些东西。
好了,言归正传,今天研究的东西是这个BatchRendererGroup,这个在网上搜的话,基本就只有一篇Momo大神些的ECS渲染Sprite的资料,外网也多是跟ECS相关的东西,没人详细介绍这个是干什么的。由于使用ECS需要走ECS的BuildPipeLine,这点限制太大了,一个是牵扯到热更新问题,另一个是每个公司可能有自己的BuildPipeLine,这几乎是个致命性问题,其实BatchRendererGroup可以用在正常的Model,只要你的项目是URP的,就能正确使用。
那么使用这个的应用场景又是什么呢?在超大世界的概念火了之后,场景中的物体可能多得无法想象,有几十万个,加上草,植被这些可能甚至有上百万个,光是gameObject的资源占用就多得无法想象,在Unity支持GPUInstance和SRP Batch后,渲染批次倒是可以减少,不成问题,但是Cull一次,性能慢得你无法想象。那么,今天这个BatchRendererGroup就完美解决了这一问题,并且还带来了多线程做Cull和LOD的好处。
BatchRendererGroup使用非常简单,构造,AddBatch,GetBatchMatrices,就这三个接口就够了。
我们先来看构造函数。
这个构造函数,是传入一个Cull函数,这个Cull函数我们后面讲,大部分功能都在里边。
然后看看AddBatch,AddBatch的参数很多,大家可以参考Unity的文档。
大部分参数都是一目了然,我主要说几个需要注意的参数,一个是Bounds,这个的意思是用来控制这个Batch的显示和隐藏,他会跟相机做Cull运算,一般我们取你所有BoundingBox的合集。还有一个会填入MaterialPropertyBlock,这个可以用来做GPUInstance,传入Block变量在Shader中使用。
这里有个int返回值,这个相当于一把钥匙,标记当前Batch在BatchGroup的Index。
我们需要使用这个Index来用我们的第三个接口GetBatchMatrices,这个会获取矩阵列表,标记Mesh的Transform信息,我们需要往里填入矩阵。
接下来我们大部分工作都是在Cull里边进行了。
Cull的方法定义是这样的
JobHandle OnPerformCulling(BatchRendererGroup rendererGroup, BatchCullingContext cullingContext)
两个参数,BatchRendererGroup,这个就是你构造的那个Group,这个是用来区分可能你同一个Cull函数会生成多个Group,第二个BatchCullingContext,这个是你需要填入的Cull结果。BatchCullingContext有四个变量
//
      // 摘要:
      //   Planes to cull against.
      public readonly NativeArray<Plane> cullingPlanes;
      //
      // 摘要:
      //   See Also: LODParameters.
      public readonly LODParameters lodParameters;
      //
      // 摘要:
      //   Visibility information for the batch.
      public NativeArray<BatchVisibility> batchVisibility;
      //
      // 摘要:
      //   Array of visible indices for all the batches in the group.
      public NativeArray<int> visibleIndices;前两个只读的变量是给我们的信息,第一个是相机的六个面,第二个是LOD参数,里边也是相机的参数,诸如正交还是透视,FOV这些。
下面两个参数是我们需要填入的Cull结果。
上面的是标记可视物体总数以及一些只读的信息,下面这个是可视物体的Index,标记第几个是可视的。
最关键的地方是这个可以返回一个JobHandle,也就是说可以利用Unity的JobSystem。
有了上面的理论基础,我们开始着手解决我们的问题。
以植被为例,思路是这样的:
我们把所有的植被信息,包括植被的平移,旋转,缩放,颜色(或其他GPUInstance的变量)Mesh,LodMesh,Lod信息,材质等等,保存成Asset,在游戏运行时,这部分GameObject就可以删掉了。
我们拿到Asset后开始构造RenderBatchGroup,通过Asset里的GPUInstance变量构建Block给Shader使用,然后将LodMesh和Mesh分别使用AddBatch添加,通过平移,旋转,缩放,构建矩阵填入Batch,剩下的工作就是在Cull多线程执行Cull工作了。Cull的原理也比较简单,一个是LOD决定显示哪一级,一个是视椎体裁剪。
我们先来看看效果


https://www.zhihu.com/video/1209201304478162944


在FrameDebug中可以看到
基本就两个批次,当然这里得用强大的URP (SRP)Batch和GPUInstance配合使用。
贴一下我的测试工程代码以及工程源码(毕竟公司的正式代码放上来不太好)
namespace YY
{
    using System;
    using System.Collections.Generic;
    using Unity.Burst;
    using Unity.Collections;
    using Unity.Jobs;
    using Unity.Mathematics;
    using Unity.Rendering;
    using UnityEngine;
    using UnityEngine.Jobs;
    using UnityEngine.Rendering;

    /// <summary>
    /// The test script for batch render.
    /// </summary>
    public sealed class BatchRender : MonoBehaviour
    {
      
      private Mesh mesh;

      
      private Mesh lowMesh;

      
      private float lodDis;

      
      private Material material;

      public bool log = false;

      private BatchRendererGroup batchRendererGroup;

      NativeArray<CullData> cullData;
      private void Awake()
      {
            batchRendererGroup = new BatchRendererGroup(this.OnPerformCulling);
            cullData = new NativeArray<CullData>(25000, Allocator.Persistent);

            for (int j = 0; j < 50; j++)
            {
                var pos = new float3;
                var rot = new quaternion;
                var scale = new float3;
                for (int i = 0; i < 50; i++)
                {
                  pos = new float3(i * 2, 0, j*2);
                  rot = quaternion.identity;
                  scale = new float3(1.0f, 1.0f, 1.0f);
                }
                this.AddBatch(100*j, 50, pos, rot, scale);
            }
      }
      public void AddBatch(int offset,int count,float3[] pos, quaternion[] rot, float3[] scale)
      {
            AABB localBond;
            localBond.Center = this.mesh.bounds.center;
            localBond.Extents = this.mesh.bounds.extents;
            MaterialPropertyBlock block = new MaterialPropertyBlock();
            var colors = new List<Vector4>();
            for(int i=0;i<count;i++)
            {
                colors.Add(new Vector4(UnityEngine.Random.Range(0f,1f), UnityEngine.Random.Range(0f, 1f), UnityEngine.Random.Range(0f, 1f), UnityEngine.Random.Range(0f, 1f)));
            }
            block.SetVectorArray("_BaseColor", colors);
            var batchIndex = this.batchRendererGroup.AddBatch(
                this.mesh,
                0,
                this.material,
                0,
                ShadowCastingMode.On,
                true,
                false,
                new Bounds(Vector3.zero, 1000 * Vector3.one),
                count,
                block,
                null);
            var matrices = this.batchRendererGroup.GetBatchMatrices(batchIndex);
            for (int i=0;i< count; i++)
            {
                matrices = float4x4.TRS(pos, rot, scale);
                var aabb = AABB.Transform(matrices, localBond);
                cullData = new CullData()
                {
                  bound = aabb,
                  position = pos,
                  minDistance = 0,
                  maxDistance = lodDis,
                };
            }
            for (int i = 0; i < count; i++)
            {
                colors = new Vector4(UnityEngine.Random.Range(0f, 1f), UnityEngine.Random.Range(0f, 1f), UnityEngine.Random.Range(0f, 1f), UnityEngine.Random.Range(0f, 1f));
            }
            block.SetVectorArray("_BaseColor", colors);
            batchIndex = this.batchRendererGroup.AddBatch(
                this.lowMesh,
                0,
                this.material,
                0,
                ShadowCastingMode.On,
                true,
                false,
                new Bounds(Vector3.zero, 1000 * Vector3.one),
                count,
                block,
                null);
            matrices = this.batchRendererGroup.GetBatchMatrices(batchIndex);
            for (int i = 0; i < count; i++)
            {
                matrices = float4x4.TRS(pos, rot, scale);
                var aabb = AABB.Transform(matrices, localBond);
                cullData = new CullData()
                {
                  bound = aabb,
                  position = pos,
                  minDistance = lodDis,
                  maxDistance = 10000,
                };
            }
      }
      private void OnDestroy()
      {
            if (this.batchRendererGroup != null)
            {
                cullingDependency.Complete();
                this.batchRendererGroup.Dispose();
                this.batchRendererGroup = null;
                cullData.Dispose();
            }
      }
      JobHandle cullingDependency;
      private JobHandle OnPerformCulling(
            BatchRendererGroup rendererGroup,
            BatchCullingContext cullingContext)
      {
            var planes = FrustumCullPlanes.BuildSOAPlanePackets(cullingContext.cullingPlanes, Allocator.TempJob);
            var lodParams = LODGroupExtensions.CalculateLODParams(cullingContext.lodParameters);
            var cull = new MyCullJob()
            {
                Planes = planes,
                LODParams = lodParams,
                IndexList = cullingContext.visibleIndices,
                Batches = cullingContext.batchVisibility,
                CullDatas = cullData,
            };
            var handle = cull.Schedule(100, 32, cullingDependency);
            cullingDependency = JobHandle.CombineDependencies(handle, cullingDependency);
            return handle;
      }
      struct CullData
      {
            public AABB bound;
            public float3 position;
            public float minDistance;
            public float maxDistance;
      }

      
      struct MyCullJob : IJobParallelFor
      {
             public LODGroupExtensions.LODParams LODParams;
             public NativeArray<FrustumCullPlanes.PlanePacket4> Planes;
             public NativeArray<CullData> CullDatas;

             public NativeArray<BatchVisibility> Batches;
             public NativeArray<int> IndexList;

            public void Execute(int index)
            {
                var bv = Batches;
                var visibleInstancesIndex = 0;
                var isOrtho = LODParams.isOrtho;
                var DistanceScale = LODParams.distanceScale;
                for (int j = 0; j < bv.instancesCount; ++j)
                {
                  var cullData =CullDatas;
                  var rootLodDistance = math.select(DistanceScale * math.length(LODParams.cameraPos - cullData.position), DistanceScale, isOrtho);
                  var rootLodIntersect = (rootLodDistance < cullData.maxDistance) && (rootLodDistance >= cullData.minDistance);
                  if (rootLodIntersect)
                  {
                        var chunkIn = FrustumCullPlanes.Intersect2NoPartial(Planes, cullData.bound);
                        if (chunkIn != FrustumCullPlanes.IntersectResult.Out)
                        {
                            IndexList = j;
                            visibleInstancesIndex++;
                        }
                  }
                }
                bv.visibleCount = visibleInstancesIndex;
                Batches = bv;
            }
      }
    }
}

Unity版本:2019.3或以上
工程链接:https://pan.baidu.com/s/1NNOLr8RIEjSUy835fqFZ8A
提取码:err7


在项目过程中发现,我在Unity2019.2上使用BatchRendererGroup,在Android上有内存泄漏问题,在2019.3上没问题,不知道是LWRP的问题,还是Unity本身在2019.3修复了这个bug。
这个Batch一次只能1023个物体,跟GPUInstance的限制好像是一样的。注意拆分


鉴于好多人提到DrawMeshInstanced,来说一下我的理解。
首先,DrawMeshInstanced实质上是一个把一堆物体合成一个大物体,用这个大物体的bounding box在Unity的Render管理中进行剔除等操作,也就是说这一堆物体要么都渲染,要么都不渲染。RenderBatchGroup里的物体不会参与到Unity场景Render的剔除,他真正的是管理每一个物体的剔除,也就是说如果有1000个小物体他们会分别计算剔除。最重要的是,RenderBatchGroup给你的是一组相机参数,这意味着我们能在这里能对于每个View分别做LOD相关的东西,我们一般的场景不算RT的话有5个View(MainCamera,CSM的4个ShadowCamera)。
DrawMeshInstancedIndirect这项技术倒是比较完美,但是奈何现在移动对于他的支持不太够,没法大规模使用到移动平台。
页: [1]
查看完整版本: Unity大世界超多物体渲染---BatchRendererGroup