Draw Sorting

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.

For arbitrary sorting of semi-transparent surfaces the only real option is to draw them in the order you want then to appear in front to back.

Sorting of arbitrary transparent geometry is an unsolved issue for real time rendering. Full stop.

There are many existing techniques referred to as “order-independent transparency”, aka OIT, that allows for sorting of arbitrary transparent surfaces. But the two main high level approaches require rendering the scene with multiple passes (Depth Peeling), or a potentially unbounded amount of memory for the render target (per pixel linked lists or a-buffers). These are both very expensive. There are also approximate OIT methods, like stochastic transparency and Weighted Blended. Stochastic is noisy as it approximates transparency with opaque surfaces using random noise to dither. And weighted blended only works correctly with surfaces that mostly transparent, it explicitly fails with nearly opaque surfaces.

The real solution is:
Don’t use different shaders / textures per sprite. Use a single texture atlas and use a single shader for everything. If you need unique shader effects per-sprite, use if statements in the shader driven by instanced properties to enable and disable those features.
That really is the only solution.

That or stick with sorting them like you are.

1 Like

First of all, thank you for your answer and for all your answers on forum, them are so helpful!

I’ve come up to the same point of view, because in games where we have tons of sprites like factorio, there is no lots of effects, no semi-transparent sprites, all can be in the same atlas and be rendered with ± one shader.
But since i’ve already implemented both approaches, optimal one for opaque/cutout shaders, which places sprites to proper Z position, and expensive one that finds groups and render them in actual order, i can make setting for each layer that i use, and if somewhere whenever i’ll need semi-transparent sprite i’ll be able to turn on second approach per layer.