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(&#34;_BaseColor&#34;, 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(&#34;_BaseColor&#34;, 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]