Search Unity

Question DrawMeshInstanced and Constant Buffers

Discussion in 'General Graphics' started by MateiGiurgiu, Aug 26, 2022.

  1. MateiGiurgiu

    MateiGiurgiu

    Joined:
    Aug 13, 2013
    Posts:
    23
    Hi all,

    For our mobile game, we have the challenge of rendering and animating lots of army units at the same time (it's a strategy game). Since it's a mobile game we decided that using animators and skinned meshes it's not the best option. Therefore, I created a system for baking the animation data to a texture and using it to animate the units in the vertex shader. Additionally, I made a custom render system that is simply using Graphics.DrawMeshInstanced to draw units of the same type (sharing the same texture and animation texture) in a single batch. Now, all these individual units, have quite a lot of data that needs to be sent to the shader. For instance, the current animation frame (that will be used to sample the animation texture at the proper coordinate), colors, and other properties related to our game behavior. I call this PerRenderData. In order to send the PerRenderData, every batch has a GraphicsBuffer, which, before calling the draw call, has some data copied to it from a NativeArray using GraphicsBuffer.SetData. The NativeArray contains the PerRenderData for every instance inside the batch. After the PerRenderData is copied into the buffer, I call material.SetConstantBuffer and pass the Graphics Buffer as an argument. Finally, I issue the Graphics.DrawMeshInstanced call.

    Now, everything is working fine on the devices (Android and iOS). However, in the editor, it seems like the constant buffer data is not sent properly. It is very weird behavior. However, it works when adding the -force-gles command line argument to the Unity Hub, before opening the project (basically forcing Unity to render using GLES emulation).

    To illustrate the issue, I made a sample project with minimal code, that simply renders two batches (cubes and spheres) using the same method. The expected behavior is to see each instance of the cubes and spheres scaling randomly between 1 and 1.25. The scaling is done in the vertex shader by using the data from the constant buffer which is sent by calling buffer.SetData and material.SetConstantBuffer.

    Expected Behaviour (this happens only when opening the Editor with -force-gles)
    Constant Buffer - GLES.gif

    Current Behaviour (when using DX11). Yes, this is a GIF, but everything is static, the Scale property seems like is not sent to the shader at all.
    Constant Buffer - DX11.gif

    Please find attached the sample project. It only contains 2 small scripts and a shader. However, I will attach the scripts here, as well. Please, let me know if you have any clue what can go wrong, it is something that I'm doing wrong? Or is it a bug with Unity? Any input is much appreciated. Thank you!

    Editor Version used: 2020.3.36f1. Also tested with 2021.3.0f and the behaviour is the same.

    CustomRenderSystem.cs - attached to the camera
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Runtime.InteropServices;
    5. using Unity.Collections;
    6. using UnityEngine;
    7. using UnityEngine.Rendering;
    8.  
    9. public class BatchData
    10. {
    11.     public Mesh Mesh;
    12.     public Material Material;
    13.     public List<CustomRenderer> Renderers;
    14.     public GraphicsBuffer GpuBuffer;
    15.  
    16.     public BatchData(Mesh mesh, Material material)
    17.     {
    18.         Mesh = mesh;
    19.         Material = material;
    20.         Renderers = new List<CustomRenderer>();
    21.         CreateGpuBuffer(Renderers.Count + 5);
    22.     }
    23.  
    24.     public void AddRenderer(CustomRenderer customRenderer)
    25.     {
    26.         Renderers.Add(customRenderer);
    27.  
    28.         // increase the size of the GPU buffer
    29.         if (Renderers.Count > GpuBuffer.count)
    30.         {
    31.             GpuBuffer.Release();
    32.             CreateGpuBuffer(Renderers.Count + 5);
    33.         }
    34.     }
    35.  
    36.     private void CreateGpuBuffer(int count)
    37.     {
    38.         GpuBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Constant, count, Marshal.SizeOf(typeof(CustomRendererData)));
    39.     }
    40. }
    41.  
    42. public class CustomRenderSystem : MonoBehaviour
    43. {
    44.     const int MAX_BATCH_COUNT = 500;
    45.  
    46.  
    47.     // the batch list will only have two elements: one for the sphere and one for the cube
    48.     private BatchData[] batchList = new BatchData[2];
    49.  
    50.     private int _filteredCount = 0;
    51.     Matrix4x4[] _matrix4X4Array = new Matrix4x4[MAX_BATCH_COUNT];
    52.     CustomRenderer[] _filteredRenderers = new CustomRenderer[MAX_BATCH_COUNT];
    53.     NativeArray<CustomRendererData> _filteredRendererData;
    54.     private int _renderDataSize;
    55.  
    56.     void Awake()
    57.     {
    58.         _filteredRendererData = new NativeArray<CustomRendererData>(MAX_BATCH_COUNT, Allocator.Persistent);
    59.         _renderDataSize = Marshal.SizeOf(typeof(CustomRendererData));
    60.     }
    61.  
    62.     private void Update()
    63.     {
    64.         for(int i = 0; i < batchList.Length; i++)
    65.         {
    66.             BatchData batchData = batchList[i];
    67.             if (batchData != null)
    68.             {
    69.                 Render(batchData);
    70.             }
    71.         }
    72.     }
    73.  
    74.     private void OnDestroy()
    75.     {
    76.         if (_filteredRendererData != null)
    77.         {
    78.             _filteredRendererData.Dispose();
    79.         }
    80.  
    81.         for (int i = 0; i < batchList.Length; i++)
    82.         {
    83.             BatchData batchData = batchList[i];
    84.             if (batchData != null)
    85.             {
    86.                 batchData.GpuBuffer.Release();
    87.             }
    88.         }
    89.     }
    90.  
    91.     // filters out disabled or null renderers and also populate the Matrix array and the render data array
    92.     private void ProcessRenderers(List<CustomRenderer> renderers)
    93.     {
    94.         int rendererCount = renderers.Count;
    95.         _filteredCount = 0;
    96.  
    97.         for (int i = 0; i < rendererCount; i++)
    98.         {
    99.             if (renderers[i] != null && renderers[i].enabled)
    100.             {
    101.                 _filteredRenderers[_filteredCount] = renderers[i];
    102.                 _filteredRendererData[_filteredCount] = renderers[i].RenderData;
    103.                 _matrix4X4Array[_filteredCount] = renderers[i].GetTrsMatrix();
    104.                 _filteredCount++;
    105.             }
    106.         }
    107.     }
    108.  
    109.     private void Render(BatchData batchData)
    110.     {
    111.         Mesh mesh = batchData.Mesh;
    112.         Material material = batchData.Material;
    113.  
    114.         List<CustomRenderer> renderers = batchData.Renderers;
    115.         GraphicsBuffer gpuBuffer = batchData.GpuBuffer;
    116.  
    117.         ProcessRenderers(renderers);
    118.  
    119.         if (_filteredCount == 0) return;
    120.  
    121.  
    122.         gpuBuffer.SetData(_filteredRendererData, 0, 0, _filteredCount);
    123.  
    124.         material.SetConstantBuffer("UnityInstancing_PerUnitData", gpuBuffer, 0, _renderDataSize * _filteredCount);
    125.         Graphics.DrawMeshInstanced(mesh, 0, material, _matrix4X4Array, _filteredCount, null, ShadowCastingMode.Off, false);
    126.     }
    127.  
    128.     public void AddSphereRenderer(CustomRenderer customRenderer)
    129.     {
    130.         if (batchList[0] == null)
    131.         {
    132.             batchList[0] = new BatchData(customRenderer.Mesh, customRenderer.Material);
    133.         }
    134.  
    135.         batchList[0].AddRenderer(customRenderer);
    136.     }
    137.  
    138.     public void AddCubeRenderer(CustomRenderer customRenderer)
    139.     {
    140.         if (batchList[1] == null)
    141.         {
    142.             batchList[1] = new BatchData(customRenderer.Mesh, customRenderer.Material);
    143.         }
    144.  
    145.         batchList[1].AddRenderer(customRenderer);
    146.     }
    147. }
    148.  

    CustomRenderer.cs - attached to every GO that I want to render. It also has references to the material and the mesh to be used.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. // the data inside a constant buffer has to aligned to 16 bytes,
    6. //so that's why I added 3 unused float
    7. public struct CustomRendererData
    8. {
    9.     public float Scale;
    10.     public float Unused1;
    11.     public float Unused2;
    12.     public float Unused3;
    13. }
    14.  
    15. public class CustomRenderer : MonoBehaviour
    16. {
    17.     public Mesh Mesh;
    18.     public Material Material;
    19.     public CustomRendererData RenderData;
    20.  
    21.     [SerializeField] private bool IsCube;
    22.  
    23.     void Start()
    24.     {
    25.         // get the render system instance
    26.         CustomRenderSystem customRenderSystem = Camera.main.GetComponent<CustomRenderSystem>();
    27.  
    28.         if (IsCube)
    29.         {
    30.             customRenderSystem.AddCubeRenderer(this);
    31.         }
    32.         else
    33.         {
    34.             customRenderSystem.AddSphereRenderer(this);
    35.         }
    36.     }
    37.  
    38.     void Update()
    39.     {
    40.         RenderData.Scale = Mathf.Lerp(RenderData.Scale, Random.Range(1f, 1.25f), 0.25f);
    41.     }
    42.  
    43.     public Matrix4x4 GetTrsMatrix()
    44.     {
    45.         return transform.localToWorldMatrix;
    46.     }
    47.  
    48.     private void OnDrawGizmos()
    49.     {
    50.         if(IsCube)
    51.         {
    52.             Gizmos.DrawCube(transform.position, Vector3.one);
    53.         }
    54.         else
    55.         {
    56.             Gizmos.DrawSphere(transform.position, 0.5f);
    57.         }
    58.     }
    59. }
    60.  
    The shader.
    Code (CSharp):
    1. Shader "Unlit/InstancingShader"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "RenderType"="Opaque" }
    10.         LOD 100
    11.  
    12.         Pass
    13.         {
    14.             CGPROGRAM
    15.             #pragma vertex vert
    16.             #pragma fragment frag
    17.             #pragma multi_compile_instancing
    18.             #pragma instancing_options forcemaxcount:200
    19.             #pragma instancing_options nolightprobe
    20.             #pragma instancing_options nolightmap
    21.             #pragma instancing_options assumeuniformscaling
    22.  
    23.             #include "UnityCG.cginc"
    24.  
    25.             UNITY_INSTANCING_BUFFER_START(PerUnitData)
    26.                 UNITY_DEFINE_INSTANCED_PROP(float, Scale)
    27.                 UNITY_DEFINE_INSTANCED_PROP(float, Unused1)
    28.                 UNITY_DEFINE_INSTANCED_PROP(float, Unused2)
    29.                 UNITY_DEFINE_INSTANCED_PROP(float, Unused3)
    30.             UNITY_INSTANCING_BUFFER_END(PerUnitData)
    31.  
    32.             struct appdata
    33.             {
    34.                 float4 vertex : POSITION;
    35.                 float2 uv : TEXCOORD0;
    36.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    37.             };
    38.  
    39.             struct v2f
    40.             {
    41.                 float4 vertex : SV_POSITION;
    42.                 float2 uv : TEXCOORD0;
    43.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    44.             };
    45.  
    46.             sampler2D _MainTex;
    47.             float4 _MainTex_ST;
    48.  
    49.             v2f vert (appdata v)
    50.             {
    51.                 v2f o;
    52.                 UNITY_INITIALIZE_OUTPUT(v2f, o);
    53.                 UNITY_SETUP_INSTANCE_ID(v);
    54.                 UNITY_TRANSFER_INSTANCE_ID(v, o);
    55.  
    56.                 v.vertex *= UNITY_ACCESS_INSTANCED_PROP(PerUnitData, Scale);
    57.                 o.vertex = UnityObjectToClipPos(v.vertex);
    58.                 o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    59.                 return o;
    60.             }
    61.  
    62.             fixed4 frag (v2f i) : SV_Target
    63.             {
    64.                 // sample the texture
    65.                 fixed4 col = tex2D(_MainTex, i.uv);
    66.  
    67.                 return col;
    68.             }
    69.             ENDCG
    70.         }
    71.     }
    72. }
    73.  
     

    Attached Files:

    Last edited: Aug 26, 2022
  2. MateiGiurgiu

    MateiGiurgiu

    Joined:
    Aug 13, 2013
    Posts:
    23
    Also, if you are wondering why I am not using a structured buffer for storing the PerRenderData, is because we are trying to support lower-end Android devices using ARM GPU and OpenGL which doesn't support SSBO binging in the vertex shader. (On Vulkan is fine, but if it's an older ARM GPU using GLES3, then it won't work). This is indicated by SystemInfo.maxComputeBufferInputsVertex being 0.
     
  3. MateiGiurgiu

    MateiGiurgiu

    Joined:
    Aug 13, 2013
    Posts:
    23
    It seems like I found the issue! The problem happens only when the count argument of the GraphicsBuffer.SetData is lower than the GraphicsBuffer.count. In other words, it is not possible to have a GraphicBuffer of 100 allocated elements and copy data only for the first 50 of them. Can someone at Unity confirm if this is the intended behavior?
     
    jjbish, hypnoslave and joshuacwilde like this.
  4. MateiGiurgiu

    MateiGiurgiu

    Joined:
    Aug 13, 2013
    Posts:
    23