Hi,
I want to use Graphics.DrawMeshInstanced for showing thousands of 2d objects with different materials.
The problem is that I can’t handle rendering order of overlapping objects which have different materials.
I have no problem if I use DrawMesh, but it has serious performance issues.
I implemented this tutorial but it only use one material.
OK guys, I’m struggling with this problem about 10 days.
So I try to explain more about the issue.
I want to render thousands of objects with different sheet and materials.
There is two things I’m trying to handle: Rendering the overlapping objects properly and keep the “SetPass calls” as low as possible.
If I use Graphics.DrawMesh() the rendering is ok, but the “SetPass calls” will differ based on objects positions.
Here is my code and the result for that:
Mesh mesh = Utils.CreateMesh(1f, 1f);
MaterialPropertyBlock materialPropertyBlock = new MaterialPropertyBlock();
Camera camera = Camera.main;
Vector4[] uv = new Vector4[1];
int shaderPropertyId = Shader.PropertyToID("_MainTex_UV");
Entities.ForEach((ref SpriteSheetRendererComponent sprite) =>
{
uv[0] = sprite.uv;
materialPropertyBlock.SetVectorArray(shaderPropertyId, uv);
Graphics.DrawMesh
(
mesh,
sprite.matrix,
LevelManager.instance.spriteSheetMaterials[sprite.materialID],
0,
camera,
0,
materialPropertyBlock
);
}).WithoutBurst().Run();
The result:
As you can see the “SetPass calls” is high. And if I change the objects position, the “SetPass calls” changes.
And if I want to use Graphics.DrawMeshInstanced() I have two options: rendering objects of same material first and then rendering another material, or render objects based on their position which has lots of calculation and same resulat as using DrawMesh().
Here is the code when I rendering the objects based on their materials:
EntityQuery entityQuery = GetEntityQuery(typeof(Translation), typeof(SpriteSheetRendererComponent));
if (entityQuery.CalculateEntityCount() == 0) return;
NativeArray<SpriteSheetRendererComponent> sheetDataArray = entityQuery.ToComponentDataArray<SpriteSheetRendererComponent>(Allocator.TempJob);
NativeArray<Translation> translationArray = entityQuery.ToComponentDataArray<Translation>(Allocator.TempJob);
// Sorting objects from top to bottom
for (int i = 0; i < translationArray.Length; i++)
{
for (int j = i + 1; j < translationArray.Length; j++)
{
if (translationArray[i].Value.y < translationArray[j].Value.y)
{
Translation tmp = translationArray[i];
translationArray[i] = translationArray[j];
translationArray[j] = tmp;
SpriteSheetRendererComponent tmpSheet = sheetDataArray[i];
sheetDataArray[i] = sheetDataArray[j];
sheetDataArray[j] = tmpSheet;
}
}
}
Mesh mesh = Utils.CreateMesh(1f, 1f);
MaterialPropertyBlock materialPropertyBlock = new MaterialPropertyBlock();
for(int x = 0; x < LevelManager.instance.spriteSheetMaterials.Length; x++)
{
List<Matrix4x4> matrixList = new List<Matrix4x4>();
List<Vector4> uvList = new List<Vector4>();
for(int i = 0; i < sheetDataArray.Length; i++)
{
if(sheetDataArray[i].materialID == x)
{
matrixList.Add(sheetDataArray[i].matrix);
uvList.Add(sheetDataArray[i].uv);
}
}
if(uvList.Count > 0)
{
materialPropertyBlock.SetVectorArray("_MainTex_UV", uvList);
Graphics.DrawMeshInstanced(
mesh, 0, LevelManager.instance.spriteSheetMaterials[x], matrixList, materialPropertyBlock
);
}
}
sheetDataArray.Dispose();
translationArray.Dispose();
And the result:
The “SetPass calls” is 2 but objects of different materials not rendering properly. Each object which is higher will render in back.
Here is my shader if you think it’s related:
Shader "Custom/InstancedShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
LOD 100
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
//ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
//float4 _MainTex_UV;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _MainTex_UV)
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = UnityObjectToClipPos(v.vertex);
//o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv = (v.uv * UNITY_ACCESS_INSTANCED_PROP(Props, _MainTex_UV).xy) + UNITY_ACCESS_INSTANCED_PROP(Props, _MainTex_UV).zw;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
// sample the texture
fixed4 c = tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
return c;
}
ENDCG
}
}
}
After about 20 days I finally succeeded to solve the issue.
The problem was actually from the shader.
It seems the youtuber in tutorial had no knowledge about shaders as I.
With fixes to shader there is no need to sorting entities based on their Y positions.
Here is the shader:
Shader "Custom/InstancedShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
LOD 100
Cull Back
ZWrite On
Blend SrcAlpha OneMinusSrcAlpha
//Blend One OneMinusSrcAlpha
Pass
{
//ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
//float4 _MainTex_UV;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _MainTex_UV)
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
float3 worldPosition = float3(v.vertex.x, v.vertex.y, -v.vertex.y / 100);
// o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex = UnityObjectToClipPos(float4(worldPosition, 1.0f));
//o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv = (v.uv * UNITY_ACCESS_INSTANCED_PROP(Props, _MainTex_UV).xy) + UNITY_ACCESS_INSTANCED_PROP(Props, _MainTex_UV).zw;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
clip(col.a - 30.0 / 255.0);
col.rgb *= col.a;
return col;
}
ENDCG
}
}
}
And here is the system for rendering which is not completely performant and is just for demo:
(it needs slicing for more than 1023 objects of same material)
EntityQuery entityQuery = GetEntityQuery(typeof(Translation), typeof(SpriteSheetRendererComponent));
if (entityQuery.CalculateEntityCount() == 0) return;
NativeArray<SpriteSheetRendererComponent> sheetDataArray = entityQuery.ToComponentDataArray<SpriteSheetRendererComponent>(Allocator.TempJob);
NativeArray<Translation> translationArray = entityQuery.ToComponentDataArray<Translation>(Allocator.TempJob);
Mesh mesh = Utils.CreateMesh(1f, 1f);
for(int x = 0; x < LevelManager.instance.spriteSheetMaterials.Length; x++)
{
MaterialPropertyBlock materialPropertyBlock = new MaterialPropertyBlock();
List<Matrix4x4> matrixList = new List<Matrix4x4>();
List<Vector4> uvList = new List<Vector4>();
for(int i = 0; i < sheetDataArray.Length; i++)
{
if(sheetDataArray[i].materialID == x)
{
matrixList.Add(sheetDataArray[i].matrix);
uvList.Add(sheetDataArray[i].uv);
}
}
if(uvList.Count > 0)
{
materialPropertyBlock.SetVectorArray("_MainTex_UV", uvList);
Graphics.DrawMeshInstanced(
mesh, 0, LevelManager.instance.spriteSheetMaterials[x], matrixList, materialPropertyBlock
);
}
}
sheetDataArray.Dispose();
translationArray.Dispose();
I also tested this approach:
It has some issues when you have lots of spriteSheets, and also didn’t work on some mobile devices.
So I just grabbed the things I needed from its shader to fix my problem.
How is your project going? Would you consider releasing a full example project? Most of the high performance sprite rendering solutions seem to require ECS/DOTS, which I am unfamiliar with.
I released the project but it has serious issues on some android devices as shader instancing is not supported with some GPUs.
I’m not sure about performance if you don’t use ECS.
As long as the objects are static there is no problem. But if you have thousands of objects and they’re moving I think you have to use ECS.
Start ECS today, it’s scary but it’s really great.
Hey!
I updated the original CodeMonkey code to the newest syntax and I’m trying to add multiple material support. No joy. I’ve pulled what little hair I have left. If you guys got any tips, examples I would be overjoyed. Honestly, I will take divination at this point, as long as it gets me going.
I’m trying to wrap my head around ECS. After OOP it’s a hard road but I’m doing my best.
Cheers!
good job,lt working on my project, thank you a lot
I just logged in to comment on this. THIS SAVED MY LIFE! The shader solution is just perfect for my case. I was going up and down on the internet to have solution about having different materials for different entities animations by their sprite sheets, and this just saved my life. Thank you, i really appreciate it <3