Unity技术学习:渲染大量物体的解决方案,外加RenderMesh、RenderMeshInstanced、RenderMeshIndirect的简单使用

叠甲:本人比较菜,如果哪里不对或者有认知不到的地方,欢迎锐评(不玻璃心)!

导师留了个任务,渲染大量的、移动的物体。

先总结一下:

静态批处理、动态批处理、SRPBatch、GpuInstance(推荐插件:GPUInstancer)、AnimationInstace(官方插件)、手动合批

寻找解决方案:

当时找了几个解决方案:

静态批处理:

要求:字如其意,必须是静态的,不能移动旋转缩放等等。

原理:Unity 会自动将相同材质球的物体合并到一个大的mesh里,提取顶点和索引数据放到共享的顶点缓冲和索引缓冲

并不减少drawcall,减少了渲染状态的设置。

缺点;在处理的时候会把相同模型的顶点信息都保存,并变换到世界空间。会导致包体和运行时体积变大。

哪怕勾上,上万个物体就寄了(虽然灵活度很高),只需要勾上就可以了。

动态批处理:

要求:不超过300个顶点(不超过总计900个属性),不包含镜像的缩放,材质一样,物体lightmap指向的位置一样

原理:运行时将所有共享同一材质的模型顶点信息变换到世界空间,一次drawcall 绘制多个模型。

内置渲染管线:

URP:(HDRP没有)

真正意义上的减少drawcall。

缺点:会有额外的cpu性能消耗,而且新一代的图形api在批次间消耗降低,反而使用动态批处理可能更差。

对于打断动态批处理的情况:

  • 若优先级低,会自动禁用:SRPBatch>静态批处理>GPUinstance>动态批处理
  • 多passShader自动禁用动态批处理
  • shadow casters可以使用不用材质,但shadow casters pass使用的参数是相同的就不会禁用动态批处理。
  • 向前渲染路径,如果一个物体接受了多个光照,会为每个per pixel light 产生提交和绘制,从而附加多个Pass导致无法合批。

SRPBatch:

原理:简化了批处理的渲染循环,可以加速相同着色器变体的多种材质在场景中的CPU渲染速度。

要求:Shader中的变体一致,对象不可以是粒子或蒙皮网格(好像最新版已经支持蒙皮了),位置不相邻且中间夹杂着不同shader或不同变体的其他物体,不会进行同批处理。

这个详细阅读一下官方文档,这个本文不做深入研究,但注意:srpBatch与GPUinstance不兼容!(参考上方的优先级)

Unity - 手册:可编写脚本的渲染管线批处理程序

URP:(HDRP默认开启)

缺点:

降低setpass call的消耗

GPUInstance:

要求:同材质、同mesh,默认材质仅支持transform的改变。支持meshRenderer和Graphics.RenderMesh,不支持SkinMeshRenderer。缩放为负的情况下会失效,代码动态改变材质变量就不算同一材质,就会失效,但是可以通过将颜色变化等变量加入常量缓冲区实现。仅支持一个实时光,需要多个光源只能切换到延迟渲染路径。

原理:仅绘制一个,剩下的“复制”。

只需要勾上材质里的GPUInstancing、当使用该材质时,就会合批。但经实测,仅支持完全相同的物体,方块和球使用相同材质并不会合批。而且:不要对顶点少于 256 个的网格使用 GPU 实例化(官方文档提醒)。

减少了draw call 

灵活度也挺高的,但是对于超过上限(貌似是1023?)的物体数量,依旧拉胯,batch数虽然不是猛增但是帧率感人(后续可能会做性能分析)

RenderMeshIndirect:

原理:手动GPUInstance多个对象。

跑了一下示例,woc,吊的一批,就是每个块可控性比较差(也可能是我比较菜,目前已知的就是:使用动画贴图、时间来控制每个单体的行为;而且没有碰撞、单独操作难度高:需要在shader里写改变顶点位置),但是,一次性绘制10w,600帧没有一点儿压力。1000w个能跑十几帧(视觉效果拉满)。而且支持光照和阴影。

所以接下来我从 RenderMesh到RenderMeshInstanced再到RenderMeshIndirect的API给说一下,并且跑一下示例:

 RenderMesh:

使用给定的渲染参数渲染网格(就单纯绘制网格,没有任何合批)。

public static void RenderMesh(ref RenderParams rparams, Mesh mesh, int submeshIndex, Matrix4x4 objectToWorld, Nullable<Matrix4x4> prevObjectToWorld = null);

rparams:

camera用于渲染的相机。如果设置为 null(默认),则为所有摄像机渲染。
layer用于渲染的图层。要使用的图层。
lightProbeProxyVolume用于渲染的光探针代理体积 (LPPV)。
lightProbeUsage光探头使用的类型。
material用于渲染的材质。
matProps用于渲染的材质属性。
motionVectorMode用于渲染的运动矢量模式。
receiveShadows描述渲染的几何体是否应接收阴影。
reflectionProbeUsage用于渲染的反射探针的类型。
rendererPriority渲染器优先级。
renderingLayerMask用于渲染的渲染器图层蒙版。
shadowCastingMode描述几何体是否应投射阴影。
worldBounds定义几何体的世界空间边界。用于对渲染的几何体进行剔除和排序。

MaterialPropertyBlock-CSDN博客

submeshIndex:

当网格体包含多个材质(子网格)时,子网格体 Unity 的索引会呈现。对于具有单个材质的网格,请使用值 0(这个是啥子,冲浪了好久,但是找不到)

objectToWorld:

Unity 用于将网格从对象转换为世界空间的转换矩阵。

prevObjectToWorld:

Unity 使用前面的帧变换矩阵来计算网格的运动矢量。

using UnityEngine;

public class ExampleClass : MonoBehaviour
{
    public Material material;
    public Mesh mesh;

    void Update()
    {
        RenderParams rp = new RenderParams(material);
        rp.camera = Camera.main;
        rp.layer = 6;
        rp.matProps = new MaterialPropertyBlock();
        rp.worldBounds = new Bounds(new Vector3(0,0,0),new Vector3(1,1,1));
        for (int i = 0; i < 10; ++i)
            Graphics.RenderMesh(rp, mesh, 0, Matrix4x4.Translate(new Vector3(-6.5f + i, 0.0f, 5.0f)));
    }
}

RenderMeshInstanced:

使用 GPU 实例渲染网格的多个实例(但是不会传入命令)。

public static void RenderMeshInstanced(RenderParams rparams, Mesh mesh, int submeshIndex, NativeArray<T> instanceData, int instanceCount = -1, int startInstance = 0);

instanceData:

用于呈现实例的实例数据数组。

instanceCount:

要呈现的实例数。当此参数为 -1(默认值)时,Unity 会呈现从数组到末尾的所有实例。

startInstance:

要呈现的第一个实例。

using UnityEngine;

public class ExampleClass : MonoBehaviour
{
    public Material material;
    public Mesh mesh;
    const int numInstances = 1000;

    struct MyInstanceData
    {
        public Matrix4x4 objectToWorld;
        public float myOtherData;
        public uint renderingLayerMask;
    };

    void Update()
    {
        RenderParams rp = new RenderParams(material);
        MyInstanceData[] instData = new MyInstanceData[numInstances];
        for (int i = 0; i < numInstances; ++i)
        {
            instData[i].objectToWorld = Matrix4x4.Translate(new Vector3(-4.5f + i*3, 0.0f, 5.0f));
            instData[i].renderingLayerMask = (i & 1) == 0 ? 1u : 2u;
        }
        Graphics.RenderMeshInstanced(rp, mesh, 0, instData);
    }
}

RenderMeshIndirect:

与RenderMeshInstanced类似,但是可以从command buffer中获取命令参数,并且只需要调用一次即可执行。

public static void RenderMeshIndirect(ref RenderParams rparams, Mesh mesh, GraphicsBuffer commandBuffer, int commandCount = 1, int startCommand = 0); 

commandBuffer:

 把命令打包的buffer

using UnityEngine;

public class ExampleClass : MonoBehaviour
{
    public Material material;
    public Mesh mesh;

    GraphicsBuffer commandBuf;
    GraphicsBuffer.IndirectDrawIndexedArgs[] commandData;
    const int commandCount = 1;
    public uint   num=100;
    void Start()
    {
        commandBuf = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, commandCount, GraphicsBuffer.IndirectDrawIndexedArgs.size);
        commandData = new GraphicsBuffer.IndirectDrawIndexedArgs[commandCount];
    }

    void OnDestroy()
    {
        commandBuf?.Release();
        commandBuf = null;
    }

    void Update()
    {
        RenderParams rp = new RenderParams(material);
        rp.worldBounds = new Bounds(Vector3.zero, 10000 * Vector3.one); // use tighter bounds for better FOV culling
        rp.matProps = new MaterialPropertyBlock();
        rp.matProps.SetMatrix("_ObjectToWorld", Matrix4x4.Translate(new Vector3(0f, 0, 0)));
        commandData[0].indexCountPerInstance = mesh.GetIndexCount(0);
        commandData[0].instanceCount = num;


        commandBuf.SetData(commandData);
        Graphics.RenderMeshIndirect(rp, mesh, commandBuf, commandCount);
    }
}

示例shader: 

Shader "ExampleShader"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
            #include "UnityIndirect.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 color : COLOR0;
            };

            uniform float4x4 _ObjectToWorld;

            v2f vert(appdata_base v, uint svInstanceID : SV_InstanceID)
            {
                InitIndirectDrawArgs(0);
                v2f o;
                uint cmdID = GetCommandID(0);
                uint instanceID = GetIndirectInstanceID(svInstanceID);
                float timeOffset = _Time.y * 0.5; // 调整运动速度
                float xOffset = sin(timeOffset + instanceID *5) * 10; // x方向偏移
                float yOffset = cos(timeOffset + instanceID * 7) * 10; // y方向偏移
                float zOffset = sin(timeOffset + instanceID * 9) *10; // z方向偏移
              
                float4 wpos = mul(_ObjectToWorld, v.vertex + float4(instanceID%1000+ xOffset, cmdID+ yOffset, instanceID / 1000 *3+zOffset, 0));
                o.pos = mul(UNITY_MATRIX_VP, wpos);
                o.color = float4(cmdID & 1 ? 0.0f : 1.0f, cmdID & 1 ? 1.0f : 0.0f, instanceID / float(GetIndirectInstanceCount()), 0.0f);
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                return i.color;
            }
            ENDCG
        }
    }
}

100w个方块能跑150帧,cool!

手动合批:

对于UI这种,可以把一堆绘制到一张mesh上,可以看下底下这个文章,是我对高性能飘字的探索:(顺便手动合批了一下)

unity UGUI高性能飘字解决方案(对象池+合并网格)-CSDN博客

但是!!!!!

GPUInstance不支持skinMeshRenderer,也就是不支持骨骼动画,对于大量的3d物体(比如有100只行为各异的怪,自带骨骼动画)。

解决方案1:

对于这种:还有一个技术能解决这个问题(只减少drawcall,大概能做到帧数翻倍吧)AnimationInstance,这个请各位自行搜索。之前试了试跑不通。

解决方案2:

把3d物体变成2d的帧动画(大多数游戏可能就是这么干的,可以用2d的手动合批等手段提升性能)。

 小总结: 

(抄过来的,侵删)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值