Search Unity

Resolved How to sort 2D objects with same Z position in shader

Discussion in 'Shaders' started by Tony_Max, Oct 10, 2022.

  1. Tony_Max

    Tony_Max

    Joined:
    Feb 7, 2017
    Posts:
    351
    I was working on 2D sprite rendering DOTS + URP compatible solution. Using regular URP (not 2D) to render sprites. There is some ugly things in my system which I want to try to get rid of.

    In my system I can render sprites with different materials/shaders, but to do that I need to collect all, then sort and finally render in resulted order. Because, as I mentioned, there can be more then one shader, then we can have different passes. I've decided to get LTW of sprites and shift position's Z a bit to display sprites in order. For now it is .0001f per position in order, because camera can't recognize difference below that. I'm not a huge fan of some hidden constant values, but such solution also brings me a need to adjust camera's clipping planes to be able to fit all rendered sprites + editor and runtime camera have a different Z shift minimum + in scene view with 3D mode enabled rendering result looks weird, like some kind of accordion, which is getting recognizable with a lot of sprites on a scene.

    In general i just change LTW's Z position I passing to shader's matrices buffer to be able to render passes all at once and remain sprites in proper order.

    So is there any good clean ways to do what I want to do?

    Shader code:
    Code (CSharp):
    1. //TODO: try to simplify #if defined strings
    2. Shader "Universal Render Pipeline/2D/General Sprite Shader"
    3. {
    4.     Properties
    5.     {
    6.         _MainTex("_MainTex", 2D) = "white" {}
    7.     }
    8.  
    9.     HLSLINCLUDE
    10.     #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    11.  
    12.     CBUFFER_START(UnityPerMaterial)
    13.         //set here material properties
    14.     CBUFFER_END
    15.     ENDHLSL
    16.  
    17.     SubShader
    18.     {
    19.         Tags {"Queue" = "Transparent" "RenderType" = "TransparentCutout" "RenderPipeline" = "UniversalPipeline" }
    20.  
    21.         Blend SrcAlpha OneMinusSrcAlpha
    22.         Cull Off
    23.         ZWrite On
    24.  
    25.         Pass
    26.         {
    27.             Tags { "LightMode" = "UniversalForward" "Queue" = "Transparent" "RenderType" = "TransparentCutout"}
    28.  
    29.             HLSLPROGRAM
    30.             #pragma vertex UnlitVertex
    31.             #pragma fragment UnlitFragment
    32.  
    33.             #pragma target 4.5
    34.             #pragma exclude_renderers gles gles3 glcore
    35.             #pragma multi_compile_instancing
    36.             #pragma instancing_options procedural:setup
    37.  
    38.             struct Attributes
    39.             {
    40.                 float3 positionOS   : POSITION;
    41.                 float2 uv            : TEXCOORD0;
    42.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    43.             };
    44.  
    45.             struct Varyings
    46.             {
    47.                 float4  positionCS        : SV_POSITION;
    48.                 float2    uv                : TEXCOORD0;
    49.  
    50.                 float4  mainTexAtlasST  : ATLASST;
    51.  
    52.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    53.             };
    54.  
    55.             TEXTURE2D(_MainTex);
    56.             SAMPLER(sampler_MainTex);
    57.  
    58. #if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
    59.             StructuredBuffer<float4x4>   _transformMatrixBuffer;
    60.             StructuredBuffer<float4>     _mainTexSTOnAtlasBuffer; //ST means Scale + Translation
    61.             StructuredBuffer<float4>     _mainTexSTBuffer;
    62.             StructuredBuffer<int>        _flipBuffer;
    63. #endif
    64.  
    65.             void setup()
    66.             {
    67. #if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
    68.                 unity_ObjectToWorld = _transformMatrixBuffer[unity_InstanceID];
    69. #endif
    70.             }
    71.  
    72.             float2 TilingAndOffset(float2 UV, float2 Tiling, float2 Offset)
    73.             {
    74.                 //Tiling is like Width/Height ratio, like how much texture should be stratched
    75.                 //offset is just regular offset from 0,0
    76.                 //so when UV.x is 0/1 it is left/right UV coords of renderer rect
    77.                 return UV * Tiling + Offset;
    78.             }
    79.             Varyings UnlitVertex(Attributes attributes, uint instanceID : SV_InstanceID)
    80.             {
    81.                 Varyings varyings = (Varyings)0;
    82.  
    83.                 //extract all CBuffer data here
    84. #if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
    85.                 varyings.mainTexAtlasST = _mainTexSTOnAtlasBuffer[instanceID];
    86.                 float4 mainTexST = _mainTexSTBuffer[instanceID];
    87.                 int flipValue = _flipBuffer[instanceID];
    88. #else
    89.                 //fallback if somehow instancing failed
    90.                 varyings.mainTexAtlasST = float4(1, 1, 0, 0);
    91.                 float4 mainTexST = float4(1, 1, 0, 0);
    92.                 int flipValue = 0;
    93. #endif
    94.                 UNITY_SETUP_INSTANCE_ID(attributes);
    95.                 UNITY_TRANSFER_INSTANCE_ID(attributes, varyings);
    96.  
    97.                 varyings.positionCS = TransformObjectToHClip(attributes.positionOS);
    98.  
    99.                 float2 uv = attributes.uv;
    100.  
    101.                 //flip uv if necessary
    102.                 uv.x = flipValue >= 0 ? uv.x : (1.0 - uv.x);
    103.  
    104.                 //tiling and offset uv
    105.                 uv = TilingAndOffset(uv, mainTexST.xy, mainTexST.zw);
    106.  
    107.                 //pass uv to fragment shader
    108.                 varyings.uv = uv;
    109.  
    110.                 return varyings;
    111.             }
    112.  
    113.             float4 UnlitFragment(Varyings varyings) : SV_Target
    114.             {
    115.                 //finally frac uv and locate on atlas using tiling and offset
    116.                 varyings.uv = TilingAndOffset(frac(varyings.uv), varyings.mainTexAtlasST.xy, varyings.mainTexAtlasST.zw);
    117.  
    118.                 float4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, varyings.uv);
    119.                 clip(texColor.w - 0.5);
    120.                 return texColor;
    121.             }
    122.             ENDHLSL
    123.         }
    124.     }
    125.  
    126.     Fallback "Sprites/Default"
    127. }
    128.  
    Adjusting LTWs job code. Just to show what I actually do, look at
    PER_INDEX_OFFSET * index
    Code (CSharp):
    1. [BurstCompile]
    2.         internal struct FillMatricesArrayJob : IJobParallelFor
    3.         {
    4.             [ReadOnly] public NativeArray<SpriteData> spriteDataArray;
    5.             [ReadOnly] public NativeList<RenderArchetypeForSorting> archetypeLayoutData;
    6.             [WriteOnly][NativeDisableParallelForRestriction] public NativeArray<float4x4> matricesArray;
    7.  
    8.             private const float PER_INDEX_OFFSET = .0001f; //below this value camera doesn't recognize difference
    9.  
    10.             public void Execute(int index)
    11.             {
    12.                 var spriteData = spriteDataArray[index];
    13.                 var renderPosition = spriteData.position - spriteData.scale * spriteData.pivot;
    14.                 matricesArray[archetypeLayoutData[spriteData.archetypeIncludedIndex].stride + spriteData.entityInQueryIndex] = float4x4.TRS
    15.                 (
    16.                     new float3(renderPosition.x, renderPosition.y, PER_INDEX_OFFSET * index),
    17.                     quaternion.identity,
    18.                     new float3(spriteData.scale.x, spriteData.scale.y, 1f)
    19.                 );
    20.             }
    21.         }
    Example of how it looks like in scene view
    upload_2022-10-11_1-45-33.png
     
  2. Tony_Max

    Tony_Max

    Joined:
    Feb 7, 2017
    Posts:
    351
    I've found that it is possible to override depth buffer using SV_Depth signature as output in fragment shader. Simply higher the pixel the less depth he writes in depth buffer
    depth = 1 - 1.0 / ypos
    . But such approach really affects GPU performance. I'm wondering maybe there is some similar concept of how to test pixels depending on some value but more optimal then through ZTest
     
  3. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,336
    The problem with this fragment shader output is it disables early depth rejection, meaning every single sprite is being fully rendered by the GPU, and then it’s throwing away fragments that are occluded. Normally one of the advantages of using alpha testing with
    ZWrite
    over alpha blending is you get that early depth rejection so any parts of the surface that are fully occluded (at that point in rendering, which means sorting still being important) are “rejected” and don’t get calculated by the GPU.

    There is a way around this though. Modify the z of the vertex shader’s output clip space instead. This is in effect exactly the same as using
    SV_Depth
    , but doesn’t disable early depth rejection.

    Code (CSharp):
    1.  varyings.positionCS.z = wantedDepth * varyings.positionCS.w;
     
    Goularou, Tony_Max and lilacsky824 like this.