I’m working on custom sprite render system on top of DOTS ECS. My system already can work with sprites with custom shaders (which supports instancing). Drawing process is simple:
- get all sprites which need to be drawed
- sort them depending on your rules (in my case by screen Y position)
- find all sequences of sprites which can be drawned together
- gather positions and other properties data within resulting order and write it in compute buffers
- render through
Graphics.DrawMeshInstancedProcedural
Why i need to do step 3? Because sprites with different shaders / textures can’t be drawed together. But when i , for example, have 1k sprites and >1 shaders, then i gets a lot of render groups. And trying to draw all this groups leads to high SetPassCalls count, also render process of all this groups takes too much time.
I’ve tried to enable ZWrite in shader, set sprite matrix position Z depening on it’s order in draw process, but that leads to sprite overlapping (images).
I gues fully opaque sprites can avoid all this complications, but i need not only opaque sprites. So i see no really way to render whole group, because it can be blend in with other group, but maybe i can somehow decrease draw CPU cost. Any ideas? Please help!
sprite shader
Shader "Universal Render Pipeline/2D/SimpleSpriteShader"
{
Properties
{
_MainTex("_MainTex", 2D) = "white" {}
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
ENDHLSL
SubShader
{
Tags {"Queue" = "Transparent" "RenderType" = "TransparentCutout" "RenderPipeline" = "UniversalPipeline" }
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
ZWrite On
Pass
{
Tags { "LightMode" = "UniversalForward" "Queue" = "Transparent" "RenderType" = "TransparentCutout"}
HLSLPROGRAM
#pragma vertex UnlitVertex
#pragma fragment UnlitFragment
#pragma target 4.5
#pragma exclude_renderers gles gles3 glcore
#pragma multi_compile_instancing
#pragma instancing_options procedural:setup
struct Attributes
{
float3 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
float4 _MainTex_ST;
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
int _instanceIDOffset;
StructuredBuffer<float4x4> _transformMatrixBuffer;
StructuredBuffer<float4> _colorBuffer;
#endif
void setup()
{
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
unity_ObjectToWorld = _transformMatrixBuffer[_instanceIDOffset + unity_InstanceID];
#endif
}
Varyings UnlitVertex(Attributes attributes, uint instanceID : SV_InstanceID)
{
Varyings varyings = (Varyings)0;
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
varyings.color = _colorBuffer[_instanceIDOffset + instanceID];
#else
varyings.color = float4(0.5, 0.5, 0.5, 0.5);
#endif
UNITY_SETUP_INSTANCE_ID(attributes);
UNITY_TRANSFER_INSTANCE_ID(attributes, varyings);
varyings.positionCS = TransformObjectToHClip(attributes.positionOS);
varyings.uv = TRANSFORM_TEX(attributes.uv, _MainTex);
varyings.uv = attributes.uv; //what is this?
return varyings;
}
float4 UnlitFragment(Varyings varyings) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(varyings);
return varyings.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, varyings.uv);
}
ENDHLSL
}
}
Fallback "Sprites/Default"
}
UPD: I’ve tried to add clip() function to shader to skip fully transparent pixels and this works. Now no matter what order of drawing all sprites are visible depending on it’s Z position. But it stops me from use transparent sprites, because they must be drawned strictly from back to front. Another reason why i think this solution is bad is that clip function kinda bunned everywhere in terms of performance ( this thread for example). Performance is great on my pc though.